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.

Architect
Architect
Architect
Seamless Token Refresh in Spring Boot & Axios: Full Implementation Guide

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Spring BootAuthenticationaxiosgatewayJWTclient-sideserver-sidetoken refresh
Architect
Written by

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.

0 followers
Reader feedback

How this landed with the community

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.