React异步函数返回undefined Token写入localStorage异常排查
问题描述
编写了一个会在每个页面调用RefreshToken逻辑的React Hook,代码如下:
const refresh = async (refToken) => { try { if (refToken && !functionalities.isTokenExpired(refToken)) { const ntoken = await refreshToken().then((obj) => { return obj.token; }); window.localStorage.setItem("token", ntoken); } else { window.localStorage.clear(); toast.error("O seu acesso expirou! Faça novamente o login no sistema."); return false; } } catch (error) { toast.error("Não foi possível atualizar o token do usuário"); } }; async function useRefreshToken() { const refToken = localStorage.getItem("refreshToken") || ""; if (refToken && !functionalities.isTokenExpired(refToken)) { refresh(refToken).then(() => {}); // setInterval(() => { // refresh(refToken).then(); // }, 3564000); } } // 0,99hrs 3564000 - 10min 600000 -- export default useRefreshToken;
代码执行时,总会将localStorage中的token设置为undefined,但服务端实际已经返回了正确的更新后Token,接口返回内容如下:
{"msg":"AUTHORIZATION SUCCESSFUL","token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyVUlEIjoiZmZaTUZTTDF4UGNXdFQ5MDJkd2Z6RFB1WElEMyIsInBvc2l0aW9uIjoibWFuYWdlciIsImNvbXBhbnlJRCI6Ik14Z1ZleEhlSGZzN29ndTJaMm10IiwiaWF0IjoxNjU3MTQyMDU5LCJleHAiOjE2NTcxNDkyNTl9.l9OHl-84S5LrwF1r0fqQl2zuHhS-hrWaFj8QjrxDgQA"}
页面每次加载时至少会调用3次refreshToken方法,无法定位未等待API响应完成就更新Token的原因。
其中封装的refreshToken() API请求方法代码如下:
export const refreshToken = async () => { const headers = getDefaultHeaders(true); const refToken = localStorage.getItem("refreshToken"); const body = { refreshToken: refToken, }; return await axios.post(`${url}/auth/refreshToken`, body, headers); };
问题根因
- 核心错误:Axios响应结构取值错误
Axios的post方法返回的是完整的响应对象,服务端返回的业务数据统一挂载在响应对象的data属性上,代码直接从响应对象顶层取token字段,拿到的必然是undefined,这就是localStorage存入undefined的直接原因。实际响应结构如下:{ data: { msg: "AUTHORIZATION SUCCESSFUL", token: "真实token值" }, // 服务端返回的业务内容存在这一层 status: 200, statusText: "OK", headers: {}, config: {}, request: {} } - Hook写法不符合React执行规则,导致重复触发请求
编写的useRefreshToken没有用useEffect包裹副作用逻辑,只要组件重渲染就会执行一次,页面加载阶段组件多次挂载、重渲染,自然会触发3次甚至更多次refresh请求,和“没等接口返回”无关,是取值路径错误导致的异常。 - 无并发锁控制,存在竞态隐患
多个组件同时调用该Hook时,会同时发起多个刷新请求,没有任何防重复处理,不仅浪费接口资源,还可能出现先返回的旧token覆盖后返回的新token的问题。
修复方案
- 第一步:修正接口返回值的取值路径,直接返回业务层数据
export const refreshToken = async () => { const headers = getDefaultHeaders(true); const refToken = localStorage.getItem("refreshToken"); const body = { refreshToken: refToken, }; const res = await axios.post(`${url}/auth/refreshToken`, body, headers); // 直接返回服务端业务数据,不需要把整个Axios响应对象往外抛 return res.data; }; - 第二步:改造Hook符合React规范,加防重锁避免重复请求
import { useEffect, useRef } from 'react'; // 模块级请求锁,跨组件也能防重复刷新 let isRefreshing = false; const refresh = async (refToken) => { if (isRefreshing) return; try { if (refToken && !functionalities.isTokenExpired(refToken)) { isRefreshing = true; const resp = await refreshToken(); window.localStorage.setItem("token", resp.token); } else { window.localStorage.clear(); toast.error("O seu acesso expirou! Faça novamente o login no sistema."); return false; } } catch (error) { toast.error("Não foi possível atualizar o token do usuário"); } finally { // 无论请求成功失败都释放锁 isRefreshing = false; } }; function useRefreshToken() { const hasInit = useRef(false); useEffect(() => { // 防止Hook多次执行重复触发逻辑 if (hasInit.current) return; hasInit.current = true; const refToken = localStorage.getItem("refreshToken") || ""; if (refToken && !functionalities.isTokenExpired(refToken)) { refresh(refToken); // 开启定时刷新的话一定要加清理函数,避免内存泄漏 // const timer = setInterval(() => { // // 每次取最新的refreshToken,不要用闭包缓存的旧值 // const latestRefToken = localStorage.getItem("refreshToken") || ""; // refresh(latestRefToken); // }, 3564000); // return () => clearInterval(timer); } }, []); } export default useRefreshToken;
内容的提问来源于stack exchange,提问作者Daniel Alexandre Oliveira Mode
相关产品推荐
相关产品推荐

