React前端JWT刷新accessToken返回Promise问题修复
问题根因
你拿到Promise而非实际token字符串,核心是调用异步的refresh()函数时没有加await等待执行结果,直接把未决议的Promise对象存到了sessionStorage里。除此之外你的代码还有几个隐藏问题:
- 文件初始化时一次性读取storage里的token,后续token更新后不会同步读取最新值
- 刷新拿到新token后没有更新axios实例的请求头,重试请求还是带旧token
- 没有并发锁,多个请求同时过期时会重复触发token刷新
- POST请求传参用了
body字段,axios识别不到,后端收不到请求参数 - 没有做网络错误的兜底判断,断网场景下读error.response.status会直接报错
修复步骤
1. 修正前端axios封装逻辑
替换frontend/src/api/axios.js的全部代码,解决上述所有问题:
import axios from "axios"; const BASE_URL = "http://localhost:5000"; export default axios.create({ baseURL: BASE_URL, headers: { "Content-Type": "application/json" }, }); const client = axios.create({ baseURL: BASE_URL }); // 刷新token的并发锁,避免多个请求同时过期时重复调用刷新接口 let isRefreshing = false; let failedQueue = []; const processQueue = (error, token = null) => { failedQueue.forEach(prom => { if (error) { prom.reject(error); } else { prom.resolve(token); } }) failedQueue = []; } export const axiosAuth = async ({ ...options }) => { // 每次请求实时读取storage里的最新token,不要在文件初始化时一次性读取 const accessToken = sessionStorage.getItem("accessToken"); client.defaults.headers.common.Authorization = `Bearer ${accessToken}`; const onSuccess = (response) => response; const onError = async (error) => { const originalRequest = error.config; // 先判断响应存在,避免网络错误时读取status报错 if (error.response?.status === 403 && !originalRequest._retry) { if (isRefreshing) { // 刷新过程中到来的请求先加入等待队列 return new Promise(function(resolve, reject) { failedQueue.push({resolve, reject}) }).then(token => { originalRequest.headers['Authorization'] = 'Bearer ' + token; return client(originalRequest); }).catch(err => { return Promise.reject(err); }) } originalRequest._retry = true; isRefreshing = true; const refreshToken = localStorage.getItem("refreshToken"); try { // 加await等待刷新请求返回,拿到实际的token字符串 const { data } = await axios.post(`${BASE_URL}/auth/refresh/token`, { token: refreshToken, }); const newAccessToken = data.accessToken; // 新token存入storage sessionStorage.setItem("accessToken", newAccessToken); // 更新axios默认请求头 client.defaults.headers.common.Authorization = `Bearer ${newAccessToken}`; // 执行队列里等待的请求 processQueue(null, newAccessToken); // 重试最开始触发403的原请求 return client(originalRequest); } catch (refreshError) { processQueue(refreshError, null); // 刷新失败(一般是refreshToken过期/无效),清除本地登录态跳登录页 sessionStorage.removeItem("accessToken"); localStorage.removeItem("refreshToken"); window.location.href = "/login"; return Promise.reject(refreshError); } finally { isRefreshing = false; } } return Promise.reject(error); }; return client(options).then(onSuccess).catch(onError); };
2. 修正业务组件的请求参数
frontend/src/modals/Tweet.modal.js里发POST请求时,axios的请求体字段是data不是body,原写法后端收不到推文内容,把调用部分修改为:
await axiosAuth({ url: "/posts/create", method: "POST", data: { textfield }, // 把原来的body字段改为data });
3. (可选优化)后端状态码规范调整
当前鉴权中间件不管是token过期、还是token伪造无效都返回403,建议调整backend/middlewares/authenticateToken.js,把token过期场景单独返回401,和无权限的403做区分,方便前端精准判断是否需要刷新token:
const jwt = require("jsonwebtoken"); require("dotenv").config(); module.exports = function authenticateToken(req, res, next) { const authHeader = req.headers["authorization"]; const token = authHeader && authHeader.split(" ")[1]; if (token == null) return res.sendStatus(401); jwt.verify(token, process.env.ACCESS_TOKEN_SECRET, (err, user) => { if (err) { // token过期返回401,其他无效/篡改场景返回403 if (err.name === "TokenExpiredError") return res.sendStatus(401); return res.sendStatus(403); } req.user = user; next(); }); };
调整后把前端axios拦截里的状态码判断从error.response?.status === 403改成error.response?.status === 401即可,逻辑会更严谨。
内容的提问来源于stack exchange,提问作者user16949958
相关产品推荐
相关产品推荐

