思路
jwttoken不设置过期时间
redis管理过期时间,并且续签
redis中key="login:"+userId, value=jwtUser
再次访问时,解析token中userId,并且根据过期时间自动续签
JWT 实现登录认证 + Token 自动续期方案
pom文件配置
<!--Redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.7</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.7</version>
<scope>runtime</scope>
</dependency>
<!--spring security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
application.yml配置
spring:
redis:
database: 0
host: redis_ip
port: redis_port
connect-timeout: 30000
lettuce:
pool:
# 最大阻塞等待时间,负数表示没有限制
max-wait: -1
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
# 连接池中最大连接数,负数表示没有限制
max-active: 50
password: redis_password
service层logon方法
@Override
public Object login(LoginDTO loginDTO) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginDTO.getUsername(), loginDTO.getPassword());
Authentication authenticate;
try {
// 对登录用户进行认证
authenticate = authenticationManager.authenticate(authenticationToken);
} catch (AuthenticationException ex) {
throw new ServiceException(ex.getMessage());
}
if (Objects.isNull(authenticate)) {
throw new ServiceException("登录失败");
}
JWTUser jwtUser = (JWTUser)authenticate.getPrincipal();
if(jwtUser.getUserDO().getDeleteFlag()!=0){
throw new ServiceException("用户已被停用,如需开启,请联系管理员");
}
LoginSuccessDTO loginSuccessDTO = new LoginSuccessDTO();
loginSuccessDTO.setUserId(jwtUser.getId());
loginSuccessDTO.setUsername(jwtUser.getUsername());
loginSuccessDTO.setNickName(jwtUser.getUserDO().getNickName());
loginSuccessDTO.setUserRealName(jwtUser.getUserDO().getUserRealName());
loginSuccessDTO.setDepartment(jwtUser.getUserDO().getDepartment());
Map<String, Object> userInfo = DataConvert.BeanPropertyNameTraverse(loginSuccessDTO);
String token = JwtUtils.generateToken(userInfo);
String userId = jwtUser.getId();
Date expirationTime = JwtUtils.getExpirationTime();
redisUtils.set("login:" + userId, jwtUser, AuthConstant.EXPIRATION_TIME_IN_SECOND, TimeUnit.SECONDS);
loginSuccessDTO.setLoginTime(new Date());
loginSuccessDTO.setExpireTime(expirationTime);
loginSuccessDTO.setToken(token);
return loginSuccessDTO;
}
@Override
public Object logout() {
/*
* 获取SecurityContextHolder中的userId。
* 注销操作也是需要携带token的,spring security已经在内部对token进行验证,
* 并能够从redis取出用户信息
*/
UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();
JWTUser jwtUser = (JWTUser)authentication.getPrincipal();
String userId = jwtUser.getId();
redisUtils.del("login:"+userId);
return "注销成功";
}
token工具类
package cma.sxqxgxw.utils.jwt;
import cma.sxqxgxw.auth.constant.AuthConstant;
import cma.sxqxgxw.common.exception.ServiceException;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import javax.crypto.SecretKey;
import java.util.Map;
import java.util.Date;
public class JwtUtils {
private static final String SIGNATURE = "!QW@f5g%T^U&f3r32523534634634654754ygrgfdjjuhfdsdf6";
/**
* 计算token的过期时间
*
* @return 过期时间
*/
public static Date getExpirationTime() {
return new Date(System.currentTimeMillis() + AuthConstant.EXPIRATION_TIME_IN_SECOND * 1000);
}
/**
* 生成Token
* @param claims 用户信息
* @return token
*/
public static String generateToken(Map<String, Object> claims) {
Date createdTime = new Date();
//Date expirationTime = getExpirationTime();
byte[] keyBytes = SIGNATURE.getBytes();
SecretKey key = Keys.hmacShaKeyFor(keyBytes);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(createdTime)
//.setExpiration(expirationTime)
.signWith(key, SignatureAlgorithm.HS256)
.compact();
}
/**
* 从token中获取claim
*
* @param token token
* @return claim
*/
public static Claims getClaimsFromToken(String token) {
try {
return Jwts.parser()
.setSigningKey(SIGNATURE.getBytes())
.parseClaimsJws(token)
.getBody();
} catch (ExpiredJwtException | UnsupportedJwtException | MalformedJwtException | IllegalArgumentException e) {
throw new ServiceException("Token invalided.");
}
}
/**
* 获取token的过期时间
*
* @param token token
* @return 过期时间
*/
public static Date getExpirationDateFromToken(String token) {
return getClaimsFromToken(token)
.getExpiration();
}
/**
* 判断token是否过期
*
* @param token token
* @return 已过期返回true,未过期返回false
*/
public static Boolean isTokenExpired(String token) {
Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
/**
* 判断token是否非法
*
* @param token token
* @return 未过期返回true,否则返回false
*/
public static Boolean validateToken(String token) {
System.out.println("token valid");
return !isTokenExpired(token);
}
}
redis工具类
@Slf4j
@Component
public class RedisUtils {
private final RedisTemplate<String, Object> redisTemplate;
@Autowired
public RedisUtils(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
log.info("Redis连接失败,可能是Redis服务未启动。" + e.getMessage());
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public boolean del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
Boolean delete = redisTemplate.delete(key[0]);
return delete;
} else {
Long delete = redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));
return true;
}
}
return false;
}
//============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
log.info("插入操作时,Redis连接失败,可能是Redis服务未启动。" + e.getMessage());
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time, TimeUnit timeUnit) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, timeUnit);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
}
jwt过滤器
生成的token中不带有过期时间,token的过期时间由redis进行管理
当redis过期时间小于10分钟时,redis过期时间续签30分钟
@Component
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
private final RedisUtils redisUtils;
@Autowired
public JWTAuthorizationFilter(AuthenticationManager authenticationManager, RedisUtils redisUtils) {
super(authenticationManager);
this.redisUtils = redisUtils;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
request.setAttribute("errorMessage", "Token缺失,请检查请求Header");
chain.doFilter(request, response);
return;
}
try {
Claims claims = JwtUtils.getClaimsFromToken(token);
String userId = (String) claims.get("userId");
boolean hasKey = redisUtils.hasKey("login:" + userId);
if (!hasKey) {
request.setAttribute("errorMessage", "Token无效或已经过期");
throw new ServiceException("Token无效或已经过期");
}
// 令牌续签
long expire = redisUtils.getExpire("login:" + userId);
System.out.println("剩余时间: "+expire);
if (expire < AuthConstant.RENEWAL_TIME_IN_SECOND) {
redisUtils.expire("login:" + userId, AuthConstant.EXPIRATION_TIME_IN_SECOND);
System.out.println("令牌续签" + AuthConstant.EXPIRATION_TIME_IN_SECOND + "秒");
}
JWTUser jwtUser = (JWTUser) redisUtils.get("login:" + userId);
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(jwtUser, null, jwtUser.getAuthorities());
// 设置认证状态
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
chain.doFilter(request, response);
} catch (ServiceException e) {
request.setAttribute("errorMessage", e.getMessage());
chain.doFilter(request, response);
}
}
}
过期时间常量
public class AuthConstant {
/**
* 令牌有效时长
*/
public static final Long EXPIRATION_TIME_IN_SECOND = 60*30L;
/**
* 过期时间剩余多少时间时续签
*/
public static final Long RENEWAL_TIME_IN_SECOND = 60*10L;
}
Redis配置类
package cma.sxqxgxw.common.config.redis;
import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis配置
* 主要指定自定义序列化器,避免序列化反序列化失败
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 参照StringRedisTemplate内部实现指定序列化器
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 设置key的序列化器
redisTemplate.setKeySerializer(keySerializer());
redisTemplate.setHashKeySerializer(keySerializer());
// 设置value的序列化器
// 解决autoType is not support.xx.xx的问题
String[] acceptNames = {"org.springframework.security.core.authority.SimpleGrantedAuthority"};
GenericFastJsonRedisSerializer serializer = new GenericFastJsonRedisSerializer(acceptNames);
redisTemplate.setValueSerializer(serializer);
redisTemplate.setHashValueSerializer(serializer);
return redisTemplate;
}
private RedisSerializer<String> keySerializer(){
return new StringRedisSerializer();
}
//使用Jackson序列化器
private RedisSerializer<Object> valueSerializer(){
return new GenericJackson2JsonRedisSerializer();
}
}
Jwt拦截器
package cma.sxqxgxw.common.interceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
return true;
}
}