React路由跳转后sessionStorage未立即可用致JWT鉴权失败问题
核心问题出在GetAPI模块的导出逻辑:
你在文件末尾写的export default GetAPI()会在模块首次被import加载时立即执行,导出的是那一刻生成的固定axios实例——这个时机通常是应用初始化、用户还未登录的阶段,sessionStorage里还没有存储product-authkey,所以这个实例永远不带鉴权请求头。哪怕后续登录成功往sessionStorage写入了JWT,已经生成的静态实例也不会自动更新请求头配置。
你遇到的「手动刷新页面后请求正常」的现象,本质是刷新触发了全量JS模块重新加载,此时GetAPI()重新执行,能读到已经存在于sessionStorage的token,才生成了带正确鉴权头的实例。
把GetAPI()调用挪到个人主页组件的useEffect里能跑通,是因为useEffect的执行时机在登录跳转完成、sessionStorage写入token之后,调用时能拿到最新的带鉴权头的实例,但这只是临时规避问题,不是最优实践。
用axios请求拦截器动态注入鉴权头,不需要每次手动创建新实例,一劳永逸解决token动态更新问题:
- 改造原来的GetAPI文件,移除提前创建固定实例、提前执行导出的逻辑,改为创建基础实例+请求拦截器,每次发请求前实时读取最新的token:
import axios from 'axios' // 替换为你实际的Strapi服务地址 const localurl = 'http://localhost:1337/api' // 创建基础axios实例,不提前写死Authorization头 const http = axios.create({ baseURL: localurl, headers: { 'Content-type': 'application/json' } }) // 请求拦截器:每次发起请求前,实时从sessionStorage取最新token注入请求头 http.interceptors.request.use((config) => { const authToken = sessionStorage.getItem('product-authkey') if (authToken) { config.headers.Authorization = `Bearer ${authToken}` } return config }) // 可选:加响应拦截器统一处理通用逻辑,比如401未授权自动跳登录 http.interceptors.response.use( (response) => response, (error) => { if (error.response?.status === 401) { sessionStorage.removeItem('product-authkey') // 这里可以加你自己的跳转登录逻辑,清除登录态 } return Promise.reject(error) } ) export default http
- 改造完成后,你原来的
handleLogin登录逻辑不需要调整——登录接口本身不需要token,拦截器判断没有token时不会追加鉴权头,完全适配公开接口的访问需求。后续所有组件直接import这个http实例发请求即可,不需要每次手动调用GetAPI创建实例,不管token什么时候写入sessionStorage,所有请求都会自动带上最新的合法鉴权头。
把GetAPI()放到useEffect里调用虽然能暂时解决当前页面的初始化请求问题,但存在明显缺陷:
- 每个需要发请求的组件都要重复调用
GetAPI()创建新的axios实例,产生不必要的内存开销 - 如果组件内存在useEffect之外触发的请求(比如按钮点击提交、用户操作触发的数据更新请求),还是可能拿到不带token的实例,导致请求失败
- 没法统一管控全局请求逻辑,比如通用错误提示、loading状态处理、请求签名这类公共能力不好落地
如果你暂时不想重构为拦截器模式,至少要把原来的导出逻辑从export default GetAPI()改为export default GetAPI,也就是导出GetAPI函数本身,而不是函数执行后的固定结果。每次发请求前再调用GetAPI()获取当前状态匹配的实例即可,但这种方案维护性差,长期项目还是建议用拦截器方案。
内容的提问来源于stack exchange,提问作者Jelle Wouters

