接上篇《毕设利器,教你从零搭建一个有规范的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》