JWT 续期
续期
传统httpsession和spring-session都是采用httpsession默认机制。内部会有线程不停的轮询会话列表。把那些内存中的会话列表中的过期时间和当前时间进行比较,如果超过> 30分钟 自动把session删除。如果在30以内的请求,会自动续期(时间会从0开始计数)。
为什么要这样做?
你思考。如果没有续期,会怎么样?就好比你登录腾讯游戏,你登录有效时间是30分钟。那么也就意味着你每隔30分钟要退出重新登录一次。所以我们应该是在你登录以后,未来的每一次请求中,只要用户一直在发起请求,就把时间永远覆盖。这样就永远保持热活。那么不用在登录。除非你静默的实际超过了30分钟,那确实需要重新登录,
1.什么是token
Token是服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码。token其实说的更通俗点可以叫暗号,在一些数据传输之前,要先进行暗号的核对,不同的暗号被授权不同的数据操作。
使用token的好处:
=基于Token的身份验证是无状态的,我们不将用户信息存在服务器或Session中。
2.什么是JWT
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准.该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
JWT是由三段数据进行构成的:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
解析如下:
第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是
签证(signature).
3.token过期自动续费方案
第一种方案:每次请求都生成新的token
方案1: 每一次请求都进行重新生成一个新的token【频率过高,性能不好】
第二种方案:采用token签发时间续期
- 以旧换新概念
- 签发(出生)时间:2022年05月20日 10:00:00
- 刷新时间:
- 做法1:当前时间-20分钟 >= 签发时间 或者
- 做法2:(当前时间 - 签发时间) 分钟 >= 20分钟
- 减去的实际越大,代表原来的token运行时间更久
- 反之,代表原来的token运行时间更短
- 过期时间:2022年05月20日 10:30:00
package com.pug.zixun.config.interceptor;
import com.pug.zixun.common.anno.IgnoreToken;
import com.pug.zixun.common.enums.AdminUserResultEnum;
import com.pug.zixun.common.ex.PugValidatorException;
import com.pug.zixun.common.utils.date.TmDateUtil;
import com.pug.zixun.common.utils.fn.asserts.Vsserts;
import com.pug.zixun.config.jwt.JwtService;
import com.pug.zixun.config.validator.PugAssert;
import com.pug.zixun.domain.User;
import com.pug.zixun.local.UserThreadLocal;
import com.pug.zixun.service.user.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Date;
@Component
@Slf4j
public class PassportLoginInterceptor implements HandlerInterceptor {
@Autowired
private JwtService jwtService;
@Autowired
private IUserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// handler从object对象转换成具体的目标对象HandlerMethod
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 获取执行的方法
Method method = handlerMethod.getMethod();
if (method.getAnnotation(IgnoreToken.class) != null ||
handlerMethod.getBeanType().getAnnotation(IgnoreToken.class) != null) {
return true;
}
// 获取用户请求的token
String token = getToken(request);
// 如果token没有传递,直接走统一异常处理 + 统一返回(内部自动帮你把数据通过jackson转换成json返回)
PugAssert.isEmptyEx(token, AdminUserResultEnum.TOKEN_NOT_FOUND);
// 校验token是否合法
boolean verify = jwtService.verify(token);
PugAssert.isFalseEx(verify, AdminUserResultEnum.TOKEN_ERROR);
// 解析token,获取用户id
Long tokenUserId = jwtService.getTokenUserId(token);
PugAssert.isNullEx(tokenUserId, AdminUserResultEnum.USER_NULL_ERROR);
// 根据用户查询用户信息,实时性 db 0.007
User user = userService.getById(tokenUserId);
PugAssert.isNullEx(user, AdminUserResultEnum.USER_NULL_ERROR);
// 拉黑处理
if (user.getForbbiden() != null && user.getForbbiden().equals(0)) {
throw new PugValidatorException(AdminUserResultEnum.USER_FORBIDDEN_ERROR);
}
// 删除处理
if (user.getIsDelete() != null && user.getIsDelete().equals(1)) {
throw new PugValidatorException(AdminUserResultEnum.USER_FORBIDDEN_ERROR);
}
// 把用户信息放入到UserThreadLocal
UserThreadLocal.put(user);
// token续期
// 获取token的签发时间
Date signTokenTime = jwtService.getTokenIssuedTime(token);
Date expireTime = jwtService.getTokenExpireTime(token);
System.out.println("签发时间:" + TmDateUtil.dateToString(signTokenTime,"yyyy-MM-dd HH:mm:ss"));
System.out.println("当前时间时间:" + TmDateUtil.dateToString(new Date(),"yyyy-MM-dd HH:mm:ss"));
System.out.println("过期时间:" + TmDateUtil.dateToString(expireTime,"yyyy-MM-dd HH:mm:ss"));
// 用当前时间 减去 签发时间 如果大于 > 10
// 10分钟 什么意思:就代表,当前的token以及使用10分钟了。如果超过了这个时间就要更换了。
int diffminutes = TmDateUtil.diffminutes(signTokenTime,new Date());
// 开始刷新token
Long period = JwtService.TOKEN_EXPIRE_TIME - 10;
if(diffminutes >= 10 ){
// 续期,重新生成一个新的token
String newToken = jwtService.createToken(tokenUserId);
// 通过response的头部输出token,然后前台通过reponse获取
response.setHeader("x-auth-token", newToken);
}
return true;
}
/**
* 获取请求头的token
*
* @param request
* @return
*/
private String getToken(HttpServletRequest request) {
String token = request.getHeader("token");
if (Vsserts.isEmpty(token)) {
return null;
}
if (!token.startsWith(JwtService.PUG_TOKEN_PREFIX)) {
throw new PugValidatorException(AdminUserResultEnum.TOKEN_ERROR_STATUS);
}
// 截图前缀
token = token.substring(JwtService.PUG_TOKEN_PREFIX.length());
// 返回
return token;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
UserThreadLocal.remove();
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserThreadLocal.remove();
}
}
第三方方案:采用token的过期时间续期
- 以旧换新
package com.pug.zixun.config.interceptor;
import com.pug.zixun.common.anno.IgnoreToken;
import com.pug.zixun.common.enums.AdminUserResultEnum;
import com.pug.zixun.common.ex.PugValidatorException;
import com.pug.zixun.common.utils.date.DateUtil;
import com.pug.zixun.common.utils.date.DateUtils;
import com.pug.zixun.common.utils.date.TmDateUtil;
import com.pug.zixun.common.utils.fn.asserts.Vsserts;
import com.pug.zixun.config.jwt.JwtService;
import com.pug.zixun.config.validator.PugAssert;
import com.pug.zixun.domain.User;
import com.pug.zixun.local.UserThreadLocal;
import com.pug.zixun.service.user.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Date;
/**
* @author 飞哥
* @Title: 学相伴出品
* @Description: 飞哥B站地址:https://space.bilibili.com/490711252
* 记得关注和三连哦!
* @Description: 我们有一个学习网站:https://www.kuangstudy.com
* @date 2022/5/16$ 22:27$
*/
@Component
@Slf4j
public class PassportLoginInterceptor implements HandlerInterceptor {
@Autowired
private JwtService jwtService;
@Autowired
private IUserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// handler从object对象转换成具体的目标对象HandlerMethod
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 获取执行的方法
Method method = handlerMethod.getMethod();
if (method.getAnnotation(IgnoreToken.class) != null ||
handlerMethod.getBeanType().getAnnotation(IgnoreToken.class) != null) {
return true;
}
// 获取用户请求的token
String token = getToken(request);
// 如果token没有传递,直接走统一异常处理 + 统一返回(内部自动帮你把数据通过jackson转换成json返回)
PugAssert.isEmptyEx(token, AdminUserResultEnum.TOKEN_NOT_FOUND);
// 校验token是否合法
boolean verify = jwtService.verify(token);
PugAssert.isFalseEx(verify, AdminUserResultEnum.TOKEN_ERROR);
// 解析token,获取用户id
Long tokenUserId = jwtService.getTokenUserId(token);
PugAssert.isNullEx(tokenUserId, AdminUserResultEnum.USER_NULL_ERROR);
// 根据用户查询用户信息,实时性 db 0.007
User user = userService.getById(tokenUserId);
PugAssert.isNullEx(user, AdminUserResultEnum.USER_NULL_ERROR);
// 拉黑处理
if (user.getForbbiden() != null && user.getForbbiden().equals(0)) {
throw new PugValidatorException(AdminUserResultEnum.USER_FORBIDDEN_ERROR);
}
// 删除处理
if (user.getIsDelete() != null && user.getIsDelete().equals(1)) {
throw new PugValidatorException(AdminUserResultEnum.USER_FORBIDDEN_ERROR);
}
// 把用户信息放入到UserThreadLocal
UserThreadLocal.put(user);
// 刷新续期
refreshToken(token, user.getId(), response);
return true;
}
/**
* 签发时间续期
*
* @param token
* @param tokenUserId
* @param response
*/
private void refreshToken(String token, Long tokenUserId, HttpServletResponse response) {
// token续期
// 获取token的签发时间 --------第一种写法
Date signTokenTime = jwtService.getTokenIssuedTime(token);
int diffminutes = TmDateUtil.diffminutes(signTokenTime,new Date());
// 开始刷新token 10的含义是:旧的token还剩下10分钟,在最后的这10分钟范围内去续期,
// 假设你的token存活时间(TOKEN_EXPIRE_TIME=30)。那么久是久的token存活20分钟,在20分钟以后时间内都是续期时间点。
Long period = JwtService.TOKEN_EXPIRE_TIME - 10;
if(diffminutes >= period ){
// 续期,重新生成一个新的token
String newToken = jwtService.createToken(tokenUserId);
// 通过response的头部输出token,然后前台通过reponse获取
response.setHeader("x-auth-token", newToken);
}
// 获取token的签发时间 --------第二种写法
// Long period = JwtService.TOKEN_EXPIRE_TIME - 10 * -1;
// Date signTokenTime = jwtService.getTokenIssuedTime(token);
// Date currentDate = TmDateUtil.addMinute(new Date(), period.intValue());
// System.out.println("签发时间" + TmDateUtil.dateToString(signTokenTime,"yyyy-MM-dd HH:mm:ss"));
// System.out.println("刷新时间" + TmDateUtil.dateToString(currentDate,"yyyy-MM-dd HH:mm:ss"));
// System.out.println(currentDate.after(signTokenTime));
//
// if (currentDate.after(signTokenTime)) {
// // 续期,重新生成一个新的token
// String newToken = jwtService.createToken(tokenUserId);
// // 通过response的头部输出token,然后前台通过reponse获取
// response.setHeader("x-auth-token", newToken);
// }
}
/**
* 过期时间续期
*
* @param token
* @param tokenUserId
* @param response
*/
private void refreshToken2(String token, Long tokenUserId, HttpServletResponse response) {
// token续期
// 获取token的签发时间 --------第一种写法
Date expireTime = jwtService.getTokenExpireTime(token);
// 假设过期时间是 30分钟,用过期时间减去当前时间:30 29 28 27 20...10
int diffminutes = TmDateUtil.diffminutes(new Date(),expireTime);
// 如果时间以及过去了20分钟,到最后十分钟的时候就开始续期 10 9 8 7
if(diffminutes <= 10 ){
// 续期,重新生成一个新的token
String newToken = jwtService.createToken(tokenUserId);
// 通过response的头部输出token,然后前台通过reponse获取
response.setHeader("x-auth-token", newToken);
}
}
/**
* 获取请求头的token
*
* @param request
* @return
*/
private String getToken(HttpServletRequest request) {
String token = request.getHeader("token");
if (Vsserts.isEmpty(token)) {
return null;
}
if (!token.startsWith(JwtService.PUG_TOKEN_PREFIX)) {
throw new PugValidatorException(AdminUserResultEnum.TOKEN_ERROR_STATUS);
}
// 截图前缀
token = token.substring(JwtService.PUG_TOKEN_PREFIX.length());
// 返回
return token;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
UserThreadLocal.remove();
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserThreadLocal.remove();
}
}
第四种方案:采用redis的双倍时间方案续期
-
登录给token签发时间30分钟,同时在redis中生成一个2倍的时间作为续期时间,60分钟
-
然后后续所有请求,都携带这个token,判断当前的token是否过期,就会根据过期token去缓存redis中去找对应时间,是否在60分钟以内。
-
如果redis的时间,还在60分钟以内,说明。用户还在继续访问和请求。那么就会重新创建一个新的token,放入到redis中继续继续的续期。
-
和上面的签发时间和过期时间处理的差别在什么地方呢,redis方案,token不用更新。而上面的需要进行替换。
1: 登录存放双倍的实际到缓存中
package com.pug.zixun.controller.login;
import com.pug.zixun.bo.UserBo;
import com.pug.zixun.common.enums.AdminUserResultEnum;
import com.pug.zixun.common.ex.PugValidatorException;
import com.pug.zixun.common.utils.pwd.MD5Util;
import com.pug.zixun.config.BaseController;
import com.pug.zixun.config.jwt.JwtService;
import com.pug.zixun.config.validator.PugAssert;
import com.pug.zixun.domain.User;
import com.pug.zixun.service.user.IUserService;
import com.pug.zixun.vo.UserVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.UUID;
@RestController
@Slf4j
public class PassportLoginController extends BaseController {
@Autowired
private IUserService userService;
@Autowired
private JwtService jwtService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 登录
*
* @param userVo
* @return
*/
@PostMapping("/login/toLogin")
public UserBo logined(@RequestBody UserVo userVo) {
// 这里有校验,spring-validator框架来完成 或者用断言 或者用自己封装的
PugAssert.isEmptyEx(userVo.getUsername(), AdminUserResultEnum.USER_NAME_NOT_EMPTY);
PugAssert.isEmptyEx(userVo.getPassword(), AdminUserResultEnum.USER_PWD_NOT_EMPTY);
// 根据用户名称查询用户信息
User dbLoginUser = userService.login(userVo);
PugAssert.isNullEx(dbLoginUser, AdminUserResultEnum.USER_NULL_ERROR);
// 用户输入的密码
String inputPwd = MD5Util.md5slat(userVo.getPassword());
// 如果输入密码和数据库密码不一致
boolean isLogin = dbLoginUser.getPassword().equalsIgnoreCase(inputPwd);
// 如果输入的账号和有误,isLogin=false.注意isFalseEx在里面取反的,所以会抛出异常
PugAssert.isFalseEx(isLogin,AdminUserResultEnum.USER_INPUT_USERNAME_ERROR);
UserBo userBo = new UserBo();
// 根据用户生成token
String token = jwtService.createToken(dbLoginUser.getId());
userBo.setToken(token);
// 注意把一些敏感信息全部清空返回
dbLoginUser.setPassword(null);
userBo.setUser(dbLoginUser);
// 登录挤下线
String tokenUuid = UUID.randomUUID().toString();
String tokenUuidKey = "pug:user:login:"+dbLoginUser.getId();
redisTemplate.opsForValue().set(tokenUuidKey,tokenUuid);
userBo.setTokenUuid(tokenUuid);
// Jwt和redis的续期双倍时间
String tokenKey = "pug:user:login:token:" + token;
redisTemplate.opsForValue().set(tokenKey,JwtService.TOKEN_EXPIRE_TIME * 2);
return userBo;
}
}
2:拦截器处理
package com.pug.zixun.config.interceptor;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.pug.zixun.common.anno.IgnoreToken;
import com.pug.zixun.common.enums.AdminUserResultEnum;
import com.pug.zixun.common.ex.PugValidatorException;
import com.pug.zixun.common.utils.date.DateUtil;
import com.pug.zixun.common.utils.date.DateUtils;
import com.pug.zixun.common.utils.date.TmDateUtil;
import com.pug.zixun.common.utils.fn.asserts.Vsserts;
import com.pug.zixun.config.jwt.JwtService;
import com.pug.zixun.config.validator.PugAssert;
import com.pug.zixun.domain.User;
import com.pug.zixun.local.UserThreadLocal;
import com.pug.zixun.service.user.IUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.concurrent.TimeUnit;
@Component
@Slf4j
public class PassportLoginInterceptor implements HandlerInterceptor {
@Autowired
private JwtService jwtService;
@Autowired
private IUserService userService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// handler从object对象转换成具体的目标对象HandlerMethod
HandlerMethod handlerMethod = (HandlerMethod) handler;
// 获取执行的方法
Method method = handlerMethod.getMethod();
if (method.getAnnotation(IgnoreToken.class) != null ||
handlerMethod.getBeanType().getAnnotation(IgnoreToken.class) != null) {
return true;
}
// 获取用户请求的token
String token = getToken(request);
// 如果token没有传递,直接走统一异常处理 + 统一返回(内部自动帮你把数据通过jackson转换成json返回)
PugAssert.isEmptyEx(token, AdminUserResultEnum.TOKEN_NOT_FOUND);
// 获取请求头的用户id
String tokenUserId = getTokenUserId(request);
// 校验token是否合法
boolean refreshTokenRedis = refreshTokenRedis(token,tokenUserId,response);
PugAssert.isFalseEx(refreshTokenRedis, AdminUserResultEnum.TOKEN_ERROR);
// 解析token,获取用户id
PugAssert.isNullEx(tokenUserId, AdminUserResultEnum.USER_NULL_ERROR);
// 根据用户查询用户信息,实时性 db 0.007
User user = userService.getById(Long.parseLong(tokenUserId));
PugAssert.isNullEx(user, AdminUserResultEnum.USER_NULL_ERROR);
// 拉黑处理
if (user.getForbbiden() != null && user.getForbbiden().equals(0)) {
throw new PugValidatorException(AdminUserResultEnum.USER_FORBIDDEN_ERROR);
}
// 删除处理
if (user.getIsDelete() != null && user.getIsDelete().equals(1)) {
throw new PugValidatorException(AdminUserResultEnum.USER_FORBIDDEN_ERROR);
}
// 把用户信息放入到UserThreadLocal
UserThreadLocal.put(user);
return true;
}
/**
* token 自动续期
* @param token
* @param userId
* @return
*/
private boolean refreshTokenRedis(String token,String userId,HttpServletResponse response){
// Redis双倍缓存key
String tokenKey = "pug:user:login:token:" + token;
String cacheToken = stringRedisTemplate.opsForValue().get(tokenKey);
if(Vsserts.isEmpty(cacheToken)){
return false;
}
try {
// 把自己校验一次,如果自己能通过,说明token还没有过期
jwtService.verify2(token);
}catch (TokenExpiredException tokenExpiredException){
// 如果过期了。redis还能找到。说明还可以继续激活使用
if (stringRedisTemplate.hasKey(tokenKey)) {
// 生成新的token
String newToken = jwtService.createToken(new Long(userId));
stringRedisTemplate.opsForValue().set(tokenKey,newToken,JwtService.TOKEN_EXPIRE_TIME * 2, TimeUnit.MILLISECONDS);
return true;
}
}catch ( Exception ex){
throw new PugValidatorException(AdminUserResultEnum.TOKEN_ERROR_STATUS);
}
return true;
}
/**
* 签发时间续期
*
* @param token
* @param tokenUserId
* @param response
*/
private void refreshToken(String token, Long tokenUserId, HttpServletResponse response) {
// token续期
// 获取token的签发时间 --------第一种写法
Date signTokenTime = jwtService.getTokenIssuedTime(token);
int diffminutes = TmDateUtil.diffminutes(signTokenTime,new Date());
// 开始刷新token 10的含义是:旧的token还剩下10分钟,在最后的这10分钟范围内去续期,
// 假设你的token存活时间(TOKEN_EXPIRE_TIME=30)。那么久是久的token存活20分钟,在20分钟以后时间内都是续期时间点。
Long period = JwtService.TOKEN_EXPIRE_TIME - 10;
if(diffminutes >= period ){
// 续期,重新生成一个新的token
String newToken = jwtService.createToken(tokenUserId);
// 通过response的头部输出token,然后前台通过reponse获取
response.setHeader("x-auth-token", newToken);
}
}
/**
* 过期时间续期
*
* @param token
* @param tokenUserId
* @param response
*/
private void refreshToken2(String token, Long tokenUserId, HttpServletResponse response) {
// token续期
// 获取token的签发时间 --------第一种写法
Date expireTime = jwtService.getTokenExpireTime(token);
// 假设过期时间是 30分钟,用过期时间减去当前时间:30 29 28 27 20...10
int diffminutes = TmDateUtil.diffminutes(new Date(),expireTime);
// 如果时间以及过去了20分钟,到最后十分钟的时候就开始续期 10 9 8 7
if(diffminutes <= 10 ){
// 续期,重新生成一个新的token
String newToken = jwtService.createToken(tokenUserId);
// 通过response的头部输出token,然后前台通过reponse获取
response.setHeader("x-auth-token", newToken);
}
}
/**
* 获取请求头的token
*
* @param request
* @return
*/
private String getToken(HttpServletRequest request) {
String token = request.getHeader("token");
if (Vsserts.isEmpty(token)) {
return null;
}
if (!token.startsWith(JwtService.PUG_TOKEN_PREFIX)) {
throw new PugValidatorException(AdminUserResultEnum.TOKEN_ERROR_STATUS);
}
// 截图前缀
token = token.substring(JwtService.PUG_TOKEN_PREFIX.length());
// 返回
return token;
}
/**
* 获取请求头的token的用户ID
*
* @param request
* @return
*/
private String getTokenUserId(HttpServletRequest request) {
String tokenUserId = request.getHeader("token_userid");
if (Vsserts.isEmpty(tokenUserId)) {
return null;
}
// 返回
return tokenUserId;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
UserThreadLocal.remove();
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserThreadLocal.remove();
}
}
小结
- 这里为啥我们都要Redis 做数据存储(我们知道缓存有本地缓存(echcache)和分布式缓存(redis))。因为未来我们程序肯定是集群部署。
服务下线
登录拦截器添加以下代码
或者在退出拦截器里加这部分代码
退出拦截器
package com.pug.zixun.config.interceptor;
import com.pug.zixun.common.enums.AdminUserResultEnum;
import com.pug.zixun.common.ex.PugValidatorException;
import com.pug.zixun.common.utils.fn.asserts.Vsserts;
import com.pug.zixun.config.jwt.JwtService;
import com.pug.zixun.config.redis.AdminRedisKeyManager;
import com.pug.zixun.config.redis.IJwtBlackService;
import com.pug.zixun.local.UserThreadLocal;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 挤下线使用
*/
@Component
@Slf4j
public class PassportLogoutInterceptor implements HandlerInterceptor, AdminRedisKeyManager {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
@Qualifier("jwtBlackStringService")
private IJwtBlackService jwtBlackService;
@Autowired
private JwtService jwtService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取用户请求的token
String token = jwtService.getToken(request);
// 判断当前token是不是一个退出token,如果存在黑名单直接返回过期或者无效
if(jwtBlackService.isBlackList(token)){
throw new PugValidatorException(AdminUserResultEnum.TOKEN_ERROR_STATUS);
}
/*********************这里是用户输入的信息********************/
// 获取用户传递过来的tokenuuid
String tokenUuid = request.getHeader(TOKEN_UUID_NAME);
// 如果没有获取到,说明没有登录
Vsserts.isEmptyEx(tokenUuid, AdminUserResultEnum.USER_LOGIN_UUID_EMPTY);
// *******************从redis获取uuid********************/
String tokenUserId = request.getHeader(TOKEN_USERID_NAME);
String tokenUuidKey = USER_LOGIN_LOGOUT_KEY + tokenUserId;
String cacheUuid = stringRedisTemplate.opsForValue().get(tokenUuidKey);
// 如果没有获取到,说明没有登录
Vsserts.isEmptyEx(tokenUuid, AdminUserResultEnum.USER_LOGIN_UUID_EMPTY);
// *******************比较********************/
// 如果你当前访问的uuid和缓存的uuid不同,就说明你在别的地方登录了。
if (!tokenUuid.equalsIgnoreCase(cacheUuid)) {
throw new PugValidatorException(AdminUserResultEnum.USER_LOGIN_SAME);
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
UserThreadLocal.remove();
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserThreadLocal.remove();
}
}
退出功能
package com.pug.zixun.controller.login;
import com.pug.zixun.bo.UserBo;
import com.pug.zixun.common.enums.AdminUserResultEnum;
import com.pug.zixun.common.utils.fn.asserts.Vsserts;
import com.pug.zixun.common.utils.pwd.MD5Util;
import com.pug.zixun.config.jwt.JwtService;
import com.pug.zixun.config.redis.AdminRedisKeyManager;
import com.pug.zixun.config.redis.IJwtBlackService;
import com.pug.zixun.controller.BaseController;
import com.pug.zixun.domain.User;
import com.pug.zixun.service.user.IUserService;
import com.pug.zixun.vo.UserVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
@RestController
@Slf4j
public class PassportLoginController extends BaseController implements AdminRedisKeyManager {
@Autowired
private IUserService userService;
@Autowired
private JwtService jwtService;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
@Qualifier("jwtBlackStringService")
private IJwtBlackService jwtBlackService;
/**
* 登录
*
* @param userVo
* @return
*/
@PostMapping("/login/toLogin")
public UserBo logined(@RequestBody UserVo userVo) {
// 这里有校验,spring-validator框架来完成 或者用断言 或者用自己封装的
Vsserts.isEmptyEx(userVo.getUsername(), AdminUserResultEnum.USER_NAME_NOT_EMPTY);
Vsserts.isEmptyEx(userVo.getPassword(), AdminUserResultEnum.USER_PWD_NOT_EMPTY);
// 根据用户名称查询用户信息
User dbLoginUser = userService.login(userVo);
Vsserts.isNullEx(dbLoginUser, AdminUserResultEnum.USER_NULL_ERROR);
// 用户输入的密码
String inputPwd = MD5Util.md5slat(userVo.getPassword());
// 如果输入密码和数据库密码不一致
boolean isLogin = dbLoginUser.getPassword().equalsIgnoreCase(inputPwd);
// 如果输入的账号和有误,isLogin=false.注意isFalseEx在里面取反的,所以会抛出异常
Vsserts.isFalseEx(isLogin, AdminUserResultEnum.USER_INPUT_USERNAME_ERROR);
UserBo userBo = new UserBo();
// 根据用户生成token
String token = jwtService.createToken(dbLoginUser.getId());
userBo.setToken(token);
// 注意把一些敏感信息全部清空返回
dbLoginUser.setPassword(null);
userBo.setUser(dbLoginUser);
// 登录挤下线
String tokenUuid = UUID.randomUUID().toString();
String tokenUuidKey = USER_LOGIN_LOGOUT_KEY + dbLoginUser.getId();
stringRedisTemplate.opsForValue().set(tokenUuidKey, tokenUuid);
userBo.setTokenUuid(tokenUuid);
// 登录创建双倍时间,用于续期
jwtService.redisToken(token);
return userBo;
}
@PostMapping("/login/logout")
public String logout(HttpServletRequest request){
// 通过请求头获取
String token = jwtService.getToken(request);
String userId = jwtService.getTokenUserId(request);
Vsserts.isEmptyEx(token,AdminUserResultEnum.TOKEN_NOT_FOUND);
Vsserts.isNullEx(userId,AdminUserResultEnum.USER_NAME_NOT_EMPTY);
// 删除下线的uuid
String tokenUuidKey = USER_LOGIN_LOGOUT_KEY + userId;
stringRedisTemplate.delete(tokenUuidKey);
// 删除续期redis的key
String tokenKey = USER_LOGIN_TOKEN_KEY + token;
stringRedisTemplate.delete(tokenKey);
// 加黑名单
jwtBlackService.addBlackList(token);
return "logout success";
}
测试
先登录
退出