Mock服务端签发JWT token的流程

在实际项目中,后端通常用 JWT 签发 token,前端需要先模拟这个过程来快速验证整套流程。Mock 服务端一般使用 json-server 或直接在代码里写一个简单的 Express 路由来处理登录请求。

登录接口接收用户名和密码后,先做简单校验。如果通过,就生成一个 JWT token。生成时需要指定 payload,通常包含 userId、username 和过期时间 exp。签名使用一个 secret key,比如 “your-secret-key”。返回的响应体结构一般是 { code: 200, data: { token: “eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…”, user: { id: 1, name: “admin” } } }。

这个 Mock 流程的核心是让前端能拿到格式正确的 token,而不用等待真实后端接口就绪。实际开发中,token 的过期时间建议设为 1 小时,刷新 token 的逻辑后续再补。Mock 代码通常放在单独的 mock 文件夹里,通过 vite-plugin-mock 或直接启动一个 node 服务来拦截 /api/login 请求。

这样做的好处是前后端可以并行开发。前端拿到 token 后立即进入状态管理环节,而不用关心后端具体的 JWT 库是 jsonwebtoken 还是其他。整个签发过程强调了 JWT 的无状态特性:服务端不需要存 session,所有信息都塞在 token 里。

(本节约 380 字)

Zustand集中管理登录token状态

Zustand 的优势在于轻量且不依赖 Context 带来的渲染问题。它通过 create 函数建立一个 store,里面存放 token、userInfo 和几个 action。

典型实现是定义一个 useAuthStore,里面有 persist 中间件把 token 存到 localStorage。persist 配置指定 name 为 “auth-storage”,partialize 函数只持久化 token 和 refreshToken,避免把整个 user 对象都存进去。setToken action 不仅更新 token,还解析 payload 把用户信息提取出来存到 user 字段。

logout action 负责清空 token 和 user,并调用可选的清理函数。getState() 可以随时在非组件环境下拿到最新 token,这对拦截器特别友好。相比 Redux,Zustand 的 boilerplate 少很多,订阅也更精确,只有真正依赖 token 的组件才会重渲染。

在项目中,通常把这个 store 放在 src/store/auth.ts 里,导出 hook 和单独的 selector 函数,比如 useAuthStore(state => state.token)。这样既能集中管理状态,又方便后续性能优化。

(本节约 350 字)

Axios拦截器自动附加Authorization头

Axios 实例创建后立即挂载两个拦截器。request 拦截器最关键,它从 Zustand store 里取出 token,如果存在就拼成 “Bearer ${token}” 塞到 config.headers.Authorization。

代码通常写成:

1
2
3
4
5
6
7
axiosInstance.interceptors.request.use(config => {
  const token = useAuthStore.getState().token;
  if (token) {
    config.headers.Authorization = `Bearer ${token}`;
  }
  return config;
});

response 拦截器则处理 401 错误。如果返回 401,就触发登出逻辑并跳转到登录页。同时可以在这里实现 token 刷新机制:当收到特定错误码时,先调用 refresh 接口拿到新 token,再重放原来的请求。

拦截器统一管理了所有请求的鉴权逻辑,避免每个 api 文件手动加 header。实际项目中还会加上超时配置、baseURL 统一前缀,以及错误统一提示。拦截器内部不能直接使用 React hook,所以必须通过 getState() 来读取 Zustand 数据。

(本节约 340 字)

路由守卫保护敏感页面的逻辑

React Router v6 中,路由守卫一般通过高阶组件或自定义的 ProtectedRoute 实现。ProtectedRoute 组件会先从 Zustand 读取 token,如果不存在就重定向到 /login,并把当前路径通过 state 传递过去,登录成功后可以跳回来。

更完整的做法是使用 loader 函数结合 React Router 的数据路由。在 loader 里检查 token,不通过就 throw redirect(’/login’)。对于 admin 这样的敏感页面,还可以再加角色判断,如果 user.role !== ‘admin’ 也进行重定向。

路由配置文件里把需要保护的路由包裹在 里,或者直接在 route definition 中指定 loader。登出后需要调用 navigate(’/login’, { replace: true }) 来清除历史记录,防止用户通过浏览器后退键回到已登出页面。

这个守卫逻辑把鉴权从组件内部提升到了路由层面,代码更清晰,也更容易统一管理权限菜单的动态渲染。

(本节约 320 字)

token存储与过期处理的安全坑点

把 token 存在 localStorage 是最常见的做法,但它也带来明显风险。XSS 攻击一旦成功,攻击者就能轻松读取 localStorage 里的 token。相比之下,httpOnly cookie 更安全,但会失去前端对 token 的控制权,刷新 token 变得麻烦。

CSRF 攻击在 JWT 场景下风险较低,因为 JWT 不依赖 cookie 的自动携带机制。但如果同时使用了 cookie 存储 refreshToken,就必须严格设置 SameSite=Strict 并验证 origin。

过期处理是另一个重点。token 通常设短过期时间(15 分钟到 1 小时),配合一个长效 refreshToken。拦截器在 401 时自动调用刷新接口,成功后更新 Zustand 并重试原请求。刷新失败才真正登出。

另外需要注意 token 泄露后的主动失效机制,后端应提供黑名单或短过期时间。前端在 logout 时除了清 localStorage,还应调用后端 logout 接口让 refreshToken 失效。

(本节约 370 字)

鉴权链路的性能优化建议

Zustand 本身已经比较轻量,但仍需避免滥用 getState() 导致不必要的 store 订阅。推荐在组件里使用浅比较的 selector,只订阅 token 是否存在,而不是整个 user 对象。

Axios 拦截器里每次请求都读一次 store 其实代价很小,但如果项目有上百个并发请求,可以考虑把 token 缓存在一个闭包变量里,只在 token 真正变化时更新。

路由守卫的 loader 如果每次都解析 JWT payload 会产生重复计算,可以把解析后的 claims 也存到 store 里,守卫只做存在性检查。

对于大型应用,建议把登录态相关的组件用 React.memo 包裹,并把 token 作为 prop 显式传递,而不是每个子组件都直接用 useAuthStore。结合 React Query 或 SWR 时,可以把 token 作为 queryClient 的默认 header,在 query 层面统一注入,避免重复的拦截器逻辑。

这些优化能把鉴权带来的额外渲染和请求开销降到最低,尤其在低端设备或慢网络环境下效果明显。

(本节约 350 字)

完整前后端鉴权链路集成验证

把前面所有环节串起来后,需要在真实项目场景中进行端到端测试。首先启动 Mock 服务,访问登录页,输入测试账号,观察网络面板是否正确返回 token。登录成功后跳转到首页,Zustand store 里应该已有 token 和用户信息。

接着打开浏览器开发者工具的 Application 面板,确认 localStorage 里存了 auth-storage。刷新页面,状态应该被 persist 恢复。然后访问 /dashboard 等受保护路由,未登录时应自动跳回登录页。

登录后再次访问,Axios 发出的每个请求头里都应带有 Authorization: Bearer xxx。故意让 token 过期,拦截器应捕获 401 并尝试刷新或直接登出。

最后测试登出流程:清除 store 和 localStorage,路由跳转回登录页,且无法通过后退键返回已登录页面。这些测试覆盖了正常登录、页面刷新、token 过期、未授权访问、主动登出等常见场景。

在实际项目中,这套链路还能方便地扩展为多角色权限控制、动态菜单加载等功能。整个流程把 JWT 的无状态优势和前端状态管理的便利性结合在一起,是目前大多数中后台项目的标准做法。

(本节约 410 字)

参考来源