Seamless Token Refresh: Backend Automatic Renewal and Frontend Token Extension with Java JWT
This article explains why invisible token refresh is needed, describes backend automatic token renewal using Java JWT, provides full Maven dependencies and JwtUtil implementation, shows unit‑test examples, and discusses frontend token extension strategies and edge‑case handling.
In modern web applications, token expiration often leads to sudden logout and a poor user experience; therefore a seamless ("无感") token refresh mechanism is required.
The backend solution checks the token's remaining lifetime and, when it is close to expiring, generates a new token and places it in the response header so that the frontend can replace the old token transparently.
Required Maven 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>The core Java class JwtUtil creates and parses JWT tokens, defines a default TTL of 24 hours, and includes helper methods for UUID generation, secret‑key creation, and token building:
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 = "qx";
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();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid){
SignatureAlgorithm sa = 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("sg")
.setIssuedAt(now)
.signWith(sa, secretKey)
.setExpiration(expDate);
}
public static SecretKey generalKey(){
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.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();
}
}A simple JUnit test demonstrates token creation and extraction of the expiration timestamp:
@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 - 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);
}
}On the frontend, token renewal can be handled by interceptors that replace the stored token when a new one appears in the response header; the common pattern uses a short‑lived access‑token (AT) and a long‑lived refresh‑token (RT).
When a user leaves a form idle for a long time and then submits, the backend may return 401 because the AT has expired. The suggested handling is to detect the 401, retrieve any saved form data from local storage, redirect the user to the login page, and after successful authentication restore the form data.
Additional considerations include periodically refreshing the RT before it expires and implementing a client‑side draft‑saving mechanism to avoid data loss during long idle periods.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
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.