Why Seamless Token Refresh?
When performing business operations on a system page, sudden logout and redirection to the login page may occur. This is typically due to token expiration causing authentication failure.
The solution involves automatic token refresh and tokan renewal.
Approach: If a token is about to expire, generate a new token during permission validation and return it to the client. The client updates the stored token. Alternatively, a scheduled task can extend the token's validity without generating a new one.
Automatic Token Refresh (Backend Solution)
The backend checks token expiration. If the token is near expiration, a new token is placed in the response header. The frontend intercepts it and updates the local token.
Example Code
Add 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>
JWT utility class:
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
public class JwtUtil {
public static final Long JWT_TTL = 60 * 60 * 1000 * 24; // 24 hours
public static final String JWT_KEY = "secret";
public static String getUUID() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
return builder.compact();
}
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());
return builder.compact();
}
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if (ttlMillis == null) {
ttlMillis = JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid)
.setSubject(subject)
.setIssuer("app")
.setIssuedAt(now)
.signWith(signatureAlgorithm, secretKey)
.setExpiration(expDate);
}
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JWT_KEY);
return new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
}
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
Test unit:
@Test
void test() throws Exception {
String token = JwtUtil.createJWT("1735209949551763457");
System.out.println("Token: " + token);
Date tokenExpirationDate = getTokenExpirationDate(token);
System.out.println(tokenExpirationDate);
long exp = tokenExpirationDate.getTime();
long cur = System.currentTimeMillis();
System.out.println(exp);
System.out.println(cur);
System.out.println(exp - cur);
}
public static Date getTokenExpirationDate(String token) {
try {
SecretKey secretKey = generalKey();
Claims claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody();
return claims.getExpiration();
} catch (ExpiredJwtException | SignatureException e) {
throw new RuntimeException("Invalid token", e);
}
}
By comparing expiration time with current time, if the difference is less than a threshold, a new token is generated and returned in the response header.
Frontend Token Renewal (Client-Side Solution)
The frontend monitors token expiration. When the token is about to expire, it sends a request too a renewal endpoint. This often uses two tokens: access token (AT) and refresh token (RT).
- AT is short-lived and sent with every request, reducing risk of hijacking.
- RT is long-lived and used only to refresh AT, enhancing convenience.
This is a standard security practice.
Handling Edge Cases
What if the user is filling a form for a long time without sending requests? When submitting, the backend returns 401.
- Backend-only solution: The frontend, upon receiving 401, saves form data locally, redirects to login, and after login restores the form.
- Frontend solution: Listen to RT expiration and proactive refresh it. Also implement a draft feature to save form data.
These strategies ensure a seamless user experience.