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.
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_tokento call a refresh endpoint and obtain a new
access_token.
Backend Implementation Details
Dependency Configuration (pom.xml)
<code><dependencies>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies></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
AccessTokenCarries user identity, used frequently
High security risk, short expiration
RefreshTokenUsed 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.
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!
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.