Axios/Laravel并发竞态条件引发随机401未授权与CSRF问题
问题描述
我有一个基于axios驱动的SPA应用。最初发现CSRF令牌会随机丢失,经排查是并发请求触发的竞态条件(race condition)。之后我将会话存储机制改为数据库,为路由添加原子阻塞,并禁用了非“action”路由的CSRF校验。
但现在出现了新问题:随机触发401未授权错误。我参考网上资料编写了会话重试拦截器,代码如下:
let retries = []; const sessionHandler = async error => { if (error.response && (error.response.status === 419 || error.response.status === 401)) { const uri = error.response.config.url; const data = error.response.config.data; // I use nothing from this endpoint, its simply to allow // axios to get the CSRF token const endpoint = '/session/user-config'; // Push to retries retries.push({uri: uri, data: data}); // After exhausting retries, force refresh if (retries.filter(attempt => attempt.uri === uri).length > 3 || retries.filter(attempt => attempt.uri === endpoint).length > 3) { window.location.reload(); } // Attempt to obtain new token return await axios.get(endpoint).then(async () => { // Attempt to fulfill request return await axios.post(uri, data).then(response => response); }); } return Promise.reject(error); }; window.axios = require('axios'); window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; axios.interceptors.response.use(response => { // Remove any attempts after a successful request retries.filter(attempt => attempt.uri = response.config.url).forEach(attempt => { retries.slice(retries.indexOf(attempt), 1); }); return response; }, sessionHandler);
该拦截器成功率约70%,剩余30%会循环重试后刷新页面并跳转至登录页。问题有时会在登录后2分钟内出现,我已将会话生命周期设为1年排查,但仅在生产环境复现,本地及测试环境均无此问题,且未找到配置差异。
此外,我发现每个axios请求都会更新本地会话cookie,怀疑存在读写冲突,但不确定是否能为cookie添加阻塞机制。观察请求可见,CSRF令牌与laravel_session cookie均已发送,但仍随机返回419或401,重新发送请求则可正常执行。
调试时还发现,触发401错误时Axios会抛出严格模式相关异常:
TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them
请问针对Laravel或axios,还有哪些措施可以彻底解决该问题?
解决方案
一、修复重试拦截器的逻辑缺陷
你的拦截器存在几个关键逻辑问题,先修正这些基础问题:
- 重试计数逻辑错误:原代码统计所有历史同URI的重试次数,而非当前请求的重试次数。改用为每个请求单独计数,避免重复统计。
- 数组操作错误:
slice不会修改原数组,需替换为splice;同时原代码中attempt.uri = response.config.url是赋值操作而非判断,会导致所有重试项URI被覆盖,需改为===判断。 - 并发重试冲突:多个请求同时触发419/401时,会同时请求令牌接口造成新竞态,需添加全局锁控制同一时间仅一个请求刷新令牌。
修正后的拦截器示例:
let isRefreshing = false; let failedQueue = []; const processQueue = (error, token = null) => { failedQueue.forEach(prom => { error ? prom.reject(error) : prom.resolve(token); }); failedQueue = []; }; const sessionHandler = async error => { const { config, response } = error; if (response && (response.status === 419 || response.status === 401)) { // 等待正在进行的令牌刷新 if (isRefreshing) { return new Promise((resolve, reject) => { failedQueue.push({ resolve, reject }); }).then(token => { config.headers['X-CSRF-TOKEN'] = token; return axios(config); }).catch(err => Promise.reject(err)); } isRefreshing = true; // 限制当前请求的重试次数 config._retryCount = config._retryCount || 0; if (config._retryCount >= 3) { isRefreshing = false; window.location.reload(); return Promise.reject(error); } config._retryCount += 1; try { // 获取新令牌,Laravel会自动更新XSRF-TOKEN cookie await axios.get('/session/user-config'); // 从cookie解析新CSRF令牌 const newCsrfToken = document.cookie.split('; ') .find(row => row.startsWith('XSRF-TOKEN='))?.split('=')[1]; const decodedToken = decodeURIComponent(newCsrfToken); axios.defaults.headers.common['X-CSRF-TOKEN'] = decodedToken; config.headers['X-CSRF-TOKEN'] = decodedToken; // 处理队列中等待的请求 processQueue(null, decodedToken); isRefreshing = false; // 重试当前请求 return axios(config); } catch (err) { processQueue(err, null); isRefreshing = false; window.location.reload(); return Promise.reject(err); } } return Promise.reject(error); }; window.axios = require('axios'); window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; // 初始化时从cookie读取CSRF令牌 const initialCsrfToken = document.cookie.split('; ') .find(row => row.startsWith('XSRF-TOKEN='))?.split('=')[1]; if (initialCsrfToken) { axios.defaults.headers.common['X-CSRF-TOKEN'] = decodeURIComponent(initialCsrfToken); } axios.interceptors.response.use(response => response, sessionHandler);
二、Laravel后端优化
- 调整会话锁定策略:数据库会话默认
lazy锁定仅在写入时生效,改为blocking锁定避免并发覆盖会话数据。在config/session.php中设置:'lock_timeout' => 10, // 锁定超时时间(秒) 'locking' => true, - 优化CSRF令牌逻辑:确保
/session/user-config接口不会重置会话,仅返回必要数据;可添加中间件在AJAX请求响应头返回最新CSRF令牌,前端直接从响应头获取,无需解析cookie。 - 控制会话更新频率:排查代码中是否存在每个请求都调用
$request->session()->regenerate()的情况,仅在登录、修改密码等敏感操作时重新生成会话,避免laravel_sessioncookie频繁变化引发冲突。 - 检查生产环境会话配置:确认
SESSION_DOMAIN、SESSION_SECURE、SESSION_SAME_SITE配置正确,生产环境建议设置:// config/session.php 'same_site' => 'lax', 'secure' => env('SESSION_SECURE', true), // HTTPS环境下开启
三、前端请求优化
- 请求队列化处理:对非紧急并发请求做队列控制,限制同一时间发送的请求数量,减少会话冲突概率。
- 修复严格模式异常:排查拦截器或第三方axios插件,移除
arguments.callee等严格模式禁止的代码;升级axios到最新稳定版,避免框架自身的兼容问题。
四、生产环境专属排查
- 分析Laravel日志:查看
storage/logs/laravel.log中401/419错误的详细原因,比如会话锁定超时、CSRF令牌不匹配等,日志会提供关键线索。 - 排查缓存与CDN:确保AJAX请求路由未被CDN或缓存服务缓存,避免返回过期的会话或CSRF数据。
- 隐私模式测试:用浏览器隐私模式访问生产环境,排除第三方扩展或浏览器缓存导致的cookie异常。
内容的提问来源于stack exchange,提问作者Jaquarh
相关产品推荐
相关产品推荐

