Seamless Token Refresh in Spring Boot & Axios: Full Implementation Guide
This article explains how to implement invisible token refresh for authentication servers using Spring Boot gateway filters, JWT handling, Axios interceptors, and a client‑side timer, compares client‑ and server‑side approaches, and provides complete code examples and practical recommendations.
Overview
This guide demonstrates how to implement silent token refresh for a JWT‑based authentication system. It covers a Spring Boot 3 + Java 17 gateway that detects expired access tokens, a client‑side Axios interceptor that triggers a refresh request, a periodic token monitor to pre‑emptively refresh tokens, and server‑side enhancements that expose token expiration timestamps.
Server‑Side Gateway Filter
@Component
public class MyAccessFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String uri = request.getURI().getPath();
HttpMethod method = request.getMethod();
if (method.matches(HttpMethod.OPTIONS.name())) return chain.filter(exchange);
if (SecurityAccessConstant.REQUEST_LOGGING_URI.equals(uri) && method.matches(HttpMethod.POST.name()))
return chain.filter(exchange);
String token = JWTHelper.getToken(request.getHeaders().getFirst(SecurityAccessConstant.HEADER_NAME_TOKEN));
if (token != null) {
if (!JWTHelper.isOutDate(token)) {
return chain.filter(exchange);
} else {
if (!SecurityAccessConstant.REQUEST_REFRESH.equals(uri))
return ResponseUtils.out(exchange, ResultData.fail(ResultCodeEnum.NEED_TO_REFRESH_TOKEN.getCode(), ResultCodeEnum.NEED_TO_REFRESH_TOKEN.getMessage()));
return ResponseUtils.out(exchange, ResultData.fail(ResultCodeEnum.RC401.getCode(), ResultCodeEnum.RC401.getMessage()));
}
}
return ResponseUtils.out(exchange, ResultData.fail(ResultCodeEnum.RC401.getCode(), ResultCodeEnum.RC401.getMessage()));
}
@Override
public int getOrder() { return Ordered.LOWEST_PRECEDENCE; }
}The filter returns a custom status code (e.g., 511 ) when the access token is expired but a refresh token is still valid. If the request itself is a refresh request, it returns 401 .
Client‑Side Axios Interceptor
service.interceptors.response.use(
response => response.data.data,
async error => {
if (!error.response) return Promise.reject(error);
const status = error.response.status;
const authStore = useAuthStore();
let message = '';
switch (status) {
case 401:
authStore.reset();
window.sessionStorage.clear();
message = 'token expired, please log in';
window.location.href = '/auth/login';
break;
case 511:
try {
const data = await refresh();
if (data) {
window.sessionStorage.setItem('token', data);
error.config.headers['Authorization'] = 'Bearer ' + data;
return service(error.config);
}
} catch (e) {
router.push('/login');
}
break;
case 403: message = 'access denied'; break;
case 404: message = 'invalid URL'; break;
case 500: message = 'server error'; break;
default: message = 'network error';
}
Message.error(message);
return Promise.reject(error);
}
);Refresh Function
export async function refresh(): Promise<string> {
const refreshToken = window.sessionStorage.getItem('refreshToken');
if (!refreshToken) return '';
try {
const response = await axios({
method: 'GET',
url: 'http://127.0.0.1:9001/api/simple/cloud/access/refresh',
headers: { Authorization: `Bearer ${refreshToken}` }
});
return response.data.data || '';
} catch (e) {
console.log(e);
return '';
}
}When the gateway responds with 511 , the interceptor calls refresh(), stores the new access token, updates the failed request’s Authorization header, and retries the request.
Proactive Token Monitoring (Client‑Side)
Because the asynchronous refresh may finish after the original request, a timer checks the remaining lifetime of the stored token and refreshes it before it expires.
import { refresh } from '@/api/system/auth/index';
import { jwtDecode } from 'jwt-decode';
export class MyTimer {
private timerId: any = null;
private delay = 30000; // 30 s default interval
private minCheck = 60000; // refresh when < 1 min left
private static instance: MyTimer;
static getInstance(): MyTimer { if (!MyTimer.instance) MyTimer.instance = new MyTimer(); return MyTimer.instance; }
start() {
this.timerId = setInterval(async () => {
const token = window.sessionStorage.getItem('token');
if (token) {
const expireStr = window.sessionStorage.getItem('tokenExpire') as string;
const expiration = parseInt(expireStr, 10);
const remaining = expiration - Date.now();
if (remaining <= this.minCheck) {
try { await refresh(); } catch (e) { console.error('refresh failed', e); }
}
} else {
await refresh();
}
}, this.delay);
}
stop() { if (this.timerId !== null) { clearInterval(this.timerId); this.timerId = null; } }
setDelay(d: number) { this.delay = d; }
setMinCheck(m: number) { this.minCheck = m; }
}
export const myTimer = MyTimer.getInstance();
export function onPageRender() { myTimer.stop(); myTimer.start(); }After a successful login the application stores token, refreshToken, and tokenExpire (epoch milliseconds) in sessionStorage and starts the timer:
const clock = new MyTimer();
clock.start();Server‑Side Enhancements
Expose Expiration in Token Payload
public static Date getExpirationDate(String token) {
if (StringUtil.isBlank(token)) return null;
Claims claims = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody();
return claims.getExpiration();
}The authentication endpoint now returns a JSON map containing:
String[] tokenArray = JWTHelper.createToken(userId, email, perms);
map.put("token", tokenArray[0]);
map.put("tokenExpire", JWTHelper.getExpirationDate(tokenArray[0]).getTime());
map.put("refreshToken", tokenArray[1]);Updated Monitor Class
class MyTimer {
private timerId: any = null;
private delay = 30000;
private minCheck = 60000;
start() {
this.timerId = setInterval(async () => {
const token = window.sessionStorage.getItem('token');
if (token) {
const expireStr = window.sessionStorage.getItem('tokenExpire') as string;
const expiration = parseInt(expireStr, 10);
const remaining = expiration - Date.now();
if (remaining <= this.minCheck) {
try { await refresh(); } catch (e) { console.error(e); }
}
} else {
await refresh();
}
}, this.delay);
}
// stop, setDelay, setMinCheck omitted for brevity
}Page Load Hook
window.addEventListener('load', () => { onPageRender(); });Choosing Refresh Strategy
Server‑side refresh advantages: centralised security, reduced client complexity, consistent token state across devices.
Client‑side refresh advantages: immediate response, offline capability, flexibility for custom policies, and off‑loading refresh traffic from the authentication server.
The optimal choice depends on security requirements, expected load, and user‑experience goals.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
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.
