Backend Development 9 min read

How to Implement Seamless Token Refresh in Spring Boot for Continuous User Sessions

This article explains how to achieve invisible token renewal in modern web systems using Spring Boot, JWT, and Redis, detailing backend auto‑renewal, frontend interception, dual‑token strategies, and handling edge cases like silent form timeouts to keep user sessions uninterrupted.

Java Tech Enthusiast
Java Tech Enthusiast
Java Tech Enthusiast
How to Implement Seamless Token Refresh in Spring Boot for Continuous User Sessions

In modern web systems, balancing user experience and security is a core challenge for backend development. This article demonstrates, through a real‑world scenario, how to use Spring Boot to implement a seamless token refresh mechanism that keeps users online without interruption while automatically renewing their identity.

Background: Why Seamless Refresh?

Imagine entering data in an admin panel and suddenly being redirected to the login page, losing all unsaved work. This typical issue occurs when a token expires, especially when tokens are stored in caches like Redis.

Root Cause

Backends often use JWT for stateless authentication, but JWTs expire and cannot be modified or revoked, leading to poor user experience without a refresh strategy.

Core Strategy: Token Seamless Renewal

Option 1 – Backend Auto‑Renewal (Recommended)

On each request, the backend checks the token's remaining validity:

If the token is near expiration (e.g., less than 5 minutes), generate a new token and include it in the response header.

The frontend intercepts the header; if a new token differs from the local one, it updates the stored token automatically.

Option 2 – Frontend Proactive Renewal (Supplementary)

The frontend maintains a pair of tokens:

access_token

(short‑lived) and

refresh_token

(long‑lived).

Periodically, the frontend uses the

refresh_token

to call a refresh endpoint and obtain a new

access_token

.

Backend Implementation Details

Dependency Configuration (pom.xml)

<code>&lt;dependencies&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;cn.hutool&lt;/groupId&gt;
        &lt;artifactId&gt;hutool-all&lt;/artifactId&gt;
        &lt;version&gt;5.5.1&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;com.alibaba&lt;/groupId&gt;
        &lt;artifactId&gt;fastjson&lt;/artifactId&gt;
        &lt;version&gt;1.2.33&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;io.jsonwebtoken&lt;/groupId&gt;
        &lt;artifactId&gt;jjwt&lt;/artifactId&gt;
        &lt;version&gt;0.9.1&lt;/version&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;</code>

JWT Utility (JwtUtil.java)

<code>package com.icoderoad.auth.utils;

import io.jsonwebtoken.*;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.*;

public class JwtUtil {
    public static final long JWT_TTL = 1000L * 60 * 60 * 24; // 24 hours
    public static final String JWT_KEY = "qx";

    public static String createJWT(String subject) {
        return getJwtBuilder(subject, null, UUID.randomUUID().toString().replace("-", "")).compact();
    }

    public static String createJWT(String subject, Long ttlMillis) {
        return getJwtBuilder(subject, ttlMillis, UUID.randomUUID().toString()).compact();
    }

    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        long nowMillis = System.currentTimeMillis();
        long expMillis = (ttlMillis != null ? nowMillis + ttlMillis : nowMillis + JWT_TTL);
        SecretKey secretKey = generalKey();
        return Jwts.builder()
                .setId(uuid)
                .setSubject(subject)
                .setIssuer("icoderoad")
                .setIssuedAt(new Date(nowMillis))
                .setExpiration(new Date(expMillis))
                .signWith(SignatureAlgorithm.HS256, secretKey);
    }

    public static Claims parseJWT(String jwt) throws Exception {
        return Jwts.parser()
                .setSigningKey(generalKey())
                .parseClaimsJws(jwt)
                .getBody();
    }

    public static SecretKey generalKey() {
        byte[] key = Base64.getDecoder().decode(JWT_KEY);
        return new SecretKeySpec(key, 0, key.length, "AES");
    }

    public static Date getExpiration(String jwt) {
        try {
            return parseJWT(jwt).getExpiration();
        } catch (Exception e) {
            throw new RuntimeException("Token parsing failed", e);
        }
    }
}
</code>

Token Interceptor (AuthInterceptor.java)

<code>public class AuthInterceptor implements HandlerInterceptor {
    private static final long REFRESH_THRESHOLD = 1000L * 60 * 5; // refresh within last 5 minutes

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("Authorization");
        if (StringUtils.isEmpty(token)) {
            throw new RuntimeException("Not logged in");
        }
        Claims claims = JwtUtil.parseJWT(token);
        long now = System.currentTimeMillis();
        long exp = claims.getExpiration().getTime();
        if (exp - now < REFRESH_THRESHOLD) {
            String newToken = JwtUtil.createJWT(claims.getSubject());
            response.setHeader("X-Token-Refresh", newToken);
        }
        return true;
    }
}
</code>

Frontend Handling (Vue + Axios Example)

<code>axios.interceptors.response.use(response => {
    const newToken = response.headers['x-token-refresh'];
    if (newToken && newToken !== localStorage.getItem('access_token')) {
        localStorage.setItem('access_token', newToken);
    }
    return response;
}, error => {
    // handle 401
    if (error.response.status === 401) {
        // optionally save draft and redirect to login
    }
    return Promise.reject(error);
});
</code>

AccessToken vs RefreshToken

Type

Purpose

Characteristics

AccessToken

Carries user identity, used frequently

High security risk, short expiration

RefreshToken

Used to renew AccessToken

Not exposed to frontend, usually stored in HttpOnly cookie

The standard double‑token model improves security and user experience by avoiding excessive refresh traffic.

Special Discussion: Silent Form Timeout Handling

Problem: Users fill a long form without sending requests; when they finally submit, the token has expired, causing a redirect to the login page and loss of data.

Recommended solutions:

Cache form data locally after a failed submission and restore it after re‑login.

Periodically send heartbeat requests based on user input activity to trigger backend renewal.

Conclusion

Implementing invisible token refresh aligns user experience with security. By combining backend intelligent checks with frontend interception—using either a dual‑token pattern or dynamic renewal—you achieve uninterrupted operations, automatic credential renewal, and finer‑grained security control.

Image
Image
backend developmentSpring BootsecurityVue.jsJWTToken Refresh
Java Tech Enthusiast
Written by

Java Tech Enthusiast

Sharing computer programming language knowledge, focusing on Java fundamentals, data structures, related tools, Spring Cloud, IntelliJ IDEA... Book giveaways, red‑packet rewards and other perks await!

0 followers
Reader feedback

How this landed with the community

login 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.