单页应用中CSRF Token自动刷新导致表单提交失败怎么办?

Mc.佳宁 阅读 26

我在开发Vue应用时遇到了CSRF防护问题,前端用了axios拦截器在每次请求带上CSRF token,但后端要求token每小时必须刷新。我尝试在axios的响应拦截器里检测403错误后自动调用刷新接口,但发现表单提交时隐藏字段的token没及时更新:


<template>
  <form @submit="submitForm">
    <input type="hidden" name="_token" :value="csrfToken">
    <!-- 表单其他字段 -->
  </form>
</template>

<script>
export default {
  data() {
    return {
      csrfToken: Cookies.get('XSRF-TOKEN')
    }
  },
  created() {
    axios.interceptors.response.use(
      response => response,
      error => {
        if (error.response.status === 403) {
          // 这里调用刷新token的API后
          this.csrfToken = newToken;
          return Promise.reject(error);
        }
        return Promise.reject(error);
      }
    )
  }
}
</script>

问题是当刷新token后,虽然组件数据更新了,但当前表单提交的请求还是用旧token被拦截。试过用Vue.set强制响应式更新,但刷新期间的新请求又会触发二次token刷新导致嵌套请求。有什么更好的同步策略能确保表单字段和请求头同时更新?

我来解答 赞 5 收藏
二维码
手机扫码查看
2 条解答
东方付楠
这问题我遇到过,核心在于你不能只依赖组件实例里的 data 来同步 token 状态。你现在的问题是拦截器里刷新了 token,但正在重发的请求还是拿着旧 token 发出去了,而且表单里的 hidden field 也没跟着变。

根本解法是:把 CSRF Token 的管理从 Vue 组件里抽出来,交给全局统一的后端处理 + 请求层代理刷新机制。

首先确保你的后端在每次返回新 token 时,通过 Set-Cookie 自动下发 XSRF-TOKEN,而不是让前端手动塞到 Cookie 或状态里。这样浏览器会在下次请求自动带上新 token,axios 默认会读 X-XSRF-TOKEN 头,配合 withCredentials: true 就能保证 cookie 同步。

然后关键点来了:不要在响应拦截器里直接 reject,而是要拦截失败请求、等 token 刷新完成后再重试原请求。你现在 return Promise.reject(error) 相当于直接放弃这次请求了,自然还会用旧 token 提交一次。

改法如下:

let isRefreshing = false
let requestQueue = []

axios.interceptors.response.use(
response => response,
error => {
const config = error.config
if (error.response.status === 403 && !config._retry) {
if (isRefreshing) {
// 如果已经在刷新,把请求缓存起来
return new Promise(resolve => {
requestQueue.push(() => {
resolve(axios(config))
})
})
}

config._retry = true
isRefreshing = true

// 先发一个 refresh 请求(这个接口必须允许无 token 或用 session 验证)
return axios.get('/auth/refresh-csrf')
.then(() => {
// 刷新成功后,重新设置 token(其实靠 set-cookie 自动更新就行)
requestQueue.forEach(cb => cb())
requestQueue = []
// 重发当前请求
return axios(config)
})
.catch(err => {
// 刷新失败,跳转登录
window.location.href = '/login'
return Promise.reject(err)
})
.finally(() => {
isRefreshing = false
})
}
return Promise.reject(error)
}
)


接着你在页面上不用管 hidden input 的值变化,只要确保初始加载时从 cookie 取一次 token 即可。因为所有后续请求都走 axios 拦截器+自动 cookie 更新,form 提交如果是通过 fetch 或 axios 发的,也不需要手动读 hidden field。

如果你非要用表单 submit 触发传统 POST(比如文件上传没用 Ajax),那就在提交前从最新 cookie 取 token 赋值:

submitForm(e) {
const token = Cookies.get('XSRF-TOKEN')
e.target._token.value = token
}


但更推荐统一走 Axios 发请求,避免混用两种模式导致状态不同步。

总结一下:别让前端“主动”更新字段,而是靠后端 set-cookie + 请求队列重试机制来保证所有请求都能拿到最新的 token。这才是正解。
点赞 10
2026-02-09 09:16
公孙子源
你这个情况的核心问题是token更新的时候,表单字段和axios拦截器用的token不同步。

我之前也遇到过类似问题,解决方案是统一token的更新逻辑,通过一个token刷新管理器来保证同步更新。直接上代码:

class TokenManager {
static token = Cookies.get('XSRF-TOKEN');
static refreshing = false;
static refreshQueue = [];

static setToken(token) {
this.token = token;
Cookies.set('XSRF-TOKEN', token);
this.flushQueue();
}

static flushQueue() {
this.refreshing = false;
this.refreshQueue.forEach(cb => cb(this.token));
this.refreshQueue = [];
}

static async refreshToken() {
if (this.refreshing) {
return new Promise(resolve => this.refreshQueue.push(resolve));
}

this.refreshing = true;
const res = await axios.get('/refresh-csrf');
this.setToken(res.token);
return this.token;
}
}

// 表单提交时
async submitForm() {
const token = TokenManager.token;
const res = await axios.post('/submit', {
_token: token,
// 其他数据
}, {
headers: { 'XSRF-TOKEN': token }
});
}

// 拦截器里
axios.interceptors.response.use(
response => response,
async error => {
if (error.response.status === 403 && !TokenManager.refreshing) {
await TokenManager.refreshToken();
error.config.headers['XSRF-TOKEN'] = TokenManager.token;
return axios(error.config);
}
return Promise.reject(error);
}
)


这样处理之后:
token更新走同一个队列,避免并发刷新
表单提交时用的token和请求头保持一致
所有token更新走同一个出口,不会出现不同步

性能上来说,这个方案避免了重复刷新token,减少了不必要的网络请求。关键点在于队列管理,保证token更新时所有等待中的请求都能拿到最新token。
点赞 5
2026-02-05 20:37