🔐 JWT 登录鉴权原理与实战:从 Cookie/Session 到 Token
学习日志 · 2026-08-20 本文基于
jwt-demo/login-demo项目实战记录,手写一个完整的「签发 Token → 携带 Token → 校验 Token」闭环。
📑 目录
- 从一个问题说起:HTTP 为什么「记不住」你?
- 认证与授权:先分清这两个概念
- 传统方案:Cookie + Session 是怎么工作的
- JWT 是什么:一张三段式「身份证」
- jsonwebtoken 核心 API:sign 与 verify
- 实战解析一:登录接口签发 Token
- 实战解析二:受保护接口校验 Token
- 前端怎么带 Token:Authorization 头
- Cookie/Session vs JWT 对比
- 安全提醒与踩坑记录
- 总结
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 表现力强) |
| Signature | 由 Header + 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 验证失败' };
}
}
}
几个细节值得记录:
- 先判空再 split:如果请求头没有
authorization,auth.split(' ')会直接报Cannot read properties of undefined,所以要先判空。这是一个典型的防御式写法。 - Bearer 前缀的剥取:前端带的是
Bearer <token>,后端用auth.split(' ')[1]把真正的 token 取出来。 - 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 + Session | JWT |
|---|---|---|
| 状态存储 | 服务器内存/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 从「听过概念」到「跑通闭环」串了起来:
- HTTP 无状态 → 需要凭证标识身份;
- Cookie/Session 能解决,但状态在服务器,分布式不友好;
- JWT 把身份信息签进客户端自带的 Token,
sign签发、verify校验,任何服务器都能独立验证; - 本项目用
jsonwebtoken+ vite-plugin-mock 在本地模拟了 登录签发 → 携带 → 校验 的完整链路; - 前端用 Axios 拦截器统一携带
Authorization: Bearer <token>,路由守卫负责「没凭证就滚去登录」。
一句话总结:JWT 的本质是「把用户的身份证明打包成一段可验证、防篡改、自包含的 Token,然后让客户端每次请求都掏出来给服务器看」。