栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

入门实战---个人博客

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

入门实战---个人博客

个人博客,项目简单,需求明确,容易上手,非常适合做为练手级项目。

blog.mszlu.com

项目讲解说明:

  1. 提供前端工程,只需要实现后端接口即可
  2. 项目以单体架构入手,先快速开发,不考虑项目优化,降低开发负担
  3. 开发完成后,开始优化项目,提升编程思维能力
  4. 比如页面静态化,缓存,云存储,日志等
  5. docker部署上线
  6. 云服务器购买,域名购买,域名备案等

项目使用技术 :

springboot + mybatisplus+redis+mysql

目录

目录

一. 工程搭建

二.  首页展示

1.文章列表

2. 首页---最热标签

3  首页--最热文章

4. 最新文章显示

 5. 文章归档显示

三. 登录

1. 登录功能实现

 2. 获取用户信息

 3. 退出登录

 四. 用户注册

五. 登录拦截器

一. 工程搭建

1.1 前端用vscode打开并运行

npm run dev

运行结果

 1.2 创建maven工程

导入依赖


        
            org.springframework.boot
            spring-boot-starter
            
            
                
                    org.springframework.boot
                    spring-boot-starter-logging
                
            
        
        
            com.qiniu
            qiniu-java-sdk
            [7.7.0, 7.7.99]
        
        
        
            org.springframework.boot
            spring-boot-starter-log4j2
        

        
            org.springframework.boot
            spring-boot-starter-aop
        

        
            org.springframework.boot
            spring-boot-starter-mail
        
        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
            com.alibaba
            fastjson
            1.2.76
        
        
            mysql
            mysql-connector-java
        
        
            org.springframework.boot
            spring-boot-configuration-processor
            true
        
        
            org.apache.commons
            commons-lang3
        
        
            commons-collections
            commons-collections
            3.2.2
        
        
        
            commons-codec
            commons-codec
        
        
            org.apache.commons
            commons-lang3
            3.12.0
        
        
            com.baomidou
            mybatis-plus-boot-starter
            3.4.3
        
        
            org.projectlombok
            lombok
        
        
        
            joda-time
            joda-time
            2.10.10
        
        
            org.springframework.boot
            spring-boot-starter-jdbc
        
        
            io.jsonwebtoken
            jjwt
            0.9.1
        
    
   
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                2.6.7
            
        
        
            
                src/main/resources
                
                    ***.properties
                
            
        
    

1.3 application配置文件配置

#server
server.port= 8888
#应用名称
spring.application.name=hnjm_blog
# datasource
spring.datasource.url=jdbc:mysql://localhost:3306/hnjm_blog?useUnicode=true&characterEncoding=UTF-8&serverTimeZone=UTC
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
#mybatis-plus
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
#标识表名均为ms_前缀
mybatis-plus.global-config.db-config.table-prefix=ms_
mybatis-plus.type-aliases-package=com.hnjm.pojo
mybatis-plus.mapper-locations=classpath:com/hnjm/mapper
    private String createDate;

    private String author;

   // private ArticleBodyVo body;

    private List tags;

    private CategoryVo category;

}

说明:java中long数据能表示的范围比js中number大,在跟前端交互时,这样也就意味着部分数值在js中存不下(变成不准确的值)。
解决办法可以这样:
使用fastjson的ToStringSerializer注解,让系统序列化时,保留相关精度。

(7)TagVo.java

@Data
public class TagVo {

    private Long id;

    private String tagName;

    private String avatar;
}

(8)CategoryVo.java

@Data
public class CategoryVo {

    private Long id;

    private String avatar;

    private String categoryName;

    private String description;
}

(9)在param文件中创建PageParams.java

根据接口文档中,前端传给后端接口的json数据,我们都封装为param类,方便操作。
PageParams 类定义了分页查询的page和pageSize,分别对应分页查询中的start和size。 

@Data
public class PageParams {

    private int page = 1;

    private int pageSize = 5;

    private Long categoryId;

    private Long tagId;

    private String year;

    private String month;

    public String getMonth(){
        if (this.month != null && this.month.length() == 1){
            return "0"+this.month;
        }
        return this.month;
    }
}

 1.2.3 Mapper接口

此处定义了三个mapper接口,分别为ArticleMapper、SysUserMapper、TagMapper。

public interface ArticleMapper extends BaseMapper {
}
public interface TagMapper extends BaseMapper {
    List findTagsByArticleId(Long articleId);
}
public interface SysUserMapper extends BaseMapper {
} 

说明:

因为tag和article有一张对应的表,所以要查询article对应的tag时,需要设计到多表的查询,但是,mybatisplus不支持多表查询,所以需要自己写mapper.xml文件。
TagMapper.xml







    
        select id,avatar,tag_name as tagName from ms_tag
        
            id in
            (select tag_id from ms_article_tag where article_id = #{articleId})
        
    

注意:mapper.xml文件所在目录要和mapper对应,本次工程都在com.hnjm.mapper下。
可以在application.properties中mybatis-plus开启驼峰命名
mybatis-plus.configuration.map-underscore-to-camel-case=true
这样SQL语句就不需要as别名。

1.2.4 Service

该阶段定义了三个service接口类。

public interface ArticleService {
    //分页查询文章列表
    Result listArticle(PageParams pageParams);
}
public interface SysUserService {
    //根据文章id获取用户信息
    SysUser findUserById(Long id);
}
public interface TagService {
    //通过文章id查询ui赢得标签,有一张表专门映射文章id和标签id
    List findTagsByArticleId(Long articleId);
}

serviceImpl类

(1)ArticleServiceImpl
该实现类目前实现了文章分页查询。

知识点:
1)Page类定义分页对象;
2)LambdaQueryWrapper定义查询wrapper;
3)selectPage()函数返回的是Page对象,通过getRecords获得Article对象列表。
4)copy和copyList函数实现Article到ArticleVo的转换
5)BeanUtils.copyProperties(article,articleVo),可以实现对象之间的复制,相同属性名复制,不同属性名为null。
 

@Service
public class ArticleServiceImpl implements ArticleService{
   @Autowired
   ArticleMapper articleMapper;
   @Autowired
   TagService tagService;
   @Autowired
   SysUserService sysUserService;
   //分页查询
   @Override
   public Result listArticle(PageParams pageParams) {
       //分页
       Page page = new Page<>(pageParams.getPage(),pageParams.getPageSize());

       LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
       //先置顶排序由属性weight决定,后按照时间排序
       wrapper.orderByDesc(Article::getWeight,Article::getCreateDate);
       Page articlePage = articleMapper.selectPage(page, wrapper);
       //文章列表
       List records = articlePage.getRecords();
       //因为页面展示出来的数据不一定和数据库数据一致,所以我们要做一个拷贝
       //将查出的数据复制到articleVo中实现解耦,vo和页面数据交换
       List articleVo = copyList(records,true,true);

       return Result.success(articleVo);
   }

   //copyList实现
   private List copyList(List records,boolean isTag,boolean isAuthor) {
       ArrayList articleVos = new ArrayList<>();
       for(Article article:records){
           articleVos.add(copy(article,isTag,isAuthor));
       }
       return articleVos;
   }
   //这个方法是主要点是BeanUtils,又Spring提供的,专门用来拷贝的,Article和articlevo相同属性的拷贝过来返回
   private ArticleVo copy(Article article,boolean isTag,boolean isAuthor){
       ArticleVo articleVo = new ArticleVo();
       BeanUtils.copyProperties(article,articleVo);
       //joda包中的DataTime.toString方法将Article的Long日期属性转为ArticleVo中的字符串日期属性
       articleVo.setCreateDate(new DateTime(article.getCreateDate()).toString("yyyy-MM-dd HH:mm"));
       //是否显示标签和作者
       if(isTag){
           articleVo.setTags(tagService.findTagsByArticleId(article.getId()));
       }
       if(isAuthor){
           articleVo.setAuthor(sysUserService.findUserById(article.getAuthorId()).getNickname());
       }
       return articleVo;
   }
}

(2) TagMapperServiceImpl
此处也会涉及对象之间的复制,原理同ArticleServiceImpl,这里实现的是Tag类复制为TagVo类。

@Service
public class TagServiceImpl implements TagService{
   @Autowired
   TagMapper tagMapper;
   @Override
   public List findTagsByArticleId(Long articleId) {
       List tags = tagMapper.findTagsByArticleId(articleId);
       return copyList(tags);
   }
   //copyList实现
   private List copyList(List tags) {
       ArrayList tagVos = new ArrayList<>();
       for(Tag tag : tags){
           tagVos.add(copy(tag));
       }
       return tagVos;
   }
   private TagVo copy(Tag tag){
       TagVo tagVo = new TagVo();
       //BeanUtils,copyProperties用于类之间的复制,相同字段复制,不同字段为null
       BeanUtils.copyProperties(tag,tagVo);
       return tagVo;
   }

}

(3)SysUserServiceImpl.java:根据id查找用户信息

@Service
public class SysUserServiceImpl implements SysUserService{
   @Autowired
   SysUserMapper sysUserMapper;
   @Override
   public SysUser findUserById(Long id) {
       return sysUserMapper.selectById(id);
   }
}

运行结果

 也可以在postman 中测试

2. 首页---最热标签

2.1 接口文档

接口url:/tags/hot

请求方式:GET

请求参数:无

返回数据:

思路分析:

思路:
1)首先在ms_article_tag表操作,通过tag_id分组并排序获得前几名,返回一个tag_id列表。
2)然后根据tag_id列表查询ms_tag表中对应的id和tagName将查询结果返回为TagVo对象(作者使用的是返回为Tag对象,但是由于前端展示的都是vo类,所以我们转换为TagVo)。

 2.2 编码实现

2.2.1 controller

@RestController
@RequestMapping("/tags")
public class TagController {
    @Autowired
    TagService tagService;

    //tags/hot相应最热标签tag对象
    @RequestMapping("/hot")
    public Result hot() {
        int limit = 6;//最热六个
        return tagService.hots(limit);
    }
}

2.2.2 TagMapper.java  

public interface TagMapper extends BaseMapper {
    //根据id 查找tag列表
    List findTagsByArticleId(Long articleId);
    //查询最热标签前limit条
    List findHotsTagId(int limit);
    //通过最热标签tagid查询最热tags
    List findTagsByIds(List tagIds);
}

TagMapper对应的Mapper文件
重点:
1)findHotsTagId涉及分组并排序
2)findTagsByIds涉及到foreach标签TagMapper.xml

 
    
        select id, tag_name from ms_tag
        where id in
        
            #{tagId}
        
    

2.2.3  TagService.java

Result hots(int limit);

  TagServiceImp.java

 //查询若干个最热标签功能
    public Result hots(int limit){
        List hotsTagId = tagMapper.findHotsTagId(limit);
        //判断hotsTagId是否为空
        if(CollectionUtils.isEmpty(hotsTagId)){
            return Result.success(Collections.emptyList());
        }
        List tagsList = tagMapper.findTagsByIds(hotsTagId);
        return Result.success(tagsList);
    }

2.3   统一异常处理

定义Handler包,设置统一异常处理类AllExceptionHandler ,不管是controller层还是service,dao层,都有可能报异常,如果是预料中的异常,可以直接捕获处理,如果是意料之外的异常,需要统一进行处理,进行记录,并给用户提示相对比较友好的信息。

//对加了@Controller的方法进行拦截处理,AOP实现
@ControllerAdvice
public class AllExceptionHandler {
    //进行异常处理,处理Exception.class异常
    @ExceptionHandler(Exception.class)
    //返回json数据
    @ResponseBody
    public Result doExceptionHandler(Exception e) {
        return Result.fail(-999,"系统异常,抱歉!");
    }
}

说明:

1)@ResponseBody  :返回jsojn数据
2)@ControllerAdvice:对加了@Controller的方法进行拦截处理,AOP实现
3)@ExceptionHandler(Exception.class)://进行异常处理,处理Exception.class异常

运行结果

3  首页--最热文章

3.1  接口文档

接口url:/articles/hot

请求方式:POST

返回数据:

 3.2 编码实现

思路:原理同最热标签查询。根据view_count排序,选择出最热文章。

(1)Articlecontroller

    //显示最热文章
    @PostMapping("/hot")
    public Result hotArticle(){
        int limit = 3;
        return articleService.hotArticle(limit);
    }

(2)ArticleService

Result hotArticle(int limit);

(3)ArticleServiceImpl

//根据view_counts字段查询最热文章
    @Override
    public Result hotArticle(int limit) {
        LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
        wrapper.orderByDesc(Article::getViewCounts)
                .select(Article::getId,Article::getTitle)
                .last("limit "+limit);
        List articles = articleMapper.selectList(wrapper);
        return Result.success(copyList(articles,false,false));
    }

运行结果

4. 最新文章显示

4.1 接口说明

接口url:/articles/new

请求方式:POST

返回数据

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
            "id": 1,
            "title": "springboot介绍以及入门案例",
        },
        {
            "id": 9,
            "title": "Vue.js 是什么",
        },
        {
            "id": 10,
            "title": "Element相关",
            
        }
    ]
}

4.2  编程实现

思路:原理和最热文章完全相同,只是通过create_date字段排序,选择出最新

(1)ArticleController

//显示最新文章
    @PostMapping("/new")
    public Result newArticle(){
        int limit = 3;
        return articleService.newArticle(limit);
    }

(2)ArticleService

    Result newArticle(int limit);

(3)ArticleServiceImpl

/最新文章
    @Override
    public Result newArticle(int limit) {
        LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
        wrapper.orderByDesc(Article::getCreateDate)
                .select(Article::getId,Article::getTitle)
                .last("limit "+limit);
        List articles = articleMapper.selectList(wrapper);
        return Result.success(copyList(articles,false,false));
    }

运行结果

 5. 文章归档显示

5.1 接口说明

接口url:/articles/listArchives

请求方式:POST

返回数据:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": [
        {
            "year": "2021",
            "month": "6",
            "count": 2
        }
            
    ]
}

5.2  编程实现

思路:

根据接口文档分析,归档查询涉及到数据库内部函数Year、Month,所以MybatisPlus不能实现,需要通过mapper.xml文件实现。
文章归档返回的对象为Archives(档案)类,将这个类放入vo包中。
(1)  Archives.java

@Data
public class Archives {
    private Integer year;
    private Integer month;
    private Long count;

}

(2) ArticleController中添加方法

//文章归档
    @PostMapping("/listArchives")
    public Result listArchives(){
        return articleService.listArchives();
    }

(3)ArticleMapper.java 中添加方法

List listArchives();

对应的ArticleMapper.xml文件中添加方法





    
    
        select YEAR(FROM_UNIXTIME(create_date/1000)) as year,
          MONTH(FROM_UNIXTIME(create_date/1000)) as month,
          count(*) as count
          from ms_article
          group by year,month
    

说明:数据库中create_date 为bigint 13位,直接year()不行,需要先转date型后year()。ROM_UNIXTIME 格式化MYSQL时间戳函数

将bigint类型转换为时间数据类型 from_unixtime(1488470400000/1000)

(4)  ArticleService与ArticleServiceImpl

Result listArchives();
//文章归档
    @Override
    public Result listArchives() {
        List archivesList = articleMapper.listArchives();
        return Result.success(archivesList);
    }

运行结果:

三. 登录

1. 登录功能实现

1.1  接口说明

接口url:/login

请求方式:POST

请求参数:

参数名称参数类型说明
accountstring账号
passwordstring密码

 返回数据:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": "token"
}

1.2  编程实现

(1)工具类:在util中添加JWT工具类

JWT

登录使用JWT技术。

jwt 可以生成 一个加密的token,做为用户登录的令牌,当用户登录成功之后,发放给客户端。

请求需要登录的资源或者接口的时候,将token携带,后端验证token是否合法。

jwt 有三部分组成:A.B.C

A:Header,{"type":"JWT","alg":"HS256"} 固定

B:playload,存放信息,比如,用户id,过期时间等等,可以被解密,不能存放敏感信息

C: 签证,A和B加上秘钥 加密而成,只要秘钥不丢失,可以认为是安全的。

jwt 验证,主要就是验证C部分 是否合法。

public class JWTUtils {
    private static final String jwtToken = "123456Mszlu!@#$$";

    public static String createToken(Long userId){
        Map claims = new HashMap<>();
        claims.put("userId",userId);
        JwtBuilder jwtBuilder = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法,秘钥为jwtToken
                .setClaims(claims) // body数据,要唯一,自行设置
                .setIssuedAt(new Date()) // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 60 * 1000));// 一天的有效时间
        String token = jwtBuilder.compact();
        return token;
    }

    public static Map checkToken(String token){
        try {
            Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);
            return (Map) parse.getBody();
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;

    }

}

(2)LoginParam (根据接口文档中参数定义)

import lombok.Data;

@Data
public class LoginParam {
    private String account;
    private  String password;
    private String  nickname;
}

(3)LoginController

@RestController
@RequestMapping("/login")
public class LoginController {

    @Autowired
    private LoginService loginService;
    //登录
    @PostMapping
    public Result login(@RequestBody LoginParam loginParam){
        return loginService.login(loginParam);
    }
}

(3)LoginService(登录由login去处理)

  Result login(LoginParam loginParam);

(4)需要开启redis服务器,并在配置文件中加载

 在application.properties配置文件中加上

#redis配置
spring.redis.host=localhost
spring.redis.port=6379

 (5)ErrorCode.class
因为每次登录都有错误验证,所以在vo包中定义了一个ErrorCode类
ErrorCode.class

public enum ErrorCode {

    PARAMS_ERROR(10001,"参数有误"),
    ACCOUNT_PWD_NOT_EXIST(10002,"用户密码不存在喔!"),
    TOKEN_ERROR(10003,"Token不合法"),
    ACCOUNT_EXIST(10004,"账号已存在"),
    NO_PERMISSION(70001,"无访问权限"),
    SESSION_TIME_OUT(90001,"会话超时"),
    NO_LOGIN(90002,"未登录"),;


    private int code;
    private String msg;

    ErrorCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}
@Service
public class LoginServiceImpl implements LoginService {

    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private RedisTemplate redisTemplate;
    //md5加密使用到的盐
    private static final String salt="mszlu!@#";
    @Override
    public Result login(LoginParam loginParam) {
        
        String account = loginParam.getAccount();
        String password = loginParam.getPassword();

        //用户名或者密码为空
        if(StringUtils.isBlank(account)||StringUtils.isBlank(password)){
            //提前写好的错误编码类,方便使用
            return Result.fail(ErrorCode.PARAMS_ERROR.getCode(),ErrorCode.PARAMS_ERROR.getMsg());
        }

        //这里使用的DigestUtils是来自于commons-codec包的,需要外部导入依赖
        //密码加盐,因为数据库中的密码是经过盐加密的
        String pwd = DigestUtils.md5Hex(password+salt);
        System.out.println(pwd);
        //验证时需要通过sysUserService查询到对应用户并返回部分用户信息
        SysUser user = sysUserService.findUser(account, pwd);
        //用户不存在
        if(user == null){//错误信息调用错误码
            return Result.fail(ErrorCode.ACCOUNT_PWD_NOT_EXIST.getCode(),ErrorCode.ACCOUNT_PWD_NOT_EXIST.getMsg());
        }
        //用户密码正确,给用户返回一个JWT (token)
        String token = JWTUtils.createToken(user.getId());
        //存入redis,要确保已开启redis
        redisTemplate.opsForValue().set("TOKEN_"+token, JSON.toJSONString(user),1, TimeUnit.DAYS);
        return Result.success(token);
    }
}

在SysUserService中添加方法

//根据账号、密码查找
    SysUser findUser(String account, String password);

SysUserServiceImpl

    @Override
    public SysUser findUser(String account, String password) {
            LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper();
            queryWrapper.eq(SysUser::getAccount,account);
            queryWrapper.eq(SysUser::getPassword,password);
            queryWrapper.select(SysUser::getId,SysUser::getAccount,SysUser::getAvatar,SysUser::getNickname);
            queryWrapper.last("limit 1");//加快速度
            SysUser sysUser = sysUserMapper.selectOne(queryWrapper);
            return sysUser;
        }

最后在postman中测试,切记不要在前端测试,因为显示用户名信息没有写完,会死掉的

 

 2. 获取用户信息

2.1 接口说明

接口url:/users/currentUser

请求方式:GET

请求参数:

参数名称参数类型说明
Authorizationstring头部信息(TOKEN)

返回数据

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": {
        "id":1,
        "account":"1",
        "nickaname":"1",
        "avatar":"ss"
    }
}

2.2  编码实现

思路:登陆后用户的token会存放在浏览器本地,当用户登陆时会在请求头携带token,token中含有用户的id,用户的信息实际存放在redis中。

(1)UserController

@RestController
@RequestMapping("/users")
public class UserController {
    @Autowired
    SysUserService sysUserService;

    //用户信息展示请求
    //token放在请求头中,所以要获取token需要@RequestHeader("Authorization")注解
    @GetMapping("/currentUser")
    public Result currentUser(@RequestHeader("Authorization") String token){
        return sysUserService.findUserByToken(token);
    }
}

(2)SysUserService

Result findUserByToken(String token);
SysUser checkToken(String token);//从reddis中获取用户信息

(3)LoginUserVo(根据接口文档中定义类)

   该类用于返回用户信息,由于页面展示用户部分信息

import lombok.Data;

@Data
public class LoginUserVo {
    private  Long id;

    private String account;

    private String nickname;

    private String avatar; //头像
}

(4) SysUserServiceImpl

    @Service
public class SysUserServiceImpl implements SysUserService {
    @Autowired
    private  SysUserMapper sysUserMapper;
    @Override
    public SysUser findUserById(Long id) {
        return sysUserMapper.selectById(id);
    }

    @Override
    public SysUser findUser(String account, String password) {
        LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper();
        queryWrapper.eq(SysUser::getAccount, account);
        queryWrapper.eq(SysUser::getPassword, password);
        queryWrapper.select(SysUser::getId, SysUser::getAccount, SysUser::getAvatar, SysUser::getNickname);
        queryWrapper.last("limit 1");//加快速度
        SysUser sysUser = sysUserMapper.selectOne(queryWrapper);
        return sysUser;
    }
    @Autowired
    private RedisTemplate redisTemplate;
    @Override
    public Result findUserByToken(String token) {
        
        SysUser user=checkToken(token);
        LoginUserVo loginUserVo = new LoginUserVo();
        loginUserVo.setAccount(user.getAccount());
        loginUserVo.setId(user.getId());
        loginUserVo.setAvatar(user.getAvatar());
        loginUserVo.setNickname(user.getNickname());
        return Result.success(loginUserVo);
    }
    public SysUser checkToken(String token) {
        if(StringUtils.isBlank(token)){//如果对象为空
            return null;
        }
        Map map = JWTUtils.checkToken(token);//解析token
        if(map == null){
            return null;
        }
        String  userJson= redisTemplate.opsForValue().get("TOKEN_"+token);
        if(StringUtils.isBlank(userJson)){
            return null;
        }
        //token解析成功,并且redis存在
        //JSON.parseObject将json对象转为SysUser对象
        SysUser sysUser=JSON.parseObject(userJson,SysUser.class);//将json对象转换为use
        return sysUser;
    }
    @Override
    public SysUser findUserByAccount(String account) {
        LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SysUser::getAccount,account)
                .last("limit 1");
        return this.sysUserMapper.selectOne(wrapper);
    }
    @Override
    public void save(SysUser sysUser) {
        //保存用户id会自动生成
        //默认生成分布式id,采用雪花算法
        //mybatis-plus
        sysUserMapper.insert(sysUser);
    }
}

运行结果

 3. 退出登录

3.1 接口文档

接口url:/logout

请求方式:GET

请求参数:

参数名称参数类型说明
Authorizationstring头部信息(TOKEN)

返回数据:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": null
}

 3.2 编码实现

思路:退出登录就是删除redis中的toke

(1)LoginOutController

@RestController
@RequestMapping("/logout")
public class LoginOutController {

    @Autowired
    private LoginService loginService;
    @GetMapping
    public Result logout(@RequestHeader("Authorization") String token){
        return loginService.logout(token);
    }
}

(2)LoginService和LoginServiceImpl

    Result logout(String token);
 //退出登录
    @Override
    public Result logout(String token) {
        redisTemplate.delete("TOKEN_"+token);
        return Result.success(null);
    }

运行结果:

 四. 用户注册

4.1. 接口说明

接口url:/register

请求方式:POST

请求参数:

参数名称参数类型说明
accountstring账号
passwordstring密码
nicknamestring昵称

返回数据:

{
    "success": true,
    "code": 200,
    "msg": "success",
    "data": "token"
}

4.2 编码实现

(1)RegisterController

@RestController
@RequestMapping("/register")
public class RegisterController {
    @Autowired
    LoginService loginService;
    //注册功能,返回数据为token
    @PostMapping
    public Result register(@RequestBody LoginParam loginParam){
        return loginService.register(loginParam);
    }
}

 (2)注册功能在LoginService实现

   LoginService与LoginServiceImpl

Result register(LoginParam loginParam);

 思路:
1)判断参数是否合法;
2)判断账户是否已经存在;
3)若合法并且不存在,则创建新用户;
4)生成token;
5)token即用户信息存入redis;
6)注意:在SysServiceImpl中设置事务,一旦出现问题,就回滚;
7)返回给前端token.

LoginServiceImpl

   @Override
    public Result register(LoginParam loginParam) {
        
        String account = loginParam.getAccount();
        String password = loginParam.getPassword();
        String nickname = loginParam.getNickname();
        //用户参数为空
        if(StringUtils.isBlank(account)||StringUtils.isBlank(password)||StringUtils.isBlank(nickname)){
            return Result.fail(ErrorCode.PARAMS_ERROR.getCode(), ErrorCode.PARAMS_ERROR.getMsg());
        }
        SysUser sysUser = sysUserService.findUserByAccount(account);
        //用户已经存在
        if(sysUser!=null){
            return Result.fail(ErrorCode.ACCOUNT_EXIST.getCode(), "账号已经被注册");
        }
        //创建用户,ID默认为自增
        sysUser =new SysUser();
        sysUser.setAccount(account);                                   //账户名
        sysUser.setNickname(nickname);                                  //昵称
        sysUser.setPassword(DigestUtils.md5Hex(password+salt));  //密码加盐md5
        sysUser.setCreateDate(System.currentTimeMillis());              //创建时间
        sysUser.setLastLogin(System.currentTimeMillis());               //最后登录时间
        sysUser.setAvatar("/static/img/logo.b3a48c0.png");              //头像
        sysUser.setAdmin(1);                                             //管理员权限
        sysUser.setDeleted(0);                                             //假删除
        sysUser.setSalt("");                                                //盐
        sysUser.setStatus("");                                              //状态
        sysUser.setEmail("");                                               //邮箱
        this.sysUserService.save(sysUser);

        //生成token
        String token = JWTUtils.createToken(sysUser.getId());
        //注册成功 使用Jwt生成Token ,返回Token 并 存到redis
        redisTemplate.opsForValue().set("TOKEN_"+token,JSON.toJSONString(sysUser),1,TimeUnit.DAYS);
        return Result.success(token);
    }

上面代码中涉及到两个方法
sysUserService.findUserByAccount(account)查询用户
sysUserService.save(sysUser)创建用户

 @Override
    public SysUser findUserByAccount(String account) {
        LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(SysUser::getAccount,account)
                .last("limit 1");
        return this.sysUserMapper.selectOne(wrapper);
    }
    @Override
    public void save(SysUser sysUser) {
        //保存用户id会自动生成
        //默认生成分布式id,采用雪花算法
        //mybatis-plus
        sysUserMapper.insert(sysUser);
    }

运行结果:

五. 登录拦截器

每次访问需要登录的资源的时候,都需要在代码中进行判断,一旦登录的逻辑有所改变,代码都得进行变动,非常不合适。

那么可不可以统一进行登录判断呢?

可以,使用拦截器,进行登录拦截,如果遇到需要登录才能访问的接口,如果未登录,拦截器直接返回,并跳转登录页面。

5.1  定义拦截器LoginInterceptor

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {

    @Autowired
    public SysUserService sysUserService;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        
        if(!(handler instanceof HandlerMethod)){
            //拦截器是拦截的controller中的方法,controller的方法其实就是一个Handler
            //handler可能是RequestResourceHandle(访问资源handle),即可能是访问静态资源的方法
            //解释:controller对应HandlerMethod,所以拦截器只拦截HandlerMethod
            return true;
        }
        //获取token
        String token = request.getHeader("Authorization");
        //日志问题,需要导入lombok下的@slf4
        log.info("=============request start=================");
        String requestURI = request.getRequestURI();
        log.info("request uri:{}",requestURI);
        log.info("request method:{}",request.getMethod());
        log.info("token:{}",token);
        log.info("=============request end===================");

        //token为空,不放行
        if(StringUtils.isBlank(token)){
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
            //设置返回消息格式
            response.setContentType("application/json;charset=utf8");
            //返回json信息
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        //token不为空,去做认证

        SysUser sysUser =sysUserService.checkToken(token) ;
        //用户不存在,即认证失败
        if(sysUser == null){
            System.out.println("没有该用户");
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), ErrorCode.NO_LOGIN.getMsg());
            response.setContentType("application/json;charset=utf8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        //登陆验证成功,放行
        return true;
    }
}

使拦截器生效,必须要在WebMVCConfig添加配置

  @Autowired
    private LoginInterceptor loginInterceptor;
@Override
    public void addInterceptors(InterceptorRegistry registry) {
        //配置拦截接口,此处配置为test,用于测试
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/test");
    }

创建测试类

@RestController
@RequestMapping("/test")
public class TestController {
    @RequestMapping
    public Result test(){
        return Result.success(null);
    }
}

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/894887.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号