毕设利器,教你从零搭建一个有规范的spring boot项目【五】——用户身份信息验证

接上篇《毕设利器,教你从零搭建一个有规范的spring boot项目【四】——参数校验》

用户身份信息验证这个问题老生常谈了。

用户登录了之后,很多接口都是要做用户信息验证的,一是让服务器知道这个用户是谁,二是出于数据安全考虑。

比如你想看你自己有多少个好友,分别都是谁,那你就要在好友列表的接口带上你的身份信息,不然接口鬼知道你是谁啊,要怎么给你返回数据呢?

学javaweb的时候通常是用session和cookie解决这个问题的,在这里推荐用token。

这也是现在比较流行的方式。

token是一段很长的字符串,有一定的时效性,过期了就没用了,通常我们就是用用户id加密生成,具体的可以看阮一峰老师的《JSON Web Token 入门教程》

阮老师的文章通常都写的很简单易懂,建议好好看看,这里就不再赘述了。

我们会在用户登录的时候返回token,然后在一些需要用户登录之后才能请求的接口,在访问这些接口的时候,在请求头带上token。

比如一个APP的首页,首页的数据很多都是不需要用户登录都能显示的,这时候接口需要不带token也能请求到。

但如果是查看我的好友列表,这些数据很明显需要让服务器知道你是哪个用户,这个时候请求就需要带上token了,如果没token,或者token失效,就让前端同学跳到登陆页面,要求用户登录。

代码的实现方式

首先我们需要一个将用户id生成token的工具类。

你也可以用别的,但考虑到这是识别用户用的,大家通常都用用户id来生成token。

引入下面的依赖:

<!--jwt相关的jar包-->
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt</artifactId>
	<version>0.9.1</version>
</dependency>
<dependency>
	<groupId>com.auth0</groupId>
	<artifactId>java-jwt</artifactId>
	<version>3.4.0</version>
</dependency>

在core包下新建一个jwt包,在里面自己编写一个工具类,用用户id生成token,我也是拿来直接用,这部分可以直接粘贴:

在这里插入图片描述

package com.TandK.turntable.core.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@Component
public class JwtUtil {

    //生成签名的时候使用的秘钥secret,这个方法本地封装了的,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
    //    //@Value("${jwt.key}")
    private static String key = "sdf23dfgkddfasdasdfghjklzxcvbhtrewq";

    /**
     * 用户登录成功后生成Jwt
     * 使用Hs256算法  私匙使用用户密码
     *
     * @param ttlMillis      jwt过期时间
     * @param userUuid 登录成功的user对象
     * @return
     */
    public static String createJWT(long ttlMillis, String userUuid) {
        //指定签名的时候使用的签名算法,也就是header那部分,jwt已经将这部分内容封装好了。
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        //生成JWT的时间
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        //创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
        Map<String, Object> claims = new HashMap<String, Object>();
        claims.put("userUuid", userUuid);

        //生成签发人 一般是公司名字,可以是中文
        String subject = "TandK";
        //下面就是在为payload添加各种标准声明和私有声明了
        //这里其实就是new一个JwtBuilder,设置jwt的body
        JwtBuilder builder = Jwts.builder()
                //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setClaims(claims)
                //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setId(UUID.randomUUID().toString())
                //iat: jwt的签发时间
                .setIssuedAt(now)
                //代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
                .setSubject(subject)
                //设置签名使用的签名算法和签名使用的秘钥
                .signWith(signatureAlgorithm, key);
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            //设置过期时间
            builder.setExpiration(exp);
        }

        return builder.compact();
    }




    public static String createJWTBySecond(long seconds, String userUuid) {
        return createJWT(seconds * 1000, userUuid);
    }


    /**
     * Token的解密
     *
     * @param token 加密后的token
     * @return
     */
    public Claims parseJWT(String token) {
        //得到DefaultJwtParser
        Claims claims = Jwts.parser()
                //设置签名的秘钥
                .setSigningKey(key)
                //设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        return claims;
    }


    /**
     * 校验token
     * 在这里可以使用官方的校验,我这里校验的是token中携带的密码于数据库一致的话就校验通过
     *
     * @param token
     * @param userUuid
     * @return
     */
    public Boolean isVerify(String token, String userUuid) {
        //得到DefaultJwtParser
        Claims claims = Jwts.parser()
                //设置签名的秘钥
                .setSigningKey(key)
                //设置需要解析的jwt
                .parseClaimsJws(token).getBody();
        if (claims.get("userUuid").equals(userUuid)) {
            return true;
        }
        return false;
    }

}

有了token的生成器后,我们可以建一张表存储用户的token,这样不用在每次请求的时候都去解析用户的token,解析出用户的id,再拿用户id去数据库查出用户的信息。

直接联表查询,查一遍完事,查出用户的token,如果token还没失效,还能顺便把用户信息查出来。

/*
 Navicat MySQL Data Transfer

 Source Server         : localhost_3306
 Source Server Type    : MySQL
 Source Server Version : 80019
 Source Host           : localhost:3306
 Source Schema         : demo

 Target Server Type    : MySQL
 Target Server Version : 80019
 File Encoding         : 65001

 Date: 29/09/2021 21:46:18
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for user_token
-- ----------------------------
DROP TABLE IF EXISTS `user_token`;
CREATE TABLE `user_token`  (
  `uuid` bigint(19) NOT NULL,
  `user_uuid` bigint(19) NULL DEFAULT NULL,
  `access_token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL,
  `expire_time` datetime(0) NULL DEFAULT NULL COMMENT '过期时间',
  `create_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0),
  `update_time` datetime(0) NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
  PRIMARY KEY (`uuid`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

结构如下,除了基础的数据外,user_uuid用来联合user表查出用户信息,access_token存的是token的明文,expire_time存的是token的过期时间。

当用户带着token访问我们的接口时,只需要用下面的sql就可以查出来token是否有效,如果有效还能顺带把用户信息查出来:

SELECT
	u.*
FROM
	user u
   	LEFT JOIN user_token ut ON u.uuid = ut.user_uuid
WHERE
   	ut.access_token = #{token}
	AND expire_time >= NOW()
	AND u.is_delete = 0

建好表之后顺带把UserTokenPO、对应的mapper和service建一建。

顺带粘一下代码:

PO:

package com.TandK.model.po;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

import java.io.Serializable;
import java.util.Date;

/**
 * 
 * @TableName user_token
 */
@TableName(value ="user_token")
@Data
public class UserTokenPO implements Serializable {
    /**
     * 
     */
    @TableId(value = "uuid")
    private Long uuid;

    /**
     * 
     */
    @TableField(value = "user_uuid")
    private Long userUuid;

    /**
     * 
     */
    @TableField(value = "access_token")
    private String accessToken;

    /**
     * 过期时间
     */
    @TableField(value = "expire_time")
    private Date expireTime;

    /**
     * 
     */
    @TableField(value = "create_time")
    private Date createTime;

    /**
     * 
     */
    @TableField(value = "update_time")
    private Date updateTime;

    @TableField(exist = false)
    private static final long serialVersionUID = 1L;
}

Mapper:

package com.TandK.mapper;

import com.TandK.model.po.UserTokenPO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

/**
 * @author TandK
 * @since 2021/9/29 22:37
 */
public interface UserTokenMapper extends BaseMapper<UserTokenPO> {
}

Service接口:

package com.TandK.service;

/**
 * @author TandK
 * @since 2021/9/29 21:57
 */
public interface UserTokenService {
}

Service实现类:

package com.TandK.service.impl;

import com.TandK.service.UserTokenService;
import org.springframework.stereotype.Service;

/**
 * @author TandK
 * @since 2021/9/29 21:58
 */
@Service
public class UserTokenServiceImpl implements UserTokenService {
    
}

有了token的生成器和对应的数据库表后,接下来我们要考虑一个问题。

什么时候校验token?

是的,什么时候校验token呢,前面也说过了,有些接口是不需要token的,而更多时候是需要拿到token解析出用户信息的,难道在每个接口都检查一遍吗?

这样也可以,不过写起来很费解就是了。

重复的东西要写好多遍。

Spring Boot提供了一个叫拦截器的东西,通过这个拦截器,我们可以在每个请求的过程中,在进入到controller之前,拦截下这个请求。

先检查它访问的这个接口需要带token吗。

  • 如果不需要,就直接放行。

  • 如果需要,就检查是否有带token。

    • 如果没带,就提示鉴权失败,让用户登录。
    • 如果有带,就检查是否过期。
      • 如果没过期,就拿到用户信息并放行。
      • 如果过期了,就提示鉴权失败,需要重新登录。

流程是这么个流程。

在建立这个拦截器之前,我们可以先写一个@IgnoreToken注解,这个注解会用在Controller层的方法上。

进入拦截器之后,如果检查到对应的方法有这个注解,就说明不需要校验请求头是否有token,如果没带,才需要校验token。

在core包下建一个annotation包,我们项目里所有的注解都可以写在这。

在这里插入图片描述

package com.TandK.turntable.core.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 携带这个注解可以忽视token校验
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IgnoreToken {

}

接下来编写拦截器,在core包下新建一个interceptor包,项目里所有的拦截器都可以放在这个包里。

package com.TandK.core.interceptor;


import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * token拦截器
 * @author TandK
 */
public class AuthInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return false;
    }
}

说明一下这个拦截器吧,spring boot里的拦截器需要实现HandlerInterceptor接口,在我们这里使用先关注preHandle这个方法即可,可以看到这个方法返回的是boolean类型,如果是ture,就是放行,让请求能够进入controller层,如果是false,就是被拦截下来,并且不会返回任何数据。

在我们这里,拦截下来了也要告诉前端,是鉴权失败了,这样前端才好做处理。

因此,如果鉴权失败了,我们直接抛前面定义过的业务异常即可,所以在这里需要定义一个鉴权失败的枚举。

细节在前面的《毕设利器,教你从零搭建一个有规范的spring boot项目【三】—— 返回结果的处理和统一异常处理》里说过,如果有不清楚的可以去看看。

这里直接贴对应的枚举:

在这里插入图片描述

UNAUTHORIZED(401, "鉴权失败", "鉴权失败");

接下来回来对拦截器继续编写,首先检查访问的controller层方法是否带有@IgnoreToken注解,如果有,直接放行:

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
	HandlerMethod handlerMethod = (HandlerMethod) handler;
	Method method = handlerMethod.getMethod();
 	// 判断有没有IgnoreToken注解,如果有,直接放行
   	if(method.isAnnotationPresent(IgnoreToken.class)){
    	return true;
  	}
    return true;
}

如果没有,就需要校验token是否有效,先获取请求头带的token。

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
	HandlerMethod handlerMethod = (HandlerMethod) handler;
	Method method = handlerMethod.getMethod();
 	// 判断有没有IgnoreToken注解,如果有,直接放行
   	if(method.isAnnotationPresent(IgnoreToken.class)){
    	return true;
  	}

	// 先获取前端传来的token,这里的Authrization是小程序推荐的叫法,具体要和前端传来的变量名一致
	String token = request.getHeader("Authrization");
	if(StringUtils.isBlank(token) || token.equals("[object Undefined]")){
		// 没带token
		throw new BusinessException(BusinessExceptionEumn.UNAUTHORIZED);
	}
    return true;
}

前端放在请求头的token,可能会有不同的名字,一般叫token或者Authrization,或者X-Access-Token,随你喜欢,如果要换个名字,String token = request.getHeader("Authrization");里的Authrization就要换成对应的名字。

可以看到,没有token,就直接抛业务异常,全局异常处理器会捕捉这里的异常并返回给前端的。

获取到了前端的token,我们就可以拿这个token去数据库里查出用户信息。

首先需要在UserTokenService里,写一个根据token查出用户信息的方法。

联表查询不建议用MyBatis-plus实现,还是手写的好,因此要自定义mapper层的方法:

Mapper层:

package com.TandK.mapper;

import com.TandK.model.po.UserPO;
import com.TandK.model.po.UserTokenPO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;

/**
 * @author TandK
 * @since 2021/9/29 22:37
 */
public interface UserTokenMapper extends BaseMapper<UserTokenPO> {

    UserPO selectUserByToken(String token);
}

mapper的自定义sql可以用注解实现,就下面这种写法。

package com.TandK.mapper;

import com.TandK.model.po.UserPO;
import com.TandK.model.po.UserTokenPO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;

/**
 * @author TandK
 * @since 2021/9/29 22:37
 */
public interface UserTokenMapper extends BaseMapper<UserTokenPO> {

    @Select("SELECT  u.* FROM user u LEFT JOIN user_token ut ON u.uuid = ut.user_uuid WHERE ut.access_token = #{token} AND expire_time >= NOW() AND u.is_delete = 0")
    UserPO selectUserByToken(String token);
}

也可以用XML的方法,个人还是喜欢xml的写法的,那样写出来的sql可以足够长,看着也足够有结构感,不会太乱。

这里贴一下XML的写法,XML或者注解,这两种写法挑一个即可。

XML需要新建一个XML的文件,在项目的resources文件夹下新建一个mapper文件夹。

在这里插入图片描述

然后新建一个XML文件,注意要和对应的mapper.java同名。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.TandK.mapper.UserTokenMapper">


    <select id="selectUserByToken" resultType="com.TandK.model.po.UserPO">
        SELECT
            u.*
        FROM
            user u
            LEFT JOIN user_token ut ON u.uuid = ut.user_uuid
        WHERE
            ut.access_token = #{token}
            AND expire_time >= NOW()
            AND u.is_delete = 0
    </select>
</mapper>

对应的说明在下图:
在这里插入图片描述
还是那句话,自定义sql的方式,注解和XML的方式选一个就行了,我是推荐用XML的方式,如果项目大了,用XML方式写出来的SQL有时还能占一整个屏幕,用注解的方式写的话会很辣眼睛,如果用了XML就把注解的方式删掉就行。

接下来贴一下service层的代码:

package com.TandK.service;

import com.TandK.model.po.UserPO;

/**
 * @author TandK
 * @since 2021/9/29 21:57
 */
public interface UserTokenService {

    /**
     * 通过token获取用户
     * @param token
     * @return
     */
    UserPO getUserByToken(String token);
}

package com.TandK.service.impl;

import com.TandK.mapper.UserTokenMapper;
import com.TandK.model.po.UserPO;
import com.TandK.service.UserTokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

/**
 * @author TandK
 * @since 2021/9/29 21:58
 */
@Service
public class UserTokenServiceImpl implements UserTokenService {


    @Autowired
    private UserTokenMapper userTokenMapper;


    @Override
    public UserPO getUserByToken(String token) {
        return userTokenMapper.selectUserByToken(token);
    }
}

根据token查询用户信息的方法写好之后,就可以回到拦截器了,用拿到的token查一遍:

package com.TandK.core.interceptor;


import com.TandK.core.annotation.IgnoreToken;
import com.TandK.core.exception.BusinessException;
import com.TandK.core.exception.BusinessExceptionEumn;
import com.TandK.model.po.UserPO;
import com.TandK.service.UserTokenService;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

/**
 * token拦截器
 * @author TandK
 */
public class AuthInterceptor implements HandlerInterceptor {
    
    @Autowired
    private UserTokenService userTokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        // 判断有没有IgnoreToken注解,如果有,直接放行
        if(method.isAnnotationPresent(IgnoreToken.class)){
            return true;
        }

        // 先获取前端传来的token,这里的Authrization是小程序推荐的叫法,具体要和前端传来的变量名一致
        String token = request.getHeader("Authrization");
        if(StringUtils.isBlank(token) || token.equals("[object Undefined]")){
            // 没带token
            throw new BusinessException(BusinessExceptionEumn.UNAUTHORIZED);
        }

        // 根据token获取用户信息
        UserPO userPO = userTokenService.getUserByToken(token);
        if(userPO == null){
            // 鉴权失败
            throw new BusinessException(BusinessExceptionEumn.UNAUTHORIZED);
        }
        return true;
    }
}

这样,如果通过了前面的重重检查,到了最后return true;就可以放行了。

但是,查出来的用户信息,我们可以放到内存中,方便后面需要的时候再随时提取出来。

这个时候我们可以用SpringBoot的ThreadLocal来存储用户信息。

简单地说明一下ThreadLocal是个什么东西。

可以这么理解,每一次访问都会有一个线程,ThreadLocal其实是Map<K, V>,每次访问,我们都可以在Controller层存东西,再在别的方法里取出来。

由于生命周期只在一次访问内,因此很适合我们这个场景。

我们在拦截器里把用户信息存起来,可以在这次请求内的任意地方拿出来,可以是controller层、service层。

在之前的core.support包下新建一个threadlocal包:

在这里插入图片描述

新建一个UserThreadLocal静态类,具体的代码实现方式如下:

package com.TandK.core.support.threadlocal;


import com.TandK.model.po.UserPO;

/**
 * @author TandK
 */
public class UserThreadLocal {

    private static ThreadLocal<UserPO> userThreadLocal = new ThreadLocal();

    public static void set(UserPO userPO){
        userThreadLocal.set(userPO);
    }

    public static UserPO get(){
        return userThreadLocal.get();
    }

    public static void remove(){
        userThreadLocal.remove();
    }
}

用的话,无非就是存和取用户信息,我们回到拦截器那里,把通过校验的用户信息存起来,这样,拦截器的功能就完整了:

package com.TandK.core.interceptor;


import com.TandK.core.annotation.IgnoreToken;
import com.TandK.core.exception.BusinessException;
import com.TandK.core.exception.BusinessExceptionEumn;
import com.TandK.core.support.threadlocal.UserThreadLocal;
import com.TandK.model.po.UserPO;
import com.TandK.service.UserTokenService;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

/**
 * token拦截器
 * @author TandK
 */
public class AuthInterceptor implements HandlerInterceptor {

    @Autowired
    private UserTokenService userTokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        Method method = handlerMethod.getMethod();
        // 判断有没有IgnoreToken注解,如果有,直接放行
        if(method.isAnnotationPresent(IgnoreToken.class)){
            return true;
        }

        // 先获取前端传来的token,这里的Authrization是小程序推荐的叫法,具体要和前端传来的变量名一致
        String token = request.getHeader("Authrization");
        if(StringUtils.isBlank(token) || token.equals("[object Undefined]")){
            // 没带token
            throw new BusinessException(BusinessExceptionEumn.UNAUTHORIZED);
        }

        // 根据token获取用户信息
        UserPO userPO = userTokenService.getUserByToken(token);
        if(userPO == null){
            // 鉴权失败
            throw new BusinessException(BusinessExceptionEumn.UNAUTHORIZED);
        }

        // 存储用户信息
        UserThreadLocal.set(userPO);
        return true;
    }
}

之后随时有需要拿出用户信息,只需要像下面这样写就可以了:

UserPO userPO = UserThreadLocal.get();

这样就可以拿出此次访问当前接口的用户信息。

上面说完了token的校验和用户信息的存取。

接下来说一下token的创建和更新,一般我们都是在用户登录的时候。

首先登录的接口要写上@IgnoreToken注解,忽略token的校验。

然后检查用户是否第一次登录,这个查一下有没有这个用户的账号密码就能知道。

如果是第一次登录,那么就生成token,如果不是,那就刷新token和token的有效时间就好。

写一个登录接口,首先还是回去之前建好的user表,加上账号和密码的字段。

在这里插入图片描述

注册接口就不写了,账号密码直接从数据库里塞进去吧。

在这里插入图片描述

数据库添加了字段,对应的po也要添加字段,不然后面可能出现一些问题:

在这里插入图片描述

建立一个LoginVO,用来接收登录接口的参数,做好校验:

package com.TandK.model.vo;

import lombok.Data;

import javax.validation.constraints.NotBlank;

/**
 * @author TandK
 * @since 2021/10/2 14:20
 */
@Data
public class LoginVO {

    @NotBlank(message = "账号不能为空")
    private String account;

    @NotBlank(message = "密码不能为空")
    private String password;
}

controller层的代码:

在这里插入图片描述
这里登录的逻辑都写在注释里了,可以好好看看:

package com.TandK.service.impl;

import com.TandK.core.exception.BusinessException;
import com.TandK.core.exception.BusinessExceptionEumn;
import com.TandK.core.jwt.JwtUtil;
import com.TandK.core.support.http.HttpResponseSupport;
import com.TandK.mapper.UserMapper;
import com.TandK.model.po.UserPO;
import com.TandK.model.vo.LoginVO;
import com.TandK.model.vo.UserVO;
import com.TandK.service.UserService;
import com.TandK.service.UserTokenService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.stream.Collectors;

/**
 * @author TandK
 * @since 2021/9/8 22:07
 */
@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private UserTokenService userTokenService;
    
    @Override
    public ResponseEntity<Object> login(LoginVO loginVO) {
        // 查询是否第一次登陆
        QueryWrapper<UserPO> wrapper = new QueryWrapper<>();
        wrapper.eq("account", loginVO.getAccount())
                .eq("password", loginVO.getPassword())
                .eq("is_delete", 0);
        UserPO userPO = userMapper.selectOne(wrapper);

        if(userPO == null){
            // 第一次登录,保存用户信息
            userPO = new UserPO();
            // 复制loginVO的账号密码的值到userPO
            BeanUtils.copyProperties(loginVO, userPO);
            // 保存用户信息
            userMapper.insert(userPO);
        }

        // 生成token信息
        String token = JwtUtil.createJWTBySecond(1000 * 60 * 60 * 24 * 30, userPO.getUuid());
        userTokenService.saveToken(userPO.getUuid(), token);
        return HttpResponseSupport.success(token);
    }
}

package com.TandK.turntable.service.impl;

import cn.hutool.core.date.DateUtil;
import com.TandK.turntable.mapper.UserTokenMapper;
import com.TandK.turntable.model.po.UserPO;
import com.TandK.turntable.model.po.UserTokenPO;
import com.TandK.turntable.service.UserTokenService;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Date;

/**
 * @author TandK
 * @since 2021/8/15 13:56
 */
@Service
public class UserTokenServiceImpl implements UserTokenService {

    @Autowired
    private UserTokenMapper userTokenMapper;

    @Transactional(rollbackFor = Throwable.class)
    @Override
    public void saveToken(String uuid, String token) {

        // 查出原先有用的token
        QueryWrapper<UserTokenPO> wrapper = new QueryWrapper();
        wrapper.eq("user_uuid", uuid)
                .orderByDesc("update_time")
                .last("LIMIT 1");
        UserTokenPO oldToken = userTokenMapper.selectOne(wrapper);

		// token的有效时间我定为一个月,DateUtil这个工具类下面有贴依赖
        Date nextMonth = DateUtil.nextMonth();
        if(oldToken == null){
            // 没有就生成新的token
            UserTokenPO userTokenPO = new UserTokenPO();
            userTokenPO.setUserUuid(uuid);
            userTokenPO.setAccessToken(token);
            userTokenPO.setExpireTime(nextMonth);
            userTokenMapper.insert(userTokenPO);
            return;
        }

        // 有就更新
        oldToken.setAccessToken(token);
        oldToken.setExpireTime(nextMonth);
        userTokenMapper.updateById(oldToken);

    }
}

上面DateUtil的工具类的依赖如下:

<!-- Hutool是一个小而全的Java工具类库,通过静态方法封装,-->
<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.1.0</version>
</dependency>

由于接口是post,需要用调试工具(没有工具也没关系,下一篇介绍的Swagger,文档工具可以提供调试功能,不需要你去安装调试软件):
在这里插入图片描述
返回接口如下,圈起来的就是token的值:
在这里插入图片描述
前端同学需要把这个token存起来,再在每次请求的时候把token带在请求头上。

那么关于token校验流程就到此为止,还有些细节你可以试试,比如在需要token校验的接口,访问它的时候不带token,比如在代码中用UserThreadLocal获取用户信息,这些都可以自己去试一试。

有什么问题可以留言,看到了都会尽量帮忙解决。


2021-10-05更新,下一篇在这里:《毕设利器,教你从零搭建一个有规范的spring boot项目【六】——接口文档和RESTful API》

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TandK

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值