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.

Architect
Architect
Architect
How to Enforce Single-Device Login and Auto Token Refresh with Spring Boot 3, JWT, and Redis

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 .

Architecture diagram
Architecture diagram
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

redisSpring BootJWTBackend Securitytoken refresh
Architect
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.