How to Implement Seamless Token Refresh in Spring Boot & Axios

This article explains how to implement seamless, invisible token refresh in a Spring Boot authentication server and a Vue/axios frontend, covering gateway interceptors, JWT expiration handling, Axios response interceptors, timer‑based proactive refresh, and complete code examples for both backend and client sides.

Architecture Digest
Architecture Digest
Architecture Digest
How to Implement Seamless Token Refresh in Spring Boot & Axios

1. Introduction

Token silent refresh is a mechanism that automatically updates an access token without user awareness, keeping the login session alive. Typically a short‑lived token is used for authentication while a longer‑lived refreshToken is used to obtain new short tokens.

Q1: Should the refresh be implemented on the server side or can the client handle it?

Q2: After a token expires, how can its expiration time be retrieved?

Q3: After obtaining a new token, how to resend the original request and return its result to the original caller?

The following sections provide a practical solution to these questions.

2. Client‑Side Implementation

2.1 Initial Version

The idea is that every request from the client is intercepted by a gateway. The gateway checks whether the token is invalid (expired) and returns a specific status code to the client.

If the token is expired, return a custom status (e.g., 511) indicating that a refresh is needed.

If the token is still valid, let the request pass through.

2.1.1 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();
        // OPTIONS request passes directly
        if (method.matches(HttpMethod.OPTIONS.name())) {
            return chain.filter(exchange);
        }
        // Login request passes directly
        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) {
            // Check if token is expired
            if (!JWTHelper.isOutDate(token)) {
                return chain.filter(exchange);
            } else {
                if (!SecurityAccessConstant.REQUEST_REFRESH.equals(uri)) {
                    // Not a refresh request, return 511
                    return ResponseUtils.out(exchange, ResultData.fail(ResultCodeEnum.NEED_TO_REFRESH_TOKEN.getCode(), ResultCodeEnum.NEED_TO_REFRESH_TOKEN.getMessage()));
                }
                // Refresh token also expired, return 401
                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.1.1.1 Solving Q2 (Token Expiration Check)

When parsing the token throws an exception, catch JwtException and treat the token as invalid/expired.

public static boolean isOutDate(String token) {
    try {
        Jws<Claims> claimsJws = Jwts.parser().setSigningKey(tokenSignKey).parseClaimsJws(token);
        Date expirationDate = claimsJws.getBody().getExpiration();
        return expirationDate.before(new Date());
    } catch (JwtException e) {
        // JWT token invalid or corrupted
        return true;
    }
}

2.1.2 Axios Interceptor

The response interceptor examines the status code. If it is 401, the user is logged out. If it is 511, the stored refreshToken is used to request a new token and retry the original request.

service.interceptors.response.use(
    response => {
        console.log(response);
        return 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 = refresh();
                    if (data !== null) {
                        data.then(value => {
                            if (value !== '') {
                                console.log('刷新 token 成功', value);
                                window.sessionStorage.setItem('token', value);
                                error.config.headers['Authorization'] = 'Bearer ' + value;
                                return service(error.config);
                            }
                        }).catch(err => console.error(err));
                    }
                } catch (err) {
                    console.log('请求刷新 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);
    }
);

2.1.3 Refresh Token Method

The refresh function sends a raw Axios request (not the wrapped request method) to avoid interceptor loops.

/**
 * Refresh token
 * Success returns new token, failure returns empty string
 */
export async function refresh(): Promise<string> {
    const refreshToken = window.sessionStorage.getItem('refreshToken');
    console.log('in >>>', 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}` }
        });
        if (response.data) {
            return response.data.data;
        } else {
            return '';
        }
    } catch (error) {
        console.log(error);
        return '';
    }
}

2.1.4 Console Output Analysis

Normal execution shows a 200 response with token ending in hizFIGg and refresh token ending in suvm‑EgQ. When the token expires, the gateway returns 511, triggering the refresh flow. The article includes screenshots illustrating both normal and error cases.

In the error scenario the client receives Uncaught error status 511, indicating that the original request has already finished and the refresh must be performed before retrying.

2.2 Improved Version

Instead of reacting after a failure, a timer checks the token’s remaining lifetime periodically. If the remaining time is below a threshold, the token is refreshed proactively.

2.2.1 Timer Class

import { refresh } from '@/api/system/auth/index';
import { jwtDecode } from 'jwt-decode';

export class MyTimer {
    private timerId: any | null = null;
    private delay: number = 30000; // default 30 s
    private minCheck: number = 60000; // default 1 min

    start(delay: number, minCheck: number): void {
        this.timerId = setInterval(async () => {
            const currentToken = window.sessionStorage.getItem('token');
            if (currentToken) {
                const tokenExpireStr = window.sessionStorage.getItem('tokenExpire') as string;
                const expirationTime = parseInt(tokenExpireStr, 10);
                const timeRemaining = expirationTime - Date.now();
                if (timeRemaining <= minCheck) {
                    try { await refresh(); } catch (e) { console.error('刷新失败:', e); }
                }
            } else {
                try { await refresh(); } catch (e) { console.error('刷新失败:', e); }
            }
        }, delay);
    }
    stop(): void {
        if (this.timerId !== null) {
            clearInterval(this.timerId);
            this.timerId = null;
        }
    }
    setDelay(delay: number): void { this.delay = delay; }
    setMinCheck(minCheck: number): void { this.minCheck = minCheck; }
}

export const myFilterInstance = MyTimer.getInstance();
export function onPageRender() {
    myFilterInstance.stop();
    myFilterInstance.start();
}

2.2.2 Login Click Handler

After a successful login, the timer is started to monitor token expiration.

import { MyTimer } from '@/utils/tokenMonitor';

const submit = () => {
    if (validate()) {
        login(formData).then((data: UserInfoRes) => {
            if (data) {
                const authStore = useAuthStore();
                authStore.setToken(data.token);
                window.sessionStorage.setItem('token', data.token);
                window.sessionStorage.setItem('refreshToken', data.refreshToken);
                authStore.setIsAuthenticated(true);
                const clock = new MyTimer();
                clock.start(1000 * 30, 1000 * 30);
                init({ message: 'logged in success', color: 'success' });
                push({ name: 'dashboard' });
            }
        }).catch(() => {
            init({ message: 'logged in fail, please check carefully!', color: '#FF0000' });
        });
    } else {
        Message.error('error submit!!');
        return false;
    }
};

2.2.3 Page Render Hook

Each page registers a load listener to restart the timer when the page is refreshed.

import { onPageRender } from '@/utils/tokenMonitor';
window.addEventListener('load', () => {
    onPageRender();
});

3. Server‑Side Implementation

The gateway intercepts requests, checks token validity, and if expired uses refreshToken to obtain a new token via WebClient. The new token is injected into the request headers before forwarding.

// Send request to auth server to get new token
Mono<ResultData> newTokenMono = WebClient.create().get()
    .uri(buildUri(SecurityAccessConstant.WEB_REQUEST_TO_AUTH_URL + SecurityAccessConstant.REQUEST_REFRESH,
        new String[]{"refreshToken", token}))
    .retrieve()
    .bodyToMono(ResultData.class);

AtomicBoolean isPass = new AtomicBoolean(false);
newTokenMono.subscribe(resultData -> {
    if (resultData.getCode() == "200") {
        exchange.getRequest().getHeaders().set(SecurityAccessConstant.HEADER_NAME_TOKEN,
            SecurityAccessConstant.TOKEN_PREFIX + resultData.getData());
        isPass.set(true);
    }
}).dispose();

if (isPass.get()) {
    return chain.filter(exchange.mutate().request().build());
}

4. Choosing Server vs. Client Refresh

Server‑side refresh offers better security and centralized management, while client‑side refresh provides real‑time responsiveness and reduces server load. The choice depends on the specific requirements of the application.

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.

BackendaxiosJWTtoken refresh
Architecture Digest
Written by

Architecture Digest

Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.

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.