SpringBoot 集成 JWT(token),
拦截器自动验证验证 token 是否过期
token 自动刷新(单个 token 刷新机制,保证活跃用户不会掉线)
标准统一的 RESTFul 返回体数据格式
异常统一拦截处理
单个 token 刷新机制(介绍):
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6ksAAtY2-1613013321715)(13.SpringBoot%20%E9%9B%86%E6%88%90token%E5%AE%9E%E8%B7%B5%E8%AF%A6%E8%A7%A3.assets/1611831269211-d13250d1-227d-48f3-bdb4-40d8bd643652.png)]
token 距离发布token 2 个小时内的token为新生token,2-3 个小时的token为老年token
每次请求,前端带上 token,
(1)如果 token 为新 token ,服务器返回原来的 token
(2)如果 token 为老年 token,服务器返回 刷新后的新生token ,
(3)如果 token 为过期 token,服务器返回token过期 状态码 401,,请求失败, 前端重新登录
jwt 依赖
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.10.3version>
dependency>
整个 SpringBoot 依赖
<properties>
<java.version>1.8java.version>
<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
<spring-boot.version>2.3.7.RELEASEspring-boot.version>
properties>
<dependencies>
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.10.3version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-devtoolsartifactId>
<scope>runtimescope>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
<exclusions>
<exclusion>
<groupId>org.junit.vintagegroupId>
<artifactId>junit-vintage-engineartifactId>
exclusion>
exclusions>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<scope>testscope>
dependency>
dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-dependenciesartifactId>
<version>${spring-boot.version}version>
<type>pomtype>
<scope>importscope>
dependency>
dependencies>
dependencyManagement>
server:
port: 8081
spring:
application:
name: tokendemo
# token
token:
privateKey: 'fdasfgdsagaxgsregdfdjyghjfhebfdgwe45ygrfbsdfshfdsag'
yangToken: 1000000
oldToken: 3000000000
代码结构如下
AuthWebMvcConfigurer
@Configuration
public class AuthWebMvcConfigurer implements WebMvcConfigurer {
@Autowired
AuthHandlerInterceptor authHandlerInterceptor;
/**
* 给除了 /login 的接口都配置拦截器,拦截转向到 authHandlerInterceptor
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(authHandlerInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/login");
}
}
TokenTestController
@RestController
public class TokenTestController {
@Autowired
TokenUtil tokenUtil;
/**
* 使用 /login 请求获得 token, /login 不经过拦截器
*/
@RequestMapping("/login")
public String login(){
return tokenUtil.getToken("靓仔","admin");
}
/**
* 使用 /test-token 测试 token,进过拦截器
*/
@RequestMapping("/test-token")
public Map testToken(HttpServletRequest request){
String token = request.getHeader("token");
return tokenUtil.parseToken(token);
}
}
TokenAuthExpiredException
public class TokenAuthExpiredException extends RuntimeException{
}
AuthHandlerInterceptor
@Slf4j
@Component
public class AuthHandlerInterceptor implements HandlerInterceptor {
@Autowired
TokenUtil tokenUtil;
@Value("${token.privateKey}")
private String privateKey;
@Value("${token.yangToken}")
private Long yangToken;
@Value("${token.oldToken}")
private Long oldToken;
/**
* 权限认证的拦截操作.
*/
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
log.info("=======进入拦截器========");
// 如果不是映射到方法直接通过,可以访问资源.
if (!(object instanceof HandlerMethod)) {
return true;
}
//为空就返回错误
String token = httpServletRequest.getHeader("token");
if (null == token || "".equals(token.trim())) {
return false;
}
log.info("==============token:" + token);
Map<String, String> map = tokenUtil.parseToken(token);
String userId = map.get("userId");
String userRole = map.get("userRole");
long timeOfUse = System.currentTimeMillis() - Long.parseLong(map.get("timeStamp"));
//1.判断 token 是否过期
//年轻 token
if (timeOfUse < yangToken) {
log.info("年轻 token");
}
//老年 token 就刷新 token
else if (timeOfUse >= yangToken && timeOfUse < oldToken) {
httpServletResponse.setHeader("token",tokenUtil.getToken(userId,userRole));
}
//过期 token 就返回 token 无效.
else {
throw new TokenAuthExpiredException();
}
//2.角色匹配.
if ("user".equals(userRole)) {
log.info("========user账户============");
return true;
}
if ("admin".equals(userRole)) {
log.info("========admin账户============");
return true;
}
return false;
}
}
GlobalExceptionHandler
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* 用户 token 过期
* @return
*/
@ExceptionHandler(value = TokenAuthExpiredException.class)
@ResponseBody
public String tokenExpiredExceptionHandler(){
log.warn("用户 token 过期");
return "用户 token 过期";
}
}
TokenUtil
@Component
public class TokenUtil {
@Value("${token.privateKey}")
private String privateKey;
/**
* 加密token.
*/
public String getToken(String userId, String userRole) {
//这个是放到负载payLoad 里面,魔法值可以使用常量类进行封装.
String token = JWT
.create()
.withClaim("userId" ,userId)
.withClaim("userRole", userRole)
.withClaim("timeStamp", System.currentTimeMillis())
.sign(Algorithm.HMAC256(privateKey));
return token;
}
/**
* 解析token.
* (优化可以用常量固定魔法值+使用DTO在 mvc 之前传输数据,而不是 map,这里因为篇幅原因就不做了)
* {
* "userId": "3412435312",
* "userRole": "ROLE_USER",
* "timeStamp": "134143214"
* }
*/
public Map<String, String> parseToken(String token) {
HashMap<String, String> map = new HashMap<>();
DecodedJWT decodedjwt = JWT.require(Algorithm.HMAC256(privateKey))
.build().verify(token);
Claim userId = decodedjwt.getClaim("userId");
Claim userRole = decodedjwt.getClaim("userRole");
Claim timeStamp = decodedjwt.getClaim("timeStamp");
map.put("userId", userId.asString());
map.put("userRole", userRole.asString());
map.put("timeStamp", timeStamp.asLong().toString());
return map;
}
}
完整项目代码的地址
访问
localhost:8081/login
效果:
将 1 测试得到的 token 放到 header 里面测试 token是否可用
访问
localhost:8081/test-token
测试全局异常拦截类拦截到 TokenAuthExpiredException
异常,然后返回提示。
将过期时间调小,修改 application.yaml 文件,3 秒钟就过期
server:
port: 8081
spring:
application:
name: tokendemo
# token
token:
privateKey: 'fdasfgdsagaxgsregdfdjyghjfhebfdgwe45ygrfbsdfshfdsag'
yangToken: 1000
oldToken: 3000
重启应用测试:
完整项目代码的地址