🔐 JWT 登录鉴权原理与实战:从 Cookie/Session 到 Token

1 阅读9分钟

🔐 JWT 登录鉴权原理与实战:从 Cookie/Session 到 Token

学习日志 · 2026-08-20 本文基于 jwt-demo/login-demo 项目实战记录,手写一个完整的「签发 Token → 携带 Token → 校验 Token」闭环。


📑 目录

  1. 从一个问题说起:HTTP 为什么「记不住」你?
  2. 认证与授权:先分清这两个概念
  3. 传统方案:Cookie + Session 是怎么工作的
  4. JWT 是什么:一张三段式「身份证」
  5. jsonwebtoken 核心 API:sign 与 verify
  6. 实战解析一:登录接口签发 Token
  7. 实战解析二:受保护接口校验 Token
  8. 前端怎么带 Token:Authorization 头
  9. Cookie/Session vs JWT 对比
  10. 安全提醒与踩坑记录
  11. 总结

1. 从一个问题说起

HTTP 协议是 无状态(Stateless) 的。也就是说:

服务器默认不记得你上一次请求是谁。

每次请求到达后端,服务器只能看到「有一个客户端发来了一个请求」,它不知道这个客户端是张三、李四,还是刚刚登录过的那个用户。

![HTTP 无状态示意]

客户端                   服务器
  │  GET /pay            │
  │ ───────────────────► │  「你是谁?」
  │                      │
  │  GET /pay            │
  │ ───────────────────► │  「你还是没告诉我你是谁」

于是问题来了:登录之后,我怎么让服务器知道「接下来的请求是我发的」?

解决方案就是 凭证(Credential)

登录成功后,服务器颁发一张「凭证」给客户端;之后的每一次请求,客户端都主动把凭证带上,服务器验证凭证合法,就知道你是谁了。

而 JWT(JSON Web Token)就是目前最流行的一种凭证形式。本项目里的所有鉴权逻辑,都围绕这张「凭证」展开。


2. 认证与授权

先分清两个概念,很多面试题都在这挖坑:

概念英文回答的问题本项目体现
认证Authentication你是谁?(验明正身)登录接口校验用户名密码,签发 Token
授权Authorization你能干什么?(检查权限)校验 Token 里的 role: 'admin' 决定能否访问 Pay 页

💡 一句话记忆:认证是「验身份」,授权是「查权限」。本项目 Pay 页被路由守卫保护,本质就是「先认证(有 token 吗),再授权(够权限吗)」。


3. 传统方案:Cookie + Session

3.1 工作流程

1. 用户登录 → 服务器验证通过
2. 服务器在【内存/Redis】中创建一个 session 对象,并生成 sessionId
3. 通过 Set-Cookie 把 sessionId 交给浏览器
4. 浏览器之后的每次请求,都会自动带上 Cookie(含 sessionId)
5. 服务器拿 sessionId 去【内存/Redis】里查:这个会话是谁?

关键点:

  • 会话数据存在服务器端(内存或 Redis),客户端只存一个随机 ID。
  • Cookie 由浏览器自动携带,不需要前端手动塞进请求头。

3.2 痛点

session 方案最大的问题是:会话状态存在服务器内存中,不利于分布式

用户登录后 sessionId = "abc" 存在 Server A 的内存里
        │
        ▼
下次请求被负载均衡转发到了 Server B
        │
        ▼
Server B 内存里根本没有这个 session  → 用户「掉线」了

要解决就得引入 Redis 等集中式会话存储,还得分片、保证高可用,架构复杂度一下就上去了。

这正是 JWT 被设计出来的原因——把状态从服务器搬到客户端,让任何一台服务器都能独立校验


4. JWT 是什么

JWT(JSON Web Token)是一串经过 签名(Signature) 的、自包含的 JSON 信息载体。

4.1 三段式结构

一个 JWT 长这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzU1NjcwMDAwfQ.abcdefg_signature
└─────────── header ───────────┘└──────────────── payload ───────────────┘└────── signature ──────┘
内容说明
Header{ "alg": "HS256", "typ": "JWT" }声明签名算法与类型
Payload{ "username": "admin", "role": "admin", "iat": ... }身份信息(可自定义,JSON 表现力强)
SignatureHeader + Payload + 密钥 签名得到防篡改的核心

三段都用 Base64Url 编码,用 . 拼起来就是完整的 Token。

4.2 为什么是「单向操作」

签(sign)出来的 Token,无法被「解」回原 JSON —— 因为 payload 只是编码,不是加密。

签名 ≠ 加密,这是 JWT 最容易误解的点:

  • Payload 是 Base64 编码,任何人拿到 Token 都能 base64 解码看到内容(所以别把密码塞进去)。
  • Signature 由密钥签名,没有密钥的人无法伪造,改一个字符都会校验失败。

项目 readme 里那句「JSON 身份对象 →JWT(单向操作)→token 颁发给登陆者」,说的就是这个签名过程。


5. jsonwebtoken 核心 API

项目里 package.json 引入了 jsonwebtoken(服务端用,这里配合 vite-plugin-mock 在本地 mock 里模拟真实后端)。

5.1 sign —— 签发 Token

const token = jwt.sign(
  { username: body.username, role: 'admin' }, // payload:身份信息
  secret,                                     // 密钥(生产环境必须放环境变量)
  { expiresIn: 86400 }                        // 过期时间:86400 秒 = 24 小时
);
参数含义
第一个参数Payload,放非敏感的身份声明(username、role、userId...)
第二个参数密钥,签名与校验都用它,绝不能泄露
第三个参数选项,最常用 expiresIn 设置有效期

5.2 verify —— 校验 Token

const decoded = jwt.verify(token, secret);
// decoded = { username: 'admin', role: 'admin', iat: ..., exp: ... }
  • 校验签名是否合法(密钥对不上 → 抛异常)
  • 校验是否过期(超时 → 抛异常)
  • 成功返回解码后的 payload 对象,失败直接 throw,所以必须 try/catch

💡 sign 是「颁发」,verify 是「验证」,一签一验,配合密钥,组成了 JWT 的最小闭环。


6. 实战解析一:登录接口签发 Token

mock/user.js 里的 /api/login 接口,模拟真实后端的登录行为:

{
  url: '/api/login',
  method: 'POST',
  response: ({ body }) => {
    console.log(body);
    // 1. 校验账号密码(真实项目中查数据库)
    if (body.username !== 'admin' || body.password !== '123456') {
      return { code: -1, msg: '用户名或密码错误' };
    }
    // 2. 校验通过 → 用 jsonwebtoken 签发 Token
    const token = jwt.sign(
      { username: body.username, role: 'admin' },
      secret,
      { expiresIn: 86400 }
    );
    // 3. 把 Token 和用户信息返回给前端
    return {
      code: 0,              // 0 表示没有错误
      user: { username: body.username },
      token: token,          // 这就是那张「身份证」
    };
  }
}

完整流程串一遍:

前端提交 { username, password }
        │
        ▼
后端校验账号密码(admin / 123456)
        │ 通过
        ▼
后端用密钥签出 JWT(有效期 24h)
        │
        ▼
返回 { code:0, token, user } → 前端把 token 存到 localStorage

前端拿到后把 token 存进 localStorage(见 Zustand 篇的 setAuth),从此每请求都带着它。


7. 实战解析二:受保护接口校验 Token

/api/repo 接口模拟「需要登录才能访问」的受保护资源:

{
  url: '/api/repo',
  method: 'GET',
  response: (req) => {
    // 1. 请求头未带 token → 直接返回 401,避免下面 split 报错
    const auth = req.headers['authorization'];
    if (!auth) return { code: 401, msg: 'token 不存在' };

    // 2. 取出 "Bearer xxx" 中的 token 部分
    const token = auth.split(' ')[1];

    // 3. 验证 token(签名是否合法、是否过期)
    try {
      const decoded = jwt.verify(token, secret);
      return { code: 0, data: decoded };
    } catch {
      return { code: 401, msg: 'token 验证失败' };
    }
  }
}

几个细节值得记录:

  1. 先判空再 split:如果请求头没有 authorizationauth.split(' ') 会直接报 Cannot read properties of undefined,所以要先判空。这是一个典型的防御式写法。
  2. Bearer 前缀的剥取:前端带的是 Bearer <token>,后端用 auth.split(' ')[1] 把真正的 token 取出来。
  3. verify 会抛异常:签名被篡改、token 过期,都会 throw,必须用 try/catch 兜住,统一返回 401

🔒 这也侧面说明了 「前端守卫只是体验优化,真正的安全在后端」——前端路由可以随便改,但拿不到合法 Token 就永远拿不到 /api/repo 的数据。


8. 前端怎么带 Token

后端要验证,前端就得「每次请求都带上 token」。做法是在请求头里加:

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

本项目由 Axios 请求拦截器统一完成(详见 Axios 篇),核心一行:

config.headers['authorization'] = `Bearer ${token}`;

流程闭环如下:

登录成功 → token 存 localStorage
        │
        ▼
每次请求前,拦截器从 localStorage 取 token
        │
        ▼
拼成 "Bearer <token>" 塞进 Authorization 请求头
        │
        ▼
后端 verify(token, secret) → 解码出身份 JSON

至此,「你是谁」这个问题有了标准答案:请求头里那张可验证的 Token。


9. Cookie/Session vs JWT 对比

维度Cookie + SessionJWT
状态存储服务器内存/Redis客户端,自包含
分布式友好❌ 需要共享会话存储(Redis)✅ 任何服务器用同一密钥即可校验
扩展性差,Session 是共享状态好,天然水平扩展
请求开销小(只带 sessionId)稍大(Token 自含全部信息)
服务端主动注销✅ 删 Session 即可❌ Token 过期前无法主动作废(需黑名单)
安全Cookie 有 CSRF 风险Token 泄漏后有效期内存活;别放敏感信息
适用场景传统 Web,需要实时控制会话前后端分离、微服务、移动端

📌 没有银弹:JWT 解决的是 「分布式下的无状态鉴权」,代价是 「无法即时注销」。真实系统常配合「刷新令牌 + 黑名单」一起用。


10. 安全提醒与踩坑记录

⚠️ 踩坑 1:密钥别硬编码在代码里

// ❌ 现在 mock 里的写法(仅限学习演示)
const secret = 'fjkoijd';

// ✅ 生产环境:放环境变量
const secret = process.env.JWT_SECRET; // 强随机、足够长

⚠️ 踩坑 2:别往 Payload 塞敏感信息

Payload 只是 Base64 编码,不是加密。放密码、身份证号 = 裸奔。只放 userId、username、role 这类「可公开、可标识」的信息。

⚠️ 踩坑 3:Token 过期时间别设太长

  • expiresIn: 86400 = 24 小时,学习演示够用。
  • 生产环境建议 短生命周期 Access Token(15min~1h) + Refresh Token 续期,把 Token 泄露的「爆炸半径」压到最小。

⚠️ 踩坑 4:JWT 无法「踢人下线」

这是 JWT 与生俱来的短板。需要强会话控制(封号、改密码立即失效)时,要么引入 Redis 黑名单,要么干脆回到 Session 方案。


11. 总结

这次学习把 JWT 从「听过概念」到「跑通闭环」串了起来:

  1. HTTP 无状态 → 需要凭证标识身份;
  2. Cookie/Session 能解决,但状态在服务器,分布式不友好;
  3. JWT 把身份信息签进客户端自带的 Token,sign 签发、verify 校验,任何服务器都能独立验证;
  4. 本项目用 jsonwebtoken + vite-plugin-mock 在本地模拟了 登录签发 → 携带 → 校验 的完整链路;
  5. 前端用 Axios 拦截器统一携带 Authorization: Bearer <token>,路由守卫负责「没凭证就滚去登录」。

一句话总结:JWT 的本质是「把用户的身份证明打包成一段可验证、防篡改、自包含的 Token,然后让客户端每次请求都掏出来给服务器看」。