思路
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: 0host: redis_ipport: redis_portconnect-timeout: 30000lettuce:pool:# 最大阻塞等待时间,负数表示没有限制max-wait: -1# 连接池中的最大空闲连接max-idle: 10# 连接池中的最小空闲连接min-idle: 0# 连接池中最大连接数,负数表示没有限制max-active: 50password: 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;@Autowiredpublic 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;@Autowiredpublic JWTAuthorizationFilter(AuthenticationManager authenticationManager, RedisUtils redisUtils) {super(authenticationManager);this.redisUtils = redisUtils;}@Overrideprotected 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 {@Beanpublic 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 {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {return true;}}