Skip to content

Commit

Permalink
基于Redis的Token自动续签优化
Browse files Browse the repository at this point in the history
  • Loading branch information
chao committed Dec 10, 2021
1 parent 8aba757 commit d2185f2
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 57 deletions.
4 changes: 4 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.jwt.config.security;

import com.example.jwt.service.RedisService;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
Expand Down Expand Up @@ -31,8 +32,10 @@
@Slf4j
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {

public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
private final RedisService redisService;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager, RedisService redisService) {
super(authenticationManager);
this.redisService = redisService;
}

@Override
Expand All @@ -48,77 +51,98 @@ private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest
*/
String token = request.getHeader("Authorization");
if (StringUtils.hasLength(token)) {
try {
Claims claims = Jwts.parser()
// 设置生成token的签名key
.setSigningKey(ConstantKey.SIGNING_KEY)
// 解析token
.parseClaimsJws(token).getBody();
String user = claims.getSubject();
String cacheToken = String.valueOf(redisService.get(token));
if (StringUtils.hasLength(token) && !"null".equals(cacheToken)) {
String user = null;
try {
Claims claims = Jwts.parser()
// 设置生成token的签名key
.setSigningKey(ConstantKey.SIGNING_KEY)
// 解析token
.parseClaimsJws(cacheToken).getBody();
// 取出用户信息
user = claims.getSubject();
// 重设Redis超时时间
resetRedisExpire(token, claims);
} catch (ExpiredJwtException e) {
log.info("Token过期续签,ExpiredJwtException={}", e.getMessage());
Claims claims = e.getClaims();
// 取出用户信息
user = claims.getSubject();
// 刷新Token
refreshToken(token, claims);
} catch (UnsupportedJwtException e) {
log.warn("访问[{}]失败,UnsupportedJwtException={}", request.getRequestURI(), e.getMessage());
} catch (MalformedJwtException e) {
log.warn("访问[{}]失败,MalformedJwtException={}", request.getRequestURI(), e.getMessage());
} catch (SignatureException e) {
log.warn("访问[{}]失败,SignatureException={}", request.getRequestURI(), e.getMessage());
} catch (IllegalArgumentException e) {
log.warn("访问[{}]失败,IllegalArgumentException={}", request.getRequestURI(), e.getMessage());
}
if (user != null) {
// 获取用户权限和角色
String[] split = user.split("-")[1].split(",");
ArrayList<GrantedAuthority> authorities = new ArrayList<>();
for (String s : split) {
authorities.add(new GrantedAuthorityImpl(s));
}
// 刷新Token
refreshToken(response, claims);
// 返回Authentication
return new UsernamePasswordAuthenticationToken(user, null, authorities);
}
} catch (ExpiredJwtException e) {
log.warn("访问[{}]失败,ExpiredJwtException={}", request.getRequestURI(), e.getMessage());
} catch (UnsupportedJwtException e) {
log.warn("访问[{}]失败,UnsupportedJwtException={}", request.getRequestURI(), e.getMessage());
} catch (MalformedJwtException e) {
log.warn("访问[{}]失败,MalformedJwtException={}", request.getRequestURI(), e.getMessage());
} catch (SignatureException e) {
log.warn("访问[{}]失败,SignatureException={}", request.getRequestURI(), e.getMessage());
} catch (IllegalArgumentException e) {
log.warn("访问[{}]失败,IllegalArgumentException={}", request.getRequestURI(), e.getMessage());
}
}
log.warn("访问[{}]失败,需要身份认证", request.getRequestURI());
return null;
}

/**
* 刷新Token
* 刷新Token的时机:
* 1. 当前时间 < token过期时间
* 2. 当前时间 > (签发时间 + (token过期时间 - token签发时间)/2)
* 重设Redis超时时间
* 当前时间 + (`cacheToken`过期时间 - `cacheToken`签发时间)
*/
private void refreshToken(HttpServletResponse response, Claims claims) {
private void resetRedisExpire(String token, Claims claims) {
// 当前时间
long current = System.currentTimeMillis();
// token签发时间
long issuedAt = claims.getIssuedAt().getTime();
// token过期时间
long expiration = claims.getExpiration().getTime();
// (当前时间 < token过期时间) && (当前时间 > (签发时间 + (token过期时间 - token签发时间)/2))
if ((current < expiration) && (current > (issuedAt + ((expiration - issuedAt) / 2)))) {
/*
* 重新生成token
*/
Calendar calendar = Calendar.getInstance();
// 设置签发时间
calendar.setTime(new Date());
Date now = calendar.getTime();
// 设置过期时间: 5分钟
calendar.add(Calendar.MINUTE, 5);
Date time = calendar.getTime();
String refreshToken = Jwts.builder()
.setSubject(claims.getSubject())
// 签发时间
.setIssuedAt(now)
// 过期时间
.setExpiration(time)
// 算法与签名(同生成token):这里算法采用HS512,常量中定义签名key
.signWith(SignatureAlgorithm.HS512, ConstantKey.SIGNING_KEY)
.compact();
// 主动刷新token,并返回给前端
response.addHeader("refreshToken", refreshToken);
log.info("刷新token执行时间: {}", (System.currentTimeMillis() - current) + " 毫秒");
}
// 当前时间 + (`cacheToken`过期时间 - `cacheToken`签发时间)
long expireAt = current + (expiration - issuedAt);
// 重设Redis超时时间
redisService.expire(token, expireAt);
}

/**
* 刷新Token
* 刷新Token的时机: 当cacheToken已过期 并且Redis在有效期内
* 重新生成Token并覆盖Redis的v值(这时候k、v值不一样了),然后设置Redis过期时间为:新Token过期时间
*/
private void refreshToken(String token, Claims claims) {
// 当前时间
long current = System.currentTimeMillis();
/*
* 重新生成token
*/
Calendar calendar = Calendar.getInstance();
// 设置签发时间
calendar.setTime(new Date());
Date now = calendar.getTime();
// 设置过期时间: 5分钟
calendar.add(Calendar.MINUTE, 5);
Date time = calendar.getTime();
String refreshToken = Jwts.builder()
.setSubject(claims.getSubject())
// 签发时间
.setIssuedAt(now)
// 过期时间
.setExpiration(time)
// 算法与签名(同生成token):这里算法采用HS512,常量中定义签名key
.signWith(SignatureAlgorithm.HS512, ConstantKey.SIGNING_KEY)
.compact();
// 将refreshToken覆盖Redis的v值,并设置超时时间为refreshToken过期时间
redisService.set(token, refreshToken, time);
// 打印日志
log.info("刷新token执行时间: {}", (System.currentTimeMillis() - current) + " 毫秒");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.alibaba.fastjson.JSON;
import com.example.jwt.entity.ResponseJson;
import com.example.jwt.service.RedisService;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
Expand Down Expand Up @@ -34,9 +35,10 @@
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter {

private final AuthenticationManager authenticationManager;

public JwtLoginFilter(AuthenticationManager authenticationManager) {
private final RedisService redisService;
public JwtLoginFilter(AuthenticationManager authenticationManager, RedisService redisService) {
this.authenticationManager = authenticationManager;
this.redisService = redisService;
}

/**
Expand Down Expand Up @@ -82,6 +84,8 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR
// 自定义算法与签名:这里算法采用HS512,常量中定义签名key
.signWith(SignatureAlgorithm.HS512, ConstantKey.SIGNING_KEY)
.compact();
// 将token存入redis,并设置超时时间为token过期时间
redisService.set(token, token, time);
/*
* 返回token
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.jwt.config.security;

import com.example.jwt.service.RedisService;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
Expand All @@ -16,9 +17,9 @@
/**
* SpringSecurity配置类
* 通过继承 WebSecurityConfigurerAdapter 实现自定义Security策略
* @Configuration:声明当前类是一个配置类
* @EnableWebSecurity:开启WebSecurity模式
* @EnableGlobalMethodSecurity(securedEnabled=true):开启注解,支持方法级别的权限控制
* `@Configuration`:声明当前类是一个配置类
* `@EnableWebSecurity`:开启WebSecurity模式
* `@EnableGlobalMethodSecurity(securedEnabled=true)`:开启注解,支持方法级别的权限控制
*
* @author : Charles
* @date : 2021/12/2
Expand All @@ -28,6 +29,9 @@
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Resource
private RedisService redisService;

@Resource
private UserDetailsService userDetailsService;

Expand Down Expand Up @@ -64,9 +68,9 @@ protected void configure(HttpSecurity http) throws Exception {
.and().authorizeRequests().anyRequest().authenticated()
.and()
// 自定义JWT登录过滤器
.addFilter(new JwtLoginFilter(authenticationManager()))
.addFilter(new JwtLoginFilter(authenticationManager(), redisService))
// 自定义JWT认证过滤器
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
.addFilter(new JwtAuthenticationFilter(authenticationManager(), redisService))
// 自定义认证拦截器,也可以直接使用内置实现类Http403ForbiddenEntryPoint
.exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPointImpl())
// 允许跨域
Expand Down
86 changes: 86 additions & 0 deletions src/main/java/com/example/jwt/service/RedisService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.example.jwt.service;

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

import javax.annotation.Resource;
import java.io.Serializable;
import java.util.Date;
import java.util.concurrent.TimeUnit;

/**
* Redis 服务工具类
*
* @author : Charles
* @date : 2021/12/10
*/
@Service
public class RedisService {
@Resource
private RedisTemplate<Serializable, Object> redisTemplate;
/**
* 读取缓存
*/
public Object get(String key) {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
return operations.get(key);
}
/**
* 判断缓存中是否存在
*/
public boolean exists(String key) {
return StringUtils.hasLength(key) && Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 删除缓存
*/
public void remove(String key) {
if (exists(key)) {
redisTemplate.delete(key);
}
}
/**
* 写入缓存
*/
public boolean set(String key, Object value) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 写入缓存 并 加上过期时间
*/
public boolean set(String key, Object value, Date date) {
boolean result = false;
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expireAt(key, date);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 写入过期时间(毫秒)
*/
public boolean expire(String key, Long expireTimeMillis) {
boolean result = false;
try {
redisTemplate.expire(key, expireTimeMillis, TimeUnit.MILLISECONDS);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
}
8 changes: 8 additions & 0 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ spring:
url: jdbc:mysql://192.168.2.100:3306/security?characterEncoding=UTF8&serverTimezone=Asia/Shanghai
username: developer
password: 05bZ/OxTB:X+yd%1
redis:
host: 192.168.2.100
port: 6379
#password: 123456
# Redis数据库索引(默认为0)
database: 0
# 连接超时时间(毫秒)
timeout: 5000

mybatis:
mapper-locations: classpath:mapper/*.xml

0 comments on commit d2185f2

Please sign in to comment.