从零搞懂 JWT 登录鉴权:一张“电子通行证”的诞生记

引言:为什么你的网站需要"记住"用户?

打开一个网站,登录一次,关闭浏览器再打开------咦,还是登录状态?网站是怎么"记住"你的?

答案藏在两个字里:鉴权

HTTP 协议本身是无状态(Stateless) 的。什么意思?简单说,服务器每次接到你的请求,都当作第一次见到你------它记不住你是谁。如果你刷新一次页面就要重新输一次密码,这体验恐怕没人受得了。

所以我们需要一种机制:用户登录之后,后续的每一次请求都能证明"我是我"。这就是身份认证要解决的问题。

目前主流的方案有两类:一种是基于服务端 Session 的 Cookie 机制,另一种就是今天的主角------JWT(JSON Web Token)

一、JWT 是什么?一张自带身份的"电子通行证"

JWT 的全称是 JSON Web Token。它本质上是一个经过加密签名的字符串,由服务器在用户登录成功后颁发给客户端。

你可以这样理解:你登录成功了,服务器没有在它自己那边给你记小本本(Session),而是直接给你发了一张"电子通行证"。这张证里写明了"你是谁""你有什么权限"。以后你每次来访问,只要出示这张证,服务器验证一下真伪,就放你进去了

1.1 JWT 长什么样?

一个典型的 JWT 是这样的字符串:

text

复制代码
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4ifQ.4Rih-Jg6X8Z0X3yZ7Jw3X7yZ7Jw3X7yZ7Jw3X7yZ7Jw

它由三部分组成,用英文句号 . 分隔

  • Header(头部) :相当于"说明书",告诉别人这张证是什么类型、用什么算法签的名。
  • Payload(载荷) :真正存放信息的地方,比如用户ID、用户名、角色等
  • Signature(签名)防篡改的关键 。服务器用只有自己知道的密钥,对前两部分进行加密计算,生成签名

这三部分各司其职:Header 和 Payload 负责"装信息",Signature 负责"保安全"。

1.2 JWT 的认证流程

整个流程可以用四步概括

  1. 用户提交凭证:用户在登录页输入用户名和密码,前端发送到服务器。
  2. 服务器验证并签发 :服务器核对用户名密码。如果正确,用 jwt.sign() 方法生成一个 JWT 令牌,返回给前端。
  3. 前端存储令牌 :前端拿到 JWT 后存起来(通常存在 localStorage 里)。
  4. 后续请求携带令牌 :之后每次请求需要权限的接口,前端都在 HTTP 请求头里带上这个 JWT。服务器收到后用 jwt.verify() 方法验证令牌的真伪和有效期。

二、JWT 对比传统 Session:为什么选择 JWT?

在 JWT 之前,主流方案是 Cookie + Session

  • 用户登录后,服务器在内存中创建一个 Session 对象,记录用户信息。
  • 服务器把一个 sessionId 写入浏览器的 Cookie 里。
  • 之后每次请求,浏览器自动带上这个 Cookie,服务器根据 sessionId 找到对应的 Session,确认用户身份

这个方案在单台服务器上运行良好,但到了分布式系统就会出问题------用户第一次请求落在服务器 A,Session 存在 A 的内存里;第二次请求被负载均衡转发到了服务器 B,B 的内存里没有这个 Session,用户就被"踢出去了"。

JWT 的优势恰恰在这里:它是自包含的 。用户信息直接写在 Token 里,任何一台服务器拿到 Token 都能自己解码验证,不需要去问别的服务器。这就天然适合分布式和微服务架构。

对比维度 Session-Cookie JWT
服务端存储 需要存 Session 不需要
分布式支持 需要共享存储 天然支持
跨域 受限 友好
信息携带 仅 sessionId 可携带用户信息

三、实战:用 React + Zustand + JWT 搭建登录系统

理论说完了,我们来看一个完整的实战项目。技术栈包括:

  • React:前端 UI 框架
  • React Router:路由管理
  • Zustand:全局状态管理
  • Axios:HTTP 请求
  • jsonwebtoken :生成和验证 JWT(模拟后端)

3.1 第一步:Mock 后端------签发 JWT

在真正的项目中,JWT 的签发和验证由后端服务器完成。但在开发阶段,我们可以用 Mock 来模拟后端行为

先安装 jsonwebtoken 包:

bash

复制代码
npm install jsonwebtoken

然后创建一个 Mock 登录接口:

javascript

javascript 复制代码
import jwt from 'jsonwebtoken';

// 这个密钥只有服务器知道,绝对不能泄露
const secret = 'secret819!$';

// Mock 接口配置
export default [
  {
    url: '/api/login',
    method: 'post',
    response: (req) => {
      const { username, password } = req.body;
      
      // 验证用户名密码
      if (username !== 'admin' || password !== '123456') {
        return {
          code: -1,
          message: '用户名或密码错误'
        };
      }
      
      // 验证通过,签发 JWT
      const token = jwt.sign(
        {
          user: username,
          role: 'admin'
        },
        secret,
        {
          expiresIn: 86400  // 有效期 24 小时,单位秒
        }
      );
      
      return {
        code: 0,
        user: { username },
        token: token
      };
    }
  }
];

这里有几个关键点:

  • jwt.sign() 方法接收三个参数:要存放的数据(Payload)、密钥、配置项(如过期时间)
  • 密钥(Secret) 是服务器的"底牌",必须保密。如果泄露,攻击者就能伪造任意用户的 Token。
  • 过期时间 是一种安全措施------就算 Token 被窃取,也只在有限时间内有效。

3.2 第二步:受保护接口------验证 JWT

有了签发的接口,还得有一个需要验证 Token 才能访问的接口:

javascript

kotlin 复制代码
{
  url: '/api/repo',
  method: 'get',
  response: (req) => {
    // 从请求头中提取 Token
    const authHeader = req.headers['authorization'];
    if (!authHeader) {
      return { code: 401, msg: '未携带 token' };
    }
    
    const token = authHeader.split(' ')[1];  // 去掉 "Bearer " 前缀
    
    try {
      // 验证 Token
      const decoded = jwt.verify(token, secret);
      return {
        code: 0,
        data: decoded.user
      };
    } catch (err) {
      return {
        code: 401,
        msg: 'Invalid token'
      };
    }
  }
}

jwt.verify() 方法会做两件事:

  1. 验证签名:用密钥重新计算签名,比对是否一致。不一致说明 Token 被篡改了。
  2. 检查过期:如果 Token 超过了设定的有效期,也会报错。

3.3 第三步:Zustand------管理登录状态

登录状态需要在整个应用中共享------导航栏要显示用户名,受保护页面要判断是否允许访问。这就需要状态管理

Zustand 是一个轻量级的 React 状态管理库。相比 Redux,它不需要写大量的样板代码,API 非常简洁。

先安装:

bash

复制代码
npm install zustand

然后创建用户状态 Store:

javascript

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

export const useAuthStore = create((set) => ({
  // 状态:从 localStorage 恢复登录信息
  token: localStorage.getItem('token') || '',
  user: JSON.parse(localStorage.getItem('user')) || null,
  
  // 动作:登录
  setAuth: ({ token, user }) => {
    localStorage.setItem('token', token);
    localStorage.setItem('user', JSON.stringify(user));
    set({ token, user });
  },
  
  // 动作:登出
  logout: () => {
    localStorage.removeItem('token');
    localStorage.removeItem('user');
    set({ token: '', user: null });
  }
}));

create() 函数接收一个回调,回调的参数 set 用来更新状态。我们在 setAuthlogout 中同时更新了 Zustand 状态和 localStorage,这样即使刷新页面,登录状态也不会丢失

在组件中使用这个 Store:

jsx

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

function Nav() {
  // 只订阅 token,组件只在 token 变化时重新渲染
  const token = useAuthStore(state => state.token);
  const logout = useAuthStore(state => state.logout);
  
  return (
    <nav>
      {!token && <Link to="/login">登录</Link>}
      {token && <button onClick={logout}>退出</button>}
    </nav>
  );
}

Zustand 的 useAuthStore(state => state.token) 这种写法叫做选择器(Selector) ,它让组件只订阅自己需要的状态片段,避免不必要的重新渲染。

3.4 第四步:Axios 拦截器------自动携带 Token

每次请求都要手动从 localStorage 取 Token 再塞到请求头里,太麻烦了。Axios 的请求拦截器 可以帮我们自动完成这件事

javascript

javascript 复制代码
import axios from 'axios';

const instance = axios.create({
  baseURL: '/api',
  timeout: 5000
});

// 请求拦截器:在请求发出前自动添加 Token
instance.interceptors.request.use(config => {
  const token = localStorage.getItem('token');
  if (token) {
    config.headers['Authorization'] = `Bearer ${token}`;
  }
  return config;
});

export default instance;

这里的 Bearer 是一个约定前缀,表示这是一个 Bearer Token。大多数后端框架都认识这个格式。

封装登录 API:

javascript

javascript 复制代码
import axios from './config';

export const login = async (data) => {
  const res = await axios.post('/login', data);
  return res.data;
};

3.5 第五步:路由守卫------保护需要登录的页面

有些页面只有登录用户才能访问,比如个人中心、支付页面。我们需要一个路由守卫 来做拦截

jsx

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

function RequireAuth({ children }) {
  const token = useAuthStore(state => state.token);
  
  // 没有 token,说明未登录,跳转到登录页
  if (!token) {
    return <Navigate to="/login" replace />;
  }
  
  // 已登录,正常渲染子组件
  return children;
}

export default RequireAuth;

在路由配置中使用:

jsx

javascript 复制代码
import RequireAuth from './components/RequireAuth';

function App() {
  return (
    <Router>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/login" element={<Login />} />
        <Route path="/pay" element={
          <RequireAuth>
            <Pay />
          </RequireAuth>
        } />
      </Routes>
    </Router>
  );
}

这样,当未登录的用户访问 /pay 时,会被自动重定向到 /login

3.6 第六步:登录页面------完整流程串联

最后看登录页面的实现,把前面的所有环节串起来:

jsx

javascript 复制代码
import { useNavigate } from 'react-router-dom';
import { login } from '../api';
import { useAuthStore } from '../store/user';

function Login() {
  const navigate = useNavigate();
  const setAuth = useAuthStore(state => state.setAuth);
  const [formData, setFormData] = useState({ username: '', password: '' });
  
  const handleLogin = async (e) => {
    e.preventDefault();
    try {
      const res = await login(formData);
      if (res.code === 0) {
        // 登录成功:更新 Zustand 状态 + localStorage
        setAuth({ token: res.token, user: res.user });
        // 跳转到首页
        navigate('/');
      } else {
        alert(res.message || '登录失败');
      }
    } catch (err) {
      console.error(err);
      alert('登录失败,请稍后重试');
    }
  };
  
  return (
    <form onSubmit={handleLogin}>
      <input name="username" onChange={...} />
      <input name="password" type="password" onChange={...} />
      <button type="submit">登录</button>
    </form>
  );
}

四、关于 Token 存储的安全提醒

在实战代码中,我们把 Token 存到了 localStorage 里。这在开发中很常见,但有必要了解其中的安全风险

localStorage 里的数据可以被同一域名下的任何 JavaScript 代码 读取。如果网站存在 XSS(跨站脚本攻击) 漏洞,攻击者就能注入恶意脚本,偷走 Token,然后冒充用户。

更安全的做法是把 Token 放在 HttpOnly Cookie 里------这种 Cookie 无法被 JavaScript 读取,只能由浏览器自动在请求中携带,能有效防御 XSS 导致的 Token 泄露。

不过 HttpOnly Cookie 也有自己的问题(比如需要防范 CSRF 攻击)。选择哪种方案,要根据项目的安全等级来决定。

五、总结

我们把整个流程再梳理一遍:

  1. 用户登录 :输入用户名密码,前端调用 /api/login
  2. 服务器签发 JWT :验证通过后,用 jwt.sign() 生成 Token 返回。
  3. 前端存储 :用 Zustand 管理 Token 和用户信息,同时写入 localStorage 做持久化。
  4. 自动携带 :Axios 请求拦截器在每个请求的 Header 中自动带上 Authorization: Bearer <token>
  5. 服务器验证 :受保护接口用 jwt.verify() 验证 Token 的有效性。
  6. 路由守卫:未登录用户访问受保护页面时自动跳转到登录页。

这套流程的核心思想是:服务器不存状态,只负责签发和验证 Token;客户端负责保管和携带 Token 。正是这种"无状态"的设计,让 JWT 在分布式系统中游刃有余

理解了这些,你不仅能用 JWT 做登录,还能举一反三,用它来处理 API 鉴权、单点登录等更多场景。

相关推荐
用户921080262861 小时前
ChatMessageList 消息列表组件:基于 BubbleList 搭建 AI 对话展示区
前端
HjhIron1 小时前
实战 React + JWT + Zustand:从零搭建登录鉴权系统
前端
晴天161 小时前
DeepSeek Harness 全景技术解析-Day23
前端·deepseek
风月说与山鬼1 小时前
三、uni-app页面配置(pages.json)
前端·uni-app
daols882 小时前
vue 实现基于 vxe-table 构建多维度产品对比表
前端·javascript·vue.js
前端 贾公子2 小时前
第08章:中间件(5)
服务器·前端·javascript
tech_zjf2 小时前
当 AI 把 Next.js Route 越写越快:我为什么做了 next-route-kit
前端·后端
常宇佳2 小时前
vue3 @代指src路径设置
前端·typescript·vue
砚凝霜3 小时前
软考网络工程师|案例分析:Eth‑Trunk 链路聚合、iStack 堆叠、CSS 集群核心考点总结
前端·css·网络