How to Enforce Single-Device Login and Auto Token Refresh with Spring Boot 3, JWT, and Redis
Learn how to build a secure authentication system in Spring Boot 3 that restricts each user to a single active session, stores tokens in Redis, and implements automatic JWT token refresh, with detailed code examples and step‑by‑step explanations.
In modern applications, user authentication and authorization are critical, especially when handling multi‑device logins and frequent requests. This article explains how to ensure that each user can only be logged in on one device at a time and how to automatically refresh JWT tokens using Spring Boot 3, Spring Security 6, JWT, and Redis.
3.1 Single‑Device Login
1) Implementing single‑device authentication
The system guarantees that a user can only be logged in on one device simultaneously. When the user logs in on a new device, the previous session is invalidated. This is achieved by combining JWT with Redis to store and verify the token.
2) Code walkthrough: single‑device authentication
Security interceptor (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);
}
}Login token issuance (code walkthrough)
The following method creates LoginResult containing access and refresh tokens, stores them in Redis, and writes permission data for later access control.
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;
}3.2 Token Refresh
1) Implementing token refresh
When a client submits an expired accessToken together with a valid refreshToken, the system validates both tokens, checks consistency with the Redis cache, and issues a new token pair.
2) Code walkthrough: token refresh
@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);
}3.3 Service Exposure
The login service creates an authentication token, authenticates the user, and returns the generated JWT tokens.
@Override
public LoginResult login(LoginForm loginForm, LoginTypeEnum type) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginForm, type);
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
return jwtUtil.getLoginResult(authenticate);
}For a complete working example, refer to the source repository (e.g., https://gitee.com/fateyifei/yf ) and the online demo at http://yf.wiki/yf-vue-admin/login .
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
