React首次渲染获取CSRF Token后并发POST请求返回403如何解决
问题根因
你遇到的403问题本质是异步竞态:获取CSRF Token的GET请求是异步执行的,而业务组件的POST请求和这个GET请求在首次渲染时并行触发,POST请求发送时Token还没获取完成,自然不会携带X-CSRF-Token头。
解决方案
下面给三个可落地的方案,按需选择:
方案1:加加载状态阻塞子组件渲染(改造成本最低)
在FetchProvider中新增加载状态,只有CSRF Token获取完成后才渲染子组件,从根源上避免提前发请求。
import axios from "axios"; import { createContext, useEffect, useState } from "react"; const FetchContext = createContext(); const { Provider } = FetchContext; const FetchProvider = ({ children }) => { const [isCsrfLoaded, setIsCsrfLoaded] = useState(false) const publicAxios = axios.create({ baseURL: '/api' }) const authAxios = axios.create({ baseURL: '/api' }) authAxios.interceptors.response.use( response => response, error => Promise.reject(error) ); useEffect(() => { const getCsrfToken = async () => { try { const { data } = await publicAxios.get('/csrf-token'); publicAxios.defaults.headers['X-CSRF-Token'] = data.csrfToken; authAxios.defaults.headers['X-CSRF-Token'] = data.csrfToken; } catch (err) { console.log(err) } finally { setIsCsrfLoaded(true) } }; getCsrfToken(); }, [authAxios, publicAxios]); // 未加载完成时可以返回自定义加载占位符,避免白屏 if (!isCsrfLoaded) return <div>加载中...</div> return ( <Provider value={{ authAxios, publicAxios }}> {children} </Provider> ); }; export { FetchContext, FetchProvider }
这个方案不需要改任何业务组件代码,适合快速修复问题。
方案2:加请求拦截器动态补全Token(体验更优)
不需要阻塞渲染,给axios实例加请求拦截器,每次发请求前自动判断有没有Token,没有就先获取再发送请求,支持并行请求自动合并Token获取操作,不会重复发/csrf-token请求。
import axios from "axios"; import { createContext, useMemo } from "react"; const FetchContext = createContext(); const { Provider } = FetchContext; // 缓存Token获取的Promise,避免并行请求重复触发接口调用 let csrfTokenPromise = null const getCsrfToken = async (publicAxios) => { if (csrfTokenPromise) return csrfTokenPromise csrfTokenPromise = publicAxios.get('/csrf-token').then(res => res.data.csrfToken) try { return await csrfTokenPromise } finally { csrfTokenPromise = null } } const FetchProvider = ({ children }) => { const publicAxios = useMemo(() => axios.create({ baseURL: '/api' }), []) const authAxios = useMemo(() => { const instance = axios.create({ baseURL: '/api' }) // 原有响应拦截器保留 instance.interceptors.response.use( response => response, error => Promise.reject(error) ) // 新增请求拦截器,动态加CSRF头 instance.interceptors.request.use( async (config) => { // 跳过GET请求和csrf-token接口本身,避免死循环 if (config.method === 'get' || config.url === '/csrf-token') return config // 没有Token就先获取 if (!instance.defaults.headers['X-CSRF-Token']) { const token = await getCsrfToken(publicAxios) instance.defaults.headers['X-CSRF-Token'] = token publicAxios.defaults.headers['X-CSRF-Token'] = token } config.headers['X-CSRF-Token'] = instance.defaults.headers['X-CSRF-Token'] return config }, error => Promise.reject(error) ) return instance }, [publicAxios]) // publicAxios如果有POST请求需求,也可以加同样的请求拦截器 return ( <Provider value={{ authAxios, publicAxios }}> {children} </Provider> ); }; export { FetchContext, FetchProvider }
这个方案不需要加全局加载状态,用户无感,适合对首屏加载速度要求高的场景。
方案3:后端配合从Cookie读取Token(维护成本最低)
如果后端支持,可以调整CSRF校验逻辑:生成CSRF Token时直接写入HttpOnly=false的Cookie,前端不需要手动拿Token设置头,后端直接从Cookie中读取对应的CSRF Token校验即可,前端完全不需要处理Token相关逻辑,从根源上避免这个问题。
内容的提问来源于stack exchange,提问作者Manishkumar Adesara
相关产品推荐
相关产品推荐

