You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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后端优化

  1. 调整会话锁定策略:数据库会话默认lazy锁定仅在写入时生效,改为blocking锁定避免并发覆盖会话数据。在config/session.php中设置:
    'lock_timeout' => 10, // 锁定超时时间(秒)
    'locking' => true,
    
  2. 优化CSRF令牌逻辑:确保/session/user-config接口不会重置会话,仅返回必要数据;可添加中间件在AJAX请求响应头返回最新CSRF令牌,前端直接从响应头获取,无需解析cookie。
  3. 控制会话更新频率:排查代码中是否存在每个请求都调用$request->session()->regenerate()的情况,仅在登录、修改密码等敏感操作时重新生成会话,避免laravel_session cookie频繁变化引发冲突。
  4. 检查生产环境会话配置:确认SESSION_DOMAIN、SESSION_SECURE、SESSION_SAME_SITE配置正确,生产环境建议设置:
    // config/session.php
    'same_site' => 'lax',
    'secure' => env('SESSION_SECURE', true), // HTTPS环境下开启
    

三、前端请求优化

  1. 请求队列化处理:对非紧急并发请求做队列控制,限制同一时间发送的请求数量,减少会话冲突概率。
  2. 修复严格模式异常:排查拦截器或第三方axios插件,移除arguments.callee等严格模式禁止的代码;升级axios到最新稳定版,避免框架自身的兼容问题。

四、生产环境专属排查

  1. 分析Laravel日志:查看storage/logs/laravel.log中401/419错误的详细原因,比如会话锁定超时、CSRF令牌不匹配等,日志会提供关键线索。
  2. 排查缓存与CDN:确保AJAX请求路由未被CDN或缓存服务缓存,避免返回过期的会话或CSRF数据。
  3. 隐私模式测试:用浏览器隐私模式访问生产环境,排除第三方扩展或浏览器缓存导致的cookie异常。

内容的提问来源于stack exchange,提问作者Jaquarh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 20:33:22