Information Security 21 min read

Seamless Token Refresh: Server‑Side and Client‑Side Implementation with Spring Boot and Axios

This article explains how to implement invisible token refresh for authentication systems, covering server‑side gateway filtering in Spring Boot, client‑side Axios interceptors, timer‑based proactive checks, and practical code examples to address token expiration, parsing, and renewal challenges.

Architect
Architect
Architect
Seamless Token Refresh: Server‑Side and Client‑Side Implementation with Spring Boot and Axios

Token silent refresh is a mechanism that automatically updates an access token without user interaction, keeping the login session alive. The article first raises three common questions: whether the refresh logic should reside on the server or client, how to obtain the token's expiration time when parsing fails, and how to resend the original request after obtaining a new token.

1. Server‑side gateway interceptor

Implemented as a GlobalFilter in Spring Boot 3 + Java 17, the filter checks the request method and URI, extracts the token from the header, and determines if it is expired. If the token is still valid, the request proceeds; otherwise a custom status code (511) is returned to indicate that a refresh token is required.

@Component
public class MyAccessFilter implements GlobalFilter, Ordered {
    @Override
    public Mono
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; }
}

2. Client‑side Axios interceptor

The response interceptor examines the HTTP status: 401 triggers a logout, while 511 triggers a call to the refreshToken endpoint. Upon successful refresh, the new token replaces the old one in session storage and the original request is replayed.

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.removeItem('isAuthenticated');
                window.sessionStorage.removeItem('token');
                window.sessionStorage.removeItem('refreshToken');
                message = 'token 失效,请重新登录';
                window.location.href = '/auth/login';
                break;
            case 511:
                try {
                    const data = await refresh();
                    if (data) {
                        console.log('刷新 token 成功', data);
                        window.sessionStorage.setItem('token', data);
                        error.config.headers['Authorization'] = 'Bearer ' + data;
                        return service(error.config);
                    }
                } catch (err) {
                    console.error('刷新 token 失败', err);
                    router.push('/login');
                }
                break;
            case 403: message = '拒绝访问'; break;
            case 404: message = '请求地址错误'; break;
            case 500: message = '服务器故障'; break;
            default:  message = '网络连接故障';
        }
        Message.error(message);
        return Promise.reject(error);
    }
);

3. Refresh token method (TypeScript)

The refresh function sends a raw Axios request (bypassing the normal request wrapper to avoid interceptor loops) to obtain a new token.

export async function refresh(): Promise
{
    const refreshToken = window.sessionStorage.getItem('refreshToken');
    if (refreshToken === undefined) 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 (error) {
        console.log(error);
        return '';
    }
}

4. Proactive timer‑based monitoring

To avoid asynchronous race conditions, a timer class periodically checks the stored token's remaining lifetime (using the tokenExpire value supplied by the server). If the remaining time falls below a configurable threshold, the timer triggers the refresh method.

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
    private minCheck = 60000; // 1 min default
    static instance: MyTimer;
    static getInstance(): MyTimer { if (!MyTimer.instance) MyTimer.instance = new MyTimer(); return MyTimer.instance; }
    private constructor() {}
    start(): void {
        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); window.location.href = '/auth/login'; }
                }
            } else {
                window.location.href = '/auth/login';
            }
        }, this.delay);
    }
    stop(): void { if (this.timerId) { clearInterval(this.timerId); this.timerId = null; } }
    setDelay(d: number) { this.delay = d; }
    setMinCheck(m: number) { this.minCheck = m; }
}
export const myFilterInstance = MyTimer.getInstance();
export function onPageRender() { myFilterInstance.stop(); myFilterInstance.start(); }

5. Server‑side token expiration exposure

Instead of decoding the JWT on the client, the backend can expose the token's expiration timestamp directly, eliminating parsing incompatibilities between different JWT libraries.

public static Date getExpirationDate(String token) {
    if (StringUtil.isBlank(token)) return null;
    Claims claims = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token).getBody();
    return claims.getExpiration();
}

// When issuing tokens
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]);

6. Comparison of server‑side vs client‑side refresh

Server‑side : higher security, centralized management, consistent state across devices, reduced client complexity.

Client‑side : immediate token renewal, offline support, lower server load, flexible per‑device strategies.

Choosing the appropriate approach depends on the specific security and performance requirements of the system.

TypeScriptAuthenticationAxiosSpringBootrefreshtoken
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

login 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.