SpringBoot实战:SpringBoot 实现一人一号,无感刷新Jwt
引言
在现代应用的安全架构中,用户认证与授权机制占据着核心地位。特别是在多设备登录和高频请求的环境中,确保每位用户仅能通过一个账号登录,并有效管理Token的刷新策略,成为了后端开发中的重要挑战。通过整合Spring Boot 3、Spring Security 6、JWT(JSON Web Tokens)以及Redis等先进技术,可以构建出一个高效、安全的用户认证体系。本文将详细阐述如何实现“一人一号”的登录限制以及Token的无感刷新功能,以提升系统的安全性和用户体验。
一、一人一号认证
在JwtTokenFilter
拦截器中,核心任务是解析请求中的JWT Token,并与Redis中存储的Token进行比对,确保用户的Token有效且未被篡改。这种机制确保了当用户的Token变更后,任何旧的或失效的Token都将被拒绝访问,从而增强系统的安全性。
1.1 JwtTokenFilter拦截器代码
@Component
@RequiredArgsConstructor
public class JwtTokenFilter extends OncePerRequestFilter {
private final JwtUtil jwtUtil;
private final RedisUtil redisUtil;
private final SystemConfiguration systemConfiguration;
private final ServerProperties properties;
@Override
protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain) throws ServletException, IOException {
String token = jwtUtil.removeTokenPrefix(request);
String uri = request.getRequestURI();
String contextPath = properties.getServlet().getContextPath();
if (StringUtils.hasText(contextPath)) {
uri = uri.substring(contextPath.length());
}
if (!SecurityUtil.isWhitelisted(uri, systemConfiguration.getSecurityWhitelistPaths()) && StringUtils.hasText(token)) {
Authentication auth = jwtUtil.getAuthentication(token);
if (auth == null) {
if (jwtUtil.isJwtExpired(token)) {
ResponseUtil.writeIResultCodeMsg(response, HttpStatus.UNAUTHORIZED, ResultCode.AUTH_TOKEN_EXPIRED);
} else {
ResponseUtil.writeIResultCodeMsg(response, HttpStatus.UNAUTHORIZED, ResultCode.AUTH_TOKEN_INVALID);
}
return;
}
Long userId = SecurityUtil.getUserId(auth);
if (userId == null) {
ResponseUtil.writeIResultCodeMsg(response, HttpStatus.UNAUTHORIZED, ResultCode.AUTH_TOKEN_INVALID);
return;
}
LoginResult loginResult = redisUtil.getCacheObject(RedisKeyConstants.USER_TOKEN_CACHE_PREFIX + userId);
if (loginResult == null) {
ResponseUtil.writeIResultCodeMsg(response, HttpStatus.UNAUTHORIZED, ResultCode.AUTH_KICK_OUT);
return;
}
if (!token.equals(loginResult.getAccessToken())) {
ResponseUtil.writeIResultCodeMsg(response, HttpStatus.FORBIDDEN, ResultCode.AUTH_USER_ELSEWHERE_LOGIN);
return;
}
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
}
1.2 代码解析
OncePerRequestFilter
:确保在每个请求中只执行一次过滤操作。JwtUtil
:用于解析和验证JWT Token。RedisUtil
:用于与Redis进行交互,存储和验证用户的Token。- 核心流程是将请求中的Token与Redis中存储的Token进行比对,不一致时则拒绝访问,确保“一人一号”的原则。
1.3 登录JWT流程
每次用户登录后,系统会将新的Token存入Redis,并覆盖掉旧的Token。当用户使用旧Token尝试访问时,拦截器会拒绝该请求。
public LoginResult getLoginResult(Authentication authenticate) {
if (authenticate == null || authenticate.getPrincipal() == null) {
return null;
}
SysUserDetails principal = (SysUserDetails) authenticate.getPrincipal();
Duration accessTokenExpirationTime = jwtConfiguration.getAccessTokenExpirationTime();
Duration refreshTokenExpirationTime = jwtConfiguration.getRefreshTokenExpirationTime();
String accessToken = generateAccessToken(authenticate);
String refreshToken = generateRefreshToken(authenticate);
LoginResult result = LoginResult.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.expires(Date.from(Instant.now().plus(accessTokenExpirationTime)).getTime())
.build();
redisUtil.setCacheObject(RedisKeyConstants.USER_TOKEN_CACHE_PREFIX + principal.getUserId(), result, refreshTokenExpirationTime.toMillis(), TimeUnit.MILLISECONDS);
redisUtil.setCacheObject(RedisKeyConstants.USER_PERMISSIONS_CACHE_PREFIX + principal.getUserId(), principal.getPermissions(), refreshTokenExpirationTime.toMillis(), TimeUnit.MILLISECONDS);
return result;
}
二、无感刷新Token
无感刷新Token是通过解析和验证用户的AccessToken与RefreshToken,确保在合法情况下才能刷新Token。以下是无感刷新Token的实现。
2.1 Token刷新流程
@Override
public LoginResult refreshToken(RefreshTokenForm refreshTokenForm) {
if (!jwtUtil.isJwtExpired(refreshTokenForm.getAccessToken())) {
throw new ServiceException(ResultCode.AUTH_MALICIOUS_TOKEN_REFRESH);
}
Long userId = jwtUtil.getRefreshTokenUserId(refreshTokenForm.getRefreshToken());
if (userId == null) {
if (jwtUtil.isJwtExpired(refreshTokenForm.getRefreshToken())) {
throw new ServiceException(ResultCode.AUTH_TOKEN_EXPIRED);
} else {
throw new ServiceException(ResultCode.AUTH_MALICIOUS_TOKEN_REFRESH);
}
}
LoginResult loginResult = redisUtil.getCacheObject(RedisKeyConstants.USER_TOKEN_CACHE_PREFIX + userId);
if (loginResult == null) {
throw new ServiceException(ResultCode.AUTH_TOKEN_EXPIRED);
}
if (!refreshTokenForm.getAccessToken().equals(loginResult.getAccessToken()) || !refreshTokenForm.getRefreshToken().equals(loginResult.getRefreshToken())) {
throw new ServiceException(ResultCode.AUTH_MALICIOUS_TOKEN_REFRESH);
}
return jwtUtil.refreshToken(refreshTokenForm);
}
2.2 代码解析
- AccessToken过期检测:如果AccessToken未过期,则视为恶意刷新,抛出异常。
- RefreshToken验证:通过解析RefreshToken获取用户ID,并与Redis中存储的Token进行比对,确保一致性。
- 返回新Token:验证通过后,系统生成新的AccessToken与RefreshToken并返回,保持会话连续性。
2.3 关键点解析
- 验证AccessToken是否过期:通过
jwtUtil.isJwtExpired
方法检测用户的AccessToken是否过期,未过期的刷新请求将视为恶意行为。 - 检验Redis中的Token信息:确保用户提交的Token与Redis中存储的完全一致,防止非法刷新。
通过该机制,系统确保了用户认证过程的安全性,同时实现了Token的无感刷新,使用户体验更加流畅。