JWT 登录不是存个 token:React 鉴权真正要闭合的是一条数据流

JWT 登录不是存个 token:React 鉴权真正要闭合的是一条数据流

很多登录 Demo 在拿到 token 后就结束了,但工程里的问题才刚开始:谁负责刷新恢复,谁负责给请求加头,路由守卫能防住什么,token 过期后又由谁收尾?

本文基于一个 React、Zustand、Axios、React Router 和 Vite Mock 组成的项目,给出一个判断:JWT 登录的关键不是某个 API,而是签发、状态、传输、页面访问与服务端校验能否形成闭环。 文中的代码来自静态分析,运行未验证;演示凭据与签名密钥不展示。

摘要:从一次登录请求出发,还原 token 在 mock 服务、Zustand、localStorage、Axios 拦截器和路由守卫之间的流动;同时划清前端放行与服务端验签的边界,并提供生产化自检表。

先把五个角色摆对位置

项目中的职责可以先压缩成一张表:

角色 做什么 不应该被误认为
Login 收集凭据、调用接口、写入状态 鉴权中心
Zustand 跨组件共享 token 与用户 持久化数据库
localStorage 刷新后恢复登录态 安全保险箱
Axios 自动附加 Bearer token 权限判断器
RequireAuth 控制页面是否渲染 服务端安全边界
API 验签并继续做授权 单纯的数据返回层

只要把其中两层混为一谈,就容易出现"页面进不去但接口能调"或"本地有 token 就以为已经安全"的错觉。

一次登录之后,token 到底去了哪里

登录页提交表单后,请求 POST /api/login。模拟接口校验演示凭据,用 jwt.sign 签发 JWT,载荷包含 userrole,有效期是 86400 秒,也就是 24 小时。

js 复制代码
const token = jwt.sign(
  { user: body.username, role: 'admin' },
  signingSecret,
  { expiresIn: 86400 }
);

登录成功后,页面调用 Zustand 的 setAuth。这个 action 同时更新内存状态和 localStorage:前者让导航栏、路由守卫立即响应;后者让页面刷新后还能恢复身份。

js 复制代码
setAuth: ({ token, user }) => {
  localStorage.setItem('token', token);
  localStorage.setItem('user', JSON.stringify(user));
  set({ token, user });
}

退出时也必须反向清理两层。如果只清 Zustand,刷新后会从 localStorage 再次读到 token;只清 localStorage,当前页面又可能继续保持已登录 UI。

这里还要划一条安全边界:JWT 的签名不等于加密,Payload 可以解码,不应放密码或秘密;localStorage 也能被同源脚本读取,一旦发生 XSS,token 泄露影响会被放大。存储方案必须结合实际威胁模型决定。

Bearer token 不该散落在每个请求里

项目创建了统一 Axios 实例,baseURL/api,超时为 5000 毫秒。请求拦截器在请求发出前读取 token,并补上标准 Bearer 头;响应拦截器则直接解包 res.data

js 复制代码
instance.interceptors.request.use(config => {
  const authState = readAuthState();
  if (authState) {
    config.headers.Authorization = buildBearerHeader(authState);
  }
  return config;
});

instance.interceptors.response.use(res => res.data);

于是业务 API 可以保持很薄:

js 复制代码
export const login = data => instance.post('/login', data);
export const getRepo = () => instance.get('/repo');

集中处理的价值不只是少写几行代码,更重要的是协议一致:不会某个接口写 Token,另一个写 Bearer,还有一个忘记带头。反过来,如果项目混用了另一个 Axios 实例,这套拦截器就不会生效,排错时要先确认 import 来源。

RequireAuth 拦的是页面,不是攻击者

受保护的 /pay 路由被 RequireAuth 包裹。组件只从 Zustand 读取 token:没有就跳到 /login,有就渲染子组件。

jsx 复制代码
function RequireAuth({ children }) {
  const isAuthenticated = useAuthStore(state => Boolean(state.token));
  if (!isAuthenticated) return <Navigate to="/login" replace />;
  return children;
}

它解决的是用户体验:未登录用户不应先看到支付页,再等待接口报错。replace 还避免登录前的受保护页面留在历史记录里。

但字符串非空不能证明 token 有效。它可能已经过期、被伪造或被撤销。任何人都可以绕过 React 页面直接构造 HTTP 请求,所以接口仍必须独立完成验签与授权。

真正的认证边界在 jwt.verify

模拟 /api/repo 从 Authorization 中拆出 token,并调用 jwt.verify。成功后读取解码结果,失败返回业务 code: 401

js 复制代码
try {
  const decoded = jwt.verify(token, signingSecret);
  return { code: 0, data: decoded.user };
} catch {
  return { code: 401, msg: 'Invalid token' };
}

这一步确认签名和有效期,但当前 Demo 还有三个值得观察的缺口:

  1. 缺少 Authorization 时直接 split,异常可能发生在 try 之前;
  2. Axios 没有统一处理 401,过期 token 不会自动清理或跳转;
  3. App 挂载时无条件请求受保护资源,即使尚未登录。

第三点尤其容易被忽略。是否发请求本身就应依赖认证状态,而不是让后端承担所有无效流量。

401 应该成为一条明确协议

一个可维护的实现需要先区分场景,再决定动作:

场景 客户端动作 服务端动作
未携带 token 导航登录或保持游客态 返回明确的未认证响应
token 格式错误 清理本地身份 拒绝请求,不尝试容错猜测
token 过期 按设计刷新或重新登录 返回可识别的过期原因
token 签名无效 清理并阻断 记录安全事件所需信息,但不泄露秘密
token 有效但无权限 保持登录态,提示无权限 返回 403 并执行资源级授权

不要把所有失败都粗暴理解为"退出登录"。如果未来加入 refresh token,还要避免多个并发请求同时触发刷新,形成刷新风暴。当前项目没有这部分实现,因此这里只给出设计边界,不声称已有运行结果。

一张鉴权闭环自检表

可以按下面顺序审查任何 React JWT 项目:

  • 登录响应是否只返回必要身份数据与 token;
  • 签名密钥是否完全留在服务端并脱离源码;
  • 内存状态与持久化状态是否同时写入、同时清理;
  • 所有业务请求是否复用同一个 Axios 实例;
  • Authorization 是否严格使用 Bearer <token>
  • 前端路由守卫是否仅被当作体验层;
  • 每个受保护接口是否验签并继续做授权;
  • 缺头、格式错、过期、伪造是否有独立处理;
  • 401/403 是否有清晰、一致的客户端协议;
  • token 存储是否结合 XSS、CSRF 威胁评估;
  • JWT Payload 是否避免密码、密钥和敏感数据;
  • 受保护请求是否只在合适的认证状态触发。

可迁移的判断

JWT 登录不是"登录接口 + localStorage"的组合,而是一条跨越 UI、状态、HTTP 客户端和服务端的数据流。Zustand 让状态可共享,Axios 让传输协议统一,React Router 改善页面体验;只有服务端验签与授权才能完成安全判断。

审查现有项目时,先沿着 签发 → 保存 → 携带 → 页面放行 → 服务端验签 走一遍,再检查失败路径。下一步最小实验可以是:补齐缺失请求头处理与统一 401 响应,然后分别用无 token、过期 token 和伪造 token 验证行为。

标签: React、JWT、Zustand、Axios、前端鉴权

相关推荐
四千岁1 小时前
稀疏向量BM25Retriever不支持中文怎么办?jieba来帮忙
前端·javascript·后端
这是个栗子1 小时前
【JS代码分析】前端鉴权基础:Token 的本地存储与状态重置实践
开发语言·前端·javascript
半个落月1 小时前
React 性能优化入门:用 memo 避免无关的子组件重复渲染
前端·react.js
界面开发小八哥3 小时前
界面控件DevExpress Blazor v26.1新版亮点 - TreeList & 数据编辑器
javascript·编辑器·.net·界面控件·blazor·devexpress·用户界面
风月说与山鬼3 小时前
二、uni-app入口文件(main.js)
javascript·uni-app
mCell11 小时前
Lua 编程入门:从基础语法到元表
javascript·算法·lua
伟大的兔神12 小时前
我做了一个本地优先的 AI 图片工作台:Loomora v1.0.0 正式发布
前端·javascript·vue.js
用户9385156350714 小时前
React + JWT 登录鉴权底层原理与工程化实践
前端·react.js
fangzhanpeng16814 小时前
(前端)2.js变量作用域样例
开发语言·前端·javascript