React + JWT 登录鉴权系统

读完这篇文章,你将彻底弄懂:JWT 是什么、Zustand 怎么用、Mock 数据如何模拟后端、路由守卫如何保护页面 ------ 以及它们之间是如何默契配合的。


1. 开胃菜:这是啥玩意儿?

想象一下,你开了一家高级会员制健身房 🏋️:

  • 前台(登录页) :客人来了,报名字和手机号(输入用户名密码)。
  • 会员卡(JWT Token) :验证通过后,前台给客人发一张带防伪标识的会员卡(有效期 24 小时)。
  • 门禁系统(路由守卫) :每个入口都有保安,只看卡不看人,有卡就放行,没卡请去前台办卡。
  • 智能储物柜(Zustand Store) :客人可以把会员卡和随身物品存进去,全健身房通用,不用每次都去前台取。
  • 自动扫码器(Axios 拦截器) :客人每次使用器材时,机器自动扫描会员卡,不用客人手动掏卡。

这就是整个项目的核心故事! 而我们今天要拆解的源码,就是这套系统的完整实现。


2. 名词解释大全

2.1 JWT(JSON Web Token)

维度 内容
官方定义 一种紧凑的、URL 安全的令牌格式,用于在各方之间安全传输声明信息
大白话 一张自带防伪标识的电子会员卡,里面存着你的身份信息,别人改不了,因为改了防伪码就对不上
代码体现 jwt.sign({ user: 'admin' }, 'secret819!$', { expiresIn: 86400 }) 生成 Token;jwt.verify(token, 'secret819!$') 验证
解决痛点 传统 Session 存在服务端内存中,分布式部署时多台服务器需要共享 Session,麻烦;JWT 无状态,任何服务器都能独立验证

💡 特别提醒 :JWT 的签名机制保证了数据未被篡改 ,但并不加密数据!Payload 里的内容是 Base64 编码的,任何人都能解码查看 ,所以绝对不要在 JWT 里存放密码、银行卡号等敏感信息!

2.2 Zustand

维度 内容
官方定义 一个轻量级的 React 状态管理库,基于不可变状态和订阅模式
大白话 一个全健身房通用的智能储物柜,任何器材区(组件)都能存取物品(状态),不用层层传递
代码体现 export const useAuthStore = create((set) => ({ token: '', setAuth: ({ token, user }) => set({ token, user }) }))
解决痛点 不用 Redux 那套繁琐的 Action/Reducer/Provider 模板,几行代码就能实现全局状态共享

2.3 Mock 服务(vite-plugin-mock)

维度 内容
官方定义 在开发环境下拦截 HTTP 请求,返回模拟数据的 Vite 插件
大白话 仿真健身房前台,不用等真正的后端开发完成,前端自己就能模拟所有接口
代码体现 viteMockServe({ mockPath: 'mock', localEnabled: true }) 开启;mock/login.js 中定义 /api/login 的响应逻辑
解决痛点 前后端并行开发时,前端不用"干等"后端接口,自己就能跑通完整流程

2.4 Axios 拦截器(Interceptor)

维度 内容
官方定义 在请求发送前或响应返回后,统一处理逻辑的钩子函数
大白话 自动扫码器------每次请求自动把会员卡(Token)挂到请求头上,不用手动操作
代码体现 instance.interceptors.request.use(config => { config.headers['Authorization'] =Bearer ${token}; return config })
解决痛点 不用在每个 API 调用处都写一遍 headers: { Authorization: token }一处配置,全局生效

2.5 路由守卫(RequireAuth)

维度 内容
官方定义 在路由渲染前进行条件判断,决定是否允许访问目标页面
大白话 健身房门禁保安,有卡(Token)才让进付费区,没卡就引导去前台(登录页)
代码体现 <Route path="/pay" element={<RequireAuth><Pay /></RequireAuth>} />;守卫内判断 if (!token) return <Navigate to="/login" />
解决痛点 防止未登录用户直接通过 URL 访问受保护页面,提升应用安全性

3. 项目文件结构总览

在深入流程之前,先看清每个文件的职责:

text

arduino 复制代码
📁 项目根目录
├── vite.config.js          ← Vite 配置,开启 Mock 服务
├── mock/
│   └── login.js            ← Mock 登录接口(模拟后端)
├── src/
│   ├── api/
│   │   ├── config.js       ← Axios 配置 + 请求/响应拦截器
│   │   ├── user.js         ← 登录 API 调用
│   │   └── repo.js         ← 仓库 API 调用(需要 token)
│   ├── store/
│   │   ├── user.js         ← Zustand 用户状态管理(含持久化)
│   │   └── todos.js        ← Zustand todos 状态管理
│   ├── components/
│   │   ├── Nav.js          ← 导航栏(显示登录状态)
│   │   └── RequireAuth.js  ← 路由守卫(检查 token)
│   ├── pages/
│   │   ├── Login.js        ← 登录页面
│   │   ├── Home.js         ← 首页
│   │   └── Pay.js          ← 支付页(受保护)
│   └── App.js              ← 根组件,定义路由 + 懒加载

4. 核心流程图解

4.1 完整登录流程时序图

4.2 后续请求自动携带 Token 流程

5. 完整流程详解:从第一次访问到登录成功

阶段一:启动项目(vite.config.js)

javascript

javascript 复制代码
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import { viteMockServe } from 'vite-plugin-mock'

export default defineConfig({
  plugins: [
    react(),
    viteMockServe({
      mockPath: 'mock',      // 👈 告诉 Vite:去 mock 文件夹找接口定义
      localEnabled: true,    // 👈 开发环境启用 Mock
    })
  ],
})

作用 :启动 Vite 开发服务器时,自动加载 mock/ 目录下的文件,拦截所有 /api/* 请求。


阶段二:用户打开页面(App.js)

javascript

javascript 复制代码
import { useAuthStore } from './store/user'
import { getRepo } from './api/repo'

function App() {
  useEffect(() => {
    // 一进来就调用 getRepo(),但这个请求会被拦截器处理
    (async () => {
      const res = await getRepo()  // 这个请求需要 token
    })()
  }, [])

  return (
    <Router>
      <Nav />  {/* 👈 导航栏,显示登录状态 */}
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/login" element={<Login />} />
        <Route path="/pay" element={
          <RequireAuth>  {/* 👈 路由守卫!检查有没有 token */}
            <Pay />
          </RequireAuth>
        } />
      </Routes>
    </Router>
  )
}

此时

  • Nav 组件读取 useAuthStore,发现 token 是空字符串 → 显示"Login"链接
  • /pay 路由被 RequireAuth 包裹 → 检查 token,没有 → 重定向到 /login

阶段三:路由守卫拦截(RequireAuth.js)

javascript

javascript 复制代码
import { Navigate } from "react-router-dom"
import { useAuthStore } from '../store/user'

function RequireAuth({ children }) {
  // 从 store 读取 token(优先用内存的,因为 localStorage 变化不会触发重新渲染)
  const token = useAuthStore(state => state.token)
  
  if (!token) {
    // 没有 token → 重定向到登录页
    return <Navigate to="/login" replace />
  }
  
  // 有 token → 渲染子组件
  return children
}

export default RequireAuth

阶段四:用户看到登录页(Login.js)

javascript

javascript 复制代码
import { useAuthStore } from '../store/user'
import { login } from '../api/user'

function Login() {
  const navigate = useNavigate()
  const setAuth = useAuthStore(state => state.setAuth)  // 👈 获取"设置认证"方法

  const [formData, setFormData] = useState({ username: '', password: '' })
  const [errors, setErrors] = useState({ username: '', password: '' })
  const [isValid, setIsValid] = useState(false)

  // 表单验证逻辑
  useEffect(() => {
    const newErrors = { username: '', password: '' }
    if (!formData.username.trim()) {
      newErrors.username = '用户名不能为空'
    } else if (formData.username.length < 3) {
      newErrors.username = '用户名至少3位'
    }
    if (!formData.password.trim()) {
      newErrors.password = '密码不能为空'
    } else if (formData.password.length < 6) {
      newErrors.password = '密码至少6位'
    }
    setErrors(newErrors)
    setIsValid(!newErrors.username && !newErrors.password)
  }, [formData])

  const handleLogin = async (e) => {
    e.preventDefault()
    try {
      const res = await login(formData)  // 👈 调用 API
      // login() 里面是:axios.post('/login', data)
      
      if (res.code === 0) {
        setAuth({ token: res.token, user: res.user })  // 👈 保存到 store
        navigate('/')  // 👈 跳转到首页
      } else {
        alert(res.message || '登录失败')
      }
    } catch (err) {
      alert('登录失败')
    }
  }

  return (
    <div>
      <h2>登录</h2>
      <form onSubmit={handleLogin}>
        {/* 用户名输入框 */}
        {/* 密码输入框 */}
        <button type="submit" disabled={!isValid}>登录</button>
      </form>
    </div>
  )
}

阶段五:登录请求发出(api/config.js + api/user.js)

这是回答你核心疑问的关键部分!

javascript

javascript 复制代码
// api/config.js - Axios 配置
import axios from 'axios'

const instance = axios.create({
  baseURL: '/api',   // 👈 所有请求自动加 /api 前缀
  timeout: 5000,     // 5 秒超时
})

// 📌 请求拦截器:每次请求发送前执行
instance.interceptors.request.use(config => {
  // config 是 Axios 的请求配置对象
  // 它包含了:url、method、headers、data、params 等所有请求信息
  
  const token = localStorage.getItem('token')  // 👈 从 localStorage 读 token
  
  if (token) {
    // 如果有 token,加到请求头
    // 'Bearer ' 是标准前缀,表示"持有者令牌"
    config.headers['Authorization'] = `Bearer ${token}`
  }
  // ⚠️ 如果没有 token,就不加(登录请求就是这种情况!)
  
  return config  // 👈 必须返回,否则请求发不出去
})

// 📌 响应拦截器:每次收到响应后执行
instance.interceptors.response.use(
  res => {
    return res.data  // 👈 只返回 data,屏蔽 axios 的元数据
  },
  err => {
    return Promise.reject(err)
  }
)

export default instance

javascript

javascript 复制代码
// api/user.js - 登录 API
import axios from './config'

export const login = async (data) => {
  const res = await axios.post('/login', data)
  // 实际请求:POST /api/login
  // 因为 config.js 中 baseURL 是 '/api'
  return res
}

这里重点回答你的问题

config 是什么?

config 是 Axios 内部创建的请求配置对象,包含:

javascript

css 复制代码
{
  url: '/login',
  method: 'post',
  headers: { 'Content-Type': 'application/json' },
  data: { username: 'admin', password: '123456' },
  baseURL: '/api',
  timeout: 5000,
  // ... 更多配置
}

拦截器就是修改这个对象 ,比如往 headers 里塞 token。

第一次访问没有 token 怎么办?

登录请求不需要 token! 流程如下:

text

javascript 复制代码
用户点击登录
    ↓
axios.post('/login', { username, password })
    ↓
请求拦截器执行:localStorage 里没有 token → 不加 Authorization 头
    ↓
请求正常发送到服务器(Mock)
    ↓
服务器验证账号密码 → 生成 token → 返回给前端
    ↓
前端保存 token 到 localStorage
    ↓
后续请求自动带上 token

阶段六:Mock 服务器处理登录(mock/login.js)

javascript

javascript 复制代码
import jwt from 'jsonwebtoken'

export default [
  {
    url: '/api/login',    // 👈 匹配 axios 的 /api/login
    method: 'post',
    timeout: 2000,
    response: (req) => {
      const body = req.body
      
      // 验证账号密码
      if (body.username !== 'admin' || body.password !== '123456') {
        return { code: -1, message: 'username or password 错误' }
      }
      
      // 🔑 生成 JWT Token
      const token = jwt.sign(
        { 
          user: body.username,   // 用户信息
          role: 'admin'          // 角色权限
        },
        'secret819!$',           // 密钥(签名用)
        { expiresIn: 86400 }     // 24 小时过期
      )
      
      // 👈 返回 token 给前端
      return {
        code: 0,
        user: { username: body.username },
        token: token
      }
    }
  }
]

阶段七:前端保存 Token(store/user.js)

javascript

javascript 复制代码
import { create } from 'zustand'

export const useAuthStore = create((set) => ({
  // 📦 状态定义(初始值从 localStorage 恢复)
  token: localStorage.getItem('token') || '',
  user: JSON.parse(localStorage.getItem('user')) || null,
  
  // 🎯 Action:登录
  setAuth: ({ token, user }) => {
    // ① 持久化到 localStorage(刷新页面不丢失)
    localStorage.setItem('token', token)
    localStorage.setItem('user', JSON.stringify(user))
    
    // ② 更新内存状态(触发 UI 重新渲染)
    set({ token, user })
  },
  
  // 🚪 Action:退出
  logout: () => {
    localStorage.removeItem('token')
    localStorage.removeItem('user')
    set({ token: '', user: null })
  }
}))

登录成功后发生了什么

javascript

php 复制代码
setAuth({ token: 'eyJhbGciOiJIUzI1NiIs...', user: { username: 'admin' } })
    ↓
localStorage 存了一份(刷新页面还在)
    ↓
内存状态更新(所有订阅的组件立即重新渲染)
    ↓
Nav 组件看到 token 变了 → 显示"Hello, admin!"和"Logout"按钮
    ↓
navigate('/') 跳转到首页

阶段八:后续请求自动带 Token

javascript

javascript 复制代码
// api/config.js - 请求拦截器再次上场
instance.interceptors.request.use(config => {
  // 这次 localStorage 里有 token 了!
  const token = localStorage.getItem('token')
  
  if (token) {
    // ✅ 自动加上 Authorization 头
    config.headers['Authorization'] = `Bearer ${token}`
  }
  
  return config
})

// 组件中调用需要认证的 API
const res = await getRepo()  // 调用 /api/repo

// 实际发出的请求:
// GET /api/repo
// Headers: { Authorization: 'Bearer eyJhbGciOiJIUzI1NiIs...' }

阶段九:服务器验证 Token(mock/repo.js)

javascript

javascript 复制代码
import jwt from 'jsonwebtoken'

export default [
  {
    url: '/api/repo',
    method: 'get',
    response: (req) => {
      // 从请求头获取 token
      const authHeader = req.headers.authorization
      const token = authHeader?.split(' ')[1]  // 去掉 "Bearer " 前缀
      
      if (!token) {
        return { code: 401, message: '未提供 Token' }
      }
      
      try {
        // ✅ 验证 token(签名 + 过期时间)
        const decoded = jwt.verify(token, 'secret819!$')
        // 验证通过,返回数据
        return {
          code: 0,
          data: [
            { id: 1, name: 'react-demo' },
            { id: 2, name: 'vue-demo' }
          ]
        }
      } catch (error) {
        if (error.name === 'TokenExpiredError') {
          return { code: 401, message: 'Token 已过期' }
        }
        return { code: 401, message: 'Token 无效' }
      }
    }
  }
]

阶段十:导航栏显示登录状态(Nav.js)

javascript

javascript 复制代码
import { Link } from 'react-router-dom'
import { useAuthStore } from '../store/user'

function Nav() {
  const token = useAuthStore(state => state.token)
  const user = useAuthStore(state => state.user)
  const logout = useAuthStore(state => state.logout)
  
  const handleLogout = () => {
    logout()  // 清除 localStorage + 内存状态
  }
  
  return (
    <nav>
      <Link to="/">Home</Link>
      <Link to="/pay">Pay</Link>
      {!token && <Link to="/login">Login</Link>}
      {user && <a href="#">Hello, {user.username}!</a>}
      {token && <button onClick={handleLogout}>Logout</button>}
    </nav>
  )
}

export default Nav

6. 重难点深度剖析

6.1 难点一:JWT 的签名与验证机制

为什么要有这个设计?

HTTP 是无状态的,服务器每次收到请求都不知道是谁发的。传统方案是 Session:服务器存一份用户信息,给客户端发个 SessionId。但问题是------分布式部署时,用户第一次请求落在服务器 A,第二次可能落在服务器 B,B 不认识这个 SessionId,就要求重新登录。

JWT 的解法是:把用户信息直接编码进 Token 里,用签名保证不可篡改 。任何服务器拿到 Token,只要用同一个密钥验证签名通过,就能信任里面的数据------完全无状态

核心代码逐行注释

javascript

javascript 复制代码
import jwt from 'jsonwebtoken'

// 登录接口的 Mock 响应
response: (req) => {
  const body = req.body
  
  // 验证账号密码(硬编码测试数据)
  if (body.username !== 'admin' || body.password !== '123456') {
    return { code: -1, message: 'username or password 错误' }
  }

  // 🔑 生成 JWT Token
  const token = jwt.sign(
    // ① Payload:要存储的用户数据(明文,仅 Base64 编码)
    { 
      user: body.username,   // 用户名
      role: 'admin'          // 角色权限
    },
    // ② 密钥:用于签名,相当于"防伪印章"
    'secret819!$',
    // ③ 选项:过期时间
    {
      expiresIn: 86400       // 24 小时(单位:秒)
    }
  )
  
  return {
    code: 0,
    user: { username: body.username },
    token: token             // 返回给前端
  }
}

验证过程(在受保护接口中)

javascript

kotlin 复制代码
// 后端验证 Token
try {
  // jwt.verify 做了三件事:
  // 1. 拆分 Token 为 Header + Payload + Signature
  // 2. 用密钥重新计算 Signature
  // 3. 比对是否一致 + 检查 exp 是否过期
  const decoded = jwt.verify(token, 'secret819!$')
  // 验证通过,decoded 就是 { user: 'admin', role: 'admin', iat: 123, exp: 456 }
} catch (error) {
  // 验证失败:可能是签名不匹配(被篡改)或已过期
  return { code: 401, message: 'Token 无效' }
}

💡 这里你可能会有疑问jwt.signjwt.verify 用的密钥是同一个字符串 'secret819!$',这就是对称加密的特点------加密和解密用同一个密钥。所以这个密钥绝对不能泄露,否则任何人都能伪造 Token!


6.2 难点二:Zustand 的"闭包陷阱"与持久化

为什么要有这个设计?

React 的状态管理核心难题是:如何在组件树中共享可变数据 。传统方案是 Props 逐层传递(太麻烦)或 Context(性能问题)。Zustand 用发布-订阅模式实现了:任何组件都能订阅 Store 的某一部分,只有该部分变化时才重新渲染。

核心代码逐行注释

javascript

javascript 复制代码
import { create } from 'zustand'

export const useAuthStore = create((set) => ({
  // 📦 状态定义(初始值)
  token: localStorage.getItem('token') || '',              // 从 localStorage 恢复
  user: JSON.parse(localStorage.getItem('user')) || null,  // 解析 JSON

  // 🎯 Action:登录
  setAuth: ({ token, user }) => {
    // 先持久化到 localStorage(刷新页面不丢失)
    localStorage.setItem('token', token)
    localStorage.setItem('user', JSON.stringify(user))
    
    // 再更新内存状态(触发 UI 更新)
    set({ token, user })
  },

  // 🚪 Action:退出
  logout: () => {
    // 清除 localStorage
    localStorage.removeItem('token')
    localStorage.removeItem('user')
    
    // 重置内存状态
    set({ token: '', user: null })
  }
}))

为什么需要同时存 localStorage 和内存?

javascript

scss 复制代码
// ❌ 只存 localStorage
localStorage.setItem('token', token)
// 问题:组件不会自动重新渲染,因为 localStorage 变化不会触发 React 更新

// ❌ 只存内存
set({ token })
// 问题:刷新页面后数据丢失,用户需要重新登录

// ✅ 两者都存
localStorage.setItem('token', token)  // 持久化
set({ token })                        // 触发 UI 更新

⚠️ 注意set 是 Zustand 提供的状态更新函数 ,它内部实现了浅比较------如果新值和旧值相同,不会触发重新渲染。这是性能优化的关键。


6.3 难点三:Axios 拦截器的"洋葱模型"

为什么要有这个设计?

每个请求都要手动加 Authorization 头?每个响应都要手动解 res.data?太累了!拦截器就像流水线上的自动化工位,在请求"出厂"前统一包装,在响应"入库"时统一拆封。

核心代码逐行注释

javascript

javascript 复制代码
import axios from 'axios'

const instance = axios.create({
  baseURL: '/api',   // 所有请求自动加前缀
  timeout: 5000,     // 5 秒超时
})

// 🔧 请求拦截器(请求发出前执行)
instance.interceptors.request.use(config => {
  // config 是请求配置对象
  // 包含:url、method、headers、data、params 等
  // 在这里我们可以修改 config 的任何属性
  
  const token = localStorage.getItem('token')
  
  if (token) {
    // 将 Token 放入请求头
    // 'Bearer ' 是标准前缀,表示"持有者令牌"
    config.headers['Authorization'] = `Bearer ${token}`
  }
  
  // 必须返回 config,否则请求发不出去
  return config
})

// 🔧 响应拦截器(收到响应后执行)
instance.interceptors.response.use(
  res => {
    // 只返回数据部分,屏蔽 axios 的元数据
    // res 包含:data、status、headers、config、request 等
    return res.data
  },
  err => {
    // 统一错误处理
    return Promise.reject(err)
  }
)

export default instance

拦截器的执行顺序("洋葱模型")

text

kotlin 复制代码
请求 → 请求拦截器(前置处理)→ 发送请求 → 响应拦截器(后置处理)→ 组件
              ↑                                            ↓
         统一加 Token                              统一解 res.data

💡 这里你可能会有疑问 :为什么响应拦截器返回的是 res.data 而不是 res?因为 res 里还包含了 configheadersstatus 等元数据,业务层只关心 data。统一处理后,组件里拿到的就是干净的 { code, data, message }


6.4 难点四:首次访问无 Token 的完整处理链路

这是你最关心的问题,我们用一个完整的时序图来展示:

7. 避坑指南:新手的十面埋伏

7.1 坑点一:localStorage 的同步读取时机

❌ 错误示范

javascript

javascript 复制代码
// 在组件中直接使用 localStorage
function UserInfo() {
  const token = localStorage.getItem('token')  // ❌ 组件不会自动更新
  return <div>{token ? '已登录' : '未登录'}</div>
}

✅ 正确姿势

javascript

javascript 复制代码
// 使用 Zustand Store,因为 set 会触发重新渲染
function UserInfo() {
  const token = useAuthStore(state => state.token)  // ✅ 响应式
  return <div>{token ? '已登录' : '未登录'}</div>
}

💥 后果

直接读 localStorage 不会触发 React 重新渲染,用户登录后 UI 不会更新,造成"登录了但界面还显示未登录"的诡异 bug。


7.2 坑点二:JWT 过期后未处理 401

❌ 错误示范

javascript

csharp 复制代码
// 没有处理 401 状态码
const res = await getRepo()  // ❌ Token 过期时直接报错,用户卡死

✅ 正确姿势

javascript

ini 复制代码
// 在响应拦截器中统一处理 401
instance.interceptors.response.use(
  res => res.data,
  err => {
    if (err.response?.status === 401) {
      // Token 过期,清除状态并跳转登录
      useAuthStore.getState().logout()
      window.location.href = '/login'
    }
    return Promise.reject(err)
  }
)

💥 后果

Token 过期后,用户还在操作页面,所有请求都失败,但界面没有任何提示------用户以为 App 坏了,体验极差。


7.3 坑点三:Mock 和真实接口混用导致混淆

❌ 错误示范

javascript

php 复制代码
// vite.config.js
viteMockServe({
  mockPath: 'mock',
  localEnabled: true,   // ❌ 一直开着,联调时也走 Mock
})

✅ 正确姿势

javascript

arduino 复制代码
// 通过环境变量控制
viteMockServe({
  mockPath: 'mock',
  localEnabled: process.env.NODE_ENV === 'development',  // ✅ 仅开发环境
  prodEnabled: false,   // 生产环境绝对关闭
})

💥 后果

联调时忘记关 Mock,前端请求被 Mock 拦截,永远调不到真实后端------查了半天才发现是 Mock 在"捣乱"。


7.4 坑点四:JWT 中存放敏感信息

❌ 错误示范

javascript

php 复制代码
// 把密码、银行卡号等敏感信息放进 JWT
const token = jwt.sign(
  { 
    user: 'admin', 
    password: '123456',      // ❌ 绝对不能放!
    creditCard: '1234-5678'  // ❌ 绝对不能放!
  },
  'secret819!$'
)

✅ 正确姿势

javascript

arduino 复制代码
// 只放必要的身份标识信息
const token = jwt.sign(
  { 
    userId: '123',           // ✅ 用户 ID
    username: 'admin',       // ✅ 用户名
    role: 'admin',           // ✅ 权限角色
    // 不要放密码、银行卡、身份证号等敏感信息
  },
  'secret819!$'
)

💥 后果

JWT 的 Payload 只是 Base64 编码,任何人都能解码查看!如果放了敏感信息,等于把密码公开了。


8. 面试官问什么?

面试题 1:说说 JWT 和 Session 的区别,为什么选 JWT?

回答大纲

  1. 存储位置不同:Session 存在服务端内存/Redis,JWT 存在客户端。
  2. 状态特性不同:Session 是有状态的,JWT 是无状态的。
  3. 分布式支持:Session 需要共享存储(如 Redis),JWT 任意服务器都能独立验证。
  4. 安全性差异:Session 可以主动失效(管理员踢人),JWT 只能等过期。
  5. 选型建议:分布式系统、微服务架构适合 JWT;单体应用、需要主动控制会话的场景适合 Session。

面试题 2:Zustand 和 Redux 有什么异同?什么时候用 Zustand?

回答大纲

  1. 相同点:都是单向数据流,都基于不可变状态,都能跨组件共享状态。

  2. 不同点

    • Redux 需要 Action、Reducer、Provider 等模板代码;Zustand 只需 create
    • Redux 依赖 Context 传递,Zustand 用发布-订阅,性能更好。
    • Redux 有完善的中间件生态(如 Redux-Thunk、Saga),Zustand 生态较轻。
  3. 选型建议:中小型项目、简单状态共享 → Zustand;大型项目、复杂副作用管理 → Redux。


面试题 3:Axios 拦截器是如何实现的?为什么能拦截请求?

回答大纲

  1. 核心机制:Axios 在发送请求前和接收响应后,会遍历拦截器数组依次执行。
  2. 实现原理 :Axios 内部维护了 interceptors.requestinterceptors.response 两个数组,每个拦截器都是一个对象 { resolved, rejected }
  3. 执行顺序 :请求拦截器后添加的先执行 (类似栈),响应拦截器先添加的先执行(类似队列)。
  4. 关键点 :每个拦截器都必须返回 configresponse,否则链式调用会中断。

面试题 4:刷新页面后 JWT Token 还在吗?如何保持登录状态?

回答大纲

  1. JWT Token 存在哪里 :存在 localStoragesessionStorage 中。
  2. 刷新页面时 :页面重新加载,React 状态重置,但 localStorage 数据还在。
  3. 恢复状态 :Zustand Store 初始化时从 localStorage 读取 token,恢复登录状态。
  4. 验证 :用户操作时,请求拦截器自动从 localStorage 取 token 加到请求头;服务器验证 token 是否有效。

9. 总结

从健身房比喻到代码逐行拆解,我们完整走完了这套 React + JWT + Zustand + Mock 登录鉴权系统的每个角落:

核心组件 职责 关键代码行
JWT 无状态身份认证 jwt.sign() / jwt.verify()
Zustand 全局状态管理 + 持久化 create((set) => ({ ... }))
Mock 服务 模拟后端接口 viteMockServe({ mockPath: 'mock' })
Axios 拦截器 统一加 Token / 解响应 interceptors.request.use()
路由守卫 保护受控页面 RequireAuth 组件 + Navigate

完整请求链路回顾

text

markdown 复制代码
1. 用户第一次访问 → 无 token → 路由守卫拦截 → 跳转登录页
2. 输入账号密码 → 登录请求(无 token)→ Mock 验证 → 返回 token
3. setAuth() → localStorage 存 token + 内存更新 → UI 刷新
4. 后续请求 → 拦截器自动加 token → 服务器验证 → 返回数据
5. 刷新页面 → localStorage 恢复 token → 保持登录状态
6. Token 过期 → 服务器返回 401 → 清除状态 → 跳转登录

这套模式已成为现代前端开发的标准范式,理解它,你就掌握了 React 全家桶生态中最重要的协作流程。🎉

相关推荐
hunterandroid1 小时前
[鸿蒙从零到一] HarmonyOS 安全加固与数据保护实战:从密钥管理到反调试的全链路防护
前端
用户921080262861 小时前
从 Bubble 到 BubbleList:加载历史对话时才真正看懂组件通信
前端
hunterandroid1 小时前
Android WebView JSBridge 治理实战:从线上白屏崩溃到协议化通信
android·前端
意疏1 小时前
2026年远控软件安全横评:六款主流工具逐项核查——官方文档、一手实测与安全事件,全摊开
大数据·前端·数据库
梦曦i2 小时前
RouterLink H5端控制台错误修复
前端·uni-app
qq_548612452 小时前
.NET 平台报表工具汇总(.NET Framework /.NET6-8,中国式复杂报表、Web 嵌入、填报、导出打印)
前端·.net
懒狗小前端2 小时前
自嗨不如一起嗨
前端
郑州光合科技余经理2 小时前
海外版多语言团购系统架构:主数据互通与核销边界
java·开发语言·前端·后端·系统架构·php·ai编程