CSRF防护中,如何在不刷新页面的情况下安全更新CSRF Token?

啸垄 Dev 阅读 40

在做单页应用时遇到了CSRF Token过期问题。前端用axios拦截器在请求头带上token,但用户长时间登录后,后端会返回403要求更新token。我尝试在响应拦截器里检测403错误后,通过/refresh-csrf接口获取新token,但发现:


axios.interceptors.response.use(response => response, async err => {
  if (err.response.status === 403 && err.config.url !== '/refresh-csrf') {
    const newToken = await refreshToken();
    localStorage.setItem('csrfToken', newToken);
    err.config.headers['X-CSRF-Token'] = newToken;
    return axios(err.config);
  }
  return Promise.reject(err);
});

但这样处理后出现两个问题:1. 并发请求时新旧token互相覆盖 2. 重复刷新导致请求链断裂。比如用户同时提交两个表单,A请求先触发403更新token,B请求在A更新完成前也触发刷新,导致A用旧token重试失败。有没有更好的实现方式能保证线程安全且不阻塞其他请求?

我来解答 赞 10 收藏
二维码
手机扫码查看
2 条解答
慕容慧娇
这个问题主要是因为并发请求时,多个请求同时检测到403错误并尝试刷新token,导致token被覆盖或者重复刷新。解决的关键在于控制token刷新的流程,保证只有一个请求负责刷新token,其他请求等待新token后再继续。

我的建议是用一个全局的Promise来管理token刷新的状态,这样可以避免重复刷新和并发冲突。具体实现如下:

先定义一个变量用来存储刷新token的Promise,初始值为null:
let refreshingTokenPromise = null;

然后在axios拦截器里调整逻辑,像这样写:


axios.interceptors.response.use(response => response, async err => {
if (err.response.status === 403 && err.config.url !== '/refresh-csrf') {
// 如果已经有刷新token的操作在进行,则等待
if (!refreshingTokenPromise) {
refreshingTokenPromise = refreshToken().then(newToken => {
localStorage.setItem('csrfToken', newToken);
return newToken;
}).finally(() => {
// 刷新完成后清空Promise
refreshingTokenPromise = null;
});
}

// 等待刷新完成后再重试当前请求
const newToken = await refreshingTokenPromise;
err.config.headers['X-CSRF-Token'] = newToken;
return axios(err.config);
}
return Promise.reject(err);
});


核心思路是通过refreshingTokenPromise这个全局变量来协调所有请求的行为。如果已经有请求在刷新token了,其他请求就不用再触发刷新,而是直接等新token下来后重试自己的请求。这样就能避免并发刷新的问题。

另外,刷新token的接口/refresh-csrf一定要设计成幂等的,确保多次调用不会产生副作用。调试看看,应该能解决问题。如果还有问题,可以检查一下后端是否正确处理了token刷新的逻辑。
点赞
2026-02-19 09:04
❤永莲
❤永莲 Lv1
用一个全局锁解决并发问题,刷新 token 时阻塞后续请求直到更新完成。代码直接改:

let isRefreshing = false;
let refreshPromises = [];

axios.interceptors.response.use(response => response, async err => {
if (err.response.status === 403 && err.config.url !== '/refresh-csrf') {
if (!isRefreshing) {
isRefreshing = true;
const newToken = await axios.get('/refresh-csrf');
localStorage.setItem('csrfToken', newToken.data);
refreshPromises.forEach(cb => cb(newToken.data));
refreshPromises = [];
isRefreshing = false;
}

return new Promise((resolve, reject) => {
refreshPromises.push((token) => {
err.config.headers['X-CSRF-Token'] = token;
resolve(axios(err.config));
});
});
}
return Promise.reject(err);
});


这样保证同一时间只刷新一次,其他请求等刷新完成再发。
点赞 11
2026-01-30 08:01