JWT 完全解析 —— 从原理到实践,掰开了揉碎了讲清楚

前言

最近在学习 FastAPI 时遇到了登录认证的问题,接触到了 JWT 这个概念。说实话刚看到 JWT 时一头雾水:它是什么?为什么登录要用它?跟 session 有什么区别?里面的那一长串乱码到底藏了什么秘密?

这篇文章就是我对 JWT 的完整学习笔记,力求掰开了、揉碎了,把每一个知识点都讲清楚。


目录

  1. JWT 是什么?
  2. 为什么需要 JWT?—— 从 Session 的痛点说起
  3. JWT 长什么样?
  4. JWT 的三部分:Header · Payload · Signature
  5. JWT 的工作流程(核心逻辑)
  6. JWT 的签名是怎么保证安全的?
  7. JWT 的优缺点
  8. JWT 在 FastAPI 中的实战
  9. 常见安全问题与防范
  10. 总结与脑图

1. JWT 是什么?

JWT 全称 JSON Web Token,读作 “jot”(/dʒɒt/)。

官方的定义是:一种紧凑的、URL 安全的、用于在双方之间传递声明(claims)的令牌格式。

翻译成人话就是:

JWT 是一段有结构的字符串,里面可以安全地存放一些信息(比如用户 ID、过期时间),而且这段字符串本身就可以证明”信息没有被篡改过”。

它有三个核心特点:

  • 自包含(Self-contained):所有需要的信息都在 token 本身里,不需要查数据库来验证
  • 紧凑(Compact):体积小,可以放在 URL 参数、HTTP Header、Cookie 中
  • URL 安全:经过 Base64URL 编码,不含特殊字符

2. 为什么需要 JWT?—— 从 Session 的痛点说起

要理解 JWT 的价值,先得知道它解决了什么问题。

传统 Session 认证的工作方式

1
2
用户登录 → 服务器创建 Session → 把 Session ID 存到 Cookie 返回给浏览器
用户下次请求 → 浏览器自动带上 Cookie(含 Session ID)→ 服务器查 Session 数据库 → 找到用户信息

Session 的痛点

问题 说明
有状态 服务器必须保存 Session 信息,通常存数据库或内存
扩展性差 多台服务器部署时,Session 需要共享(redis 或粘性会话),增加架构复杂度
跨域困难 Cookie 受同源策略限制,API 给移动端用就很麻烦
CSRF 风险 Cookie 自动携带的特性容易遭受跨站请求伪造攻击

JWT 如何解决这些问题

1
2
用户登录 → 服务器签发一个 JWT → 返回给前端
用户下次请求 → 前端手动把 JWT 放在 Header 中 → 服务器验证 JWT 签名 → 直接读取用户信息

关键区别:服务器不需要存储 JWT。所有信息都在 token 里,验证签名即可确认 token 的真实性。

这就带来了几个好处:

  • 无状态:服务器不存 session,水平扩展变得极其简单
  • 跨域友好:前端手动传 token,不受同源策略限制
  • 适合分布式:任何一台服务器只要知道密钥,就能验证 token

3. JWT 长什么样?

一个真实的 JWT 长这样:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

是不是看着像乱码?别怕,拆开来看就清楚了。

它由三部分组成,用 . 分隔:

1
[Header].[Payload].[Signature]

也就是上面那个字符串对应:

1
2
3
4
5
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9   ← Header
. ← 分隔符
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ ← Payload
. ← 分隔符
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c ← Signature

每一部分都是 Base64URL 编码的,解码后就能看到真实内容。


4. JWT 的三部分:Header · Payload · Signature

4.1 Header(头部)

Header 通常由两部分信息组成:

1
2
3
4
{
"alg": "HS256", // 签名算法,最常用的是 HS256(HMAC-SHA256)
"typ": "JWT" // token 类型,固定为 JWT
}
  • alg 告诉验证方:”我是用 HS256 算法签名的”
  • typ 告诉解析方:”这是一个 JWT”

这个 JSON 经过 Base64URL 编码,就是 JWT 的第一段。

4.2 Payload(载荷)

Payload 是存放实际信息的地方。这些信息被称为 claims(声明)。

Claims 分为三种:

注册声明(Registered Claims)

预定义的、建议使用的标准字段:

字段 全称 含义
iss Issuer 签发者
sub Subject 面向的用户
aud Audience 接收方
exp Expiration Time 过期时间(时间戳)
nbf Not Before 生效起始时间
iat Issued At 签发时间
jti JWT ID 唯一标识

公共声明(Public Claims)

可以自定义的字段,为避免冲突建议在 IANA JSON Web Token Registry 注册或使用命名空间。

私有声明(Private Claims)

双方约定的自定义字段,比如:

1
2
3
4
5
6
7
{
"sub": "1234567890",
"name": "张三",
"role": "admin",
"iat": 1516239022,
"exp": 1516242622
}

⚠️ 特别注意:Payload 只是 Base64URL 编码,不是加密!任何人都可以解码看到里面的内容。所以千万不要在 Payload 中存放密码、密钥等敏感信息。

4.3 Signature(签名)

签名是 JWT 安全性的核心。它的生成过程:

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret_key
)

简单说就是:

  1. 把编码后的 Header 和 Payload 用 . 拼起来
  2. 用指定的算法(比如 HMAC-SHA256)对这个字符串做一次带密钥的哈希运算
  3. 得到的结果就是签名

签名的意义:由于签名需要密钥才能生成,如果 token 被篡改(比如把 roleuser 改成 admin),重新计算签名会不匹配,验证就会失败。


5. JWT 的工作流程(核心逻辑)

理解了各部分之后,来看看 JWT 的完整生命周期。

5.1 签发阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
     ┌─────────────┐
│ 用户登录 │
│ 账号+密码 │
└──────┬──────┘

┌─────────────┐
│ 服务器验证 │
│ 用户名和密码 │
└──────┬──────┘
▼ (验证通过)
┌───────────────────────┐
│ 服务器生成 JWT: │
│ 1. 构造 Header │
│ 2. 构造 Payload │
│ 3. 用密钥签名 │
└──────────┬────────────┘

┌───────────────────────┐
│ 返回 JWT 给前端 │
│ (JSON 响应/Cookie) │
└───────────────────────┘

5.2 验证阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
     ┌─────────────┐
│ 前端请求 API │
│ Header 带 JWT│
└──────┬──────┘

┌───────────────────────┐
│ 服务器解析 JWT: │
│ 1. 拆解三部分 │
│ 2. 重新计算签名 │
│ 3. 对比签名是否一致 │
└──────────┬────────────┘

┌───────────────────────┐
│ 验证通过? │
│ YES → 读取 Payload │
│ 获取用户信息 │
│ NO → 返回 401 │
└───────────────────────┘

5.3 核心逻辑总结

一句话概括:

服务器签发一个带签名的”通行证”给你,你每次来都带着它,服务器只需检查签名就知道这通行证是不是它发的、有没有被改过。

关键是服务器不存任何状态——这就是所谓的 Stateless(无状态)


6. JWT 的签名是怎么保证安全的?

这是初学者最容易困惑的地方。我用一个类比来解释。

印章类比

想象你是一家公司的老板(服务器),你有一枚私人印章(密钥)。

1
2
3
4
5
6
7
你写了一张字条:"张三,级别:管理员"
然后盖上了你的私人印章 → 相当于 JWT 的签名

张三拿着这张字条来办事
前台(服务器)看到字条上的印章
拿出你的印章样本来对比 → 相当于用同一个密钥重新计算签名
印章一致 → 没问题,放行! → 签名验证通过

如果有人把字条改成”李四,级别:超级管理员”:

1
2
3
他没有你的私人印章                        → 没有密钥
伪造的印章跟你真的不一样 → 签名不匹配
前台一眼识破,拒绝放行 → 验证失败

关键技术点

  • 对称算法(HS256/HMAC):签发和验证用同一个密钥。简单但需要保护好密钥。
  • 非对称算法(RS256/ES256):签发用私钥,验证用公钥。安全性更高,适合多方验证的场景。
特性 HS256(对称) RS256(非对称)
密钥数量 1 个(对称密钥) 2 个(私钥 + 公钥)
计算速度
密钥分发 必须保密 公钥可以公开
适用场景 单一服务内部 微服务/第三方认证

7. JWT 的优缺点

优点 ✅

优点 说明
无状态、易扩展 服务器不需要存储 token,水平扩展只需保证密钥一致
跨域友好 不依赖 Cookie,前端可以自由在多个域名间传递
自包含 Payload 里存了用户信息,减少数据库查询
移动端友好 App 不需要 Cookie 支持,存 token 即可
性能好 验证只需一次签名计算,不需要查库
标准化 RFC 7519 标准,生态成熟,几乎所有语言都有实现

缺点 ❌

缺点 说明
不可撤销 签发后无法主动让 token 失效(除非维护黑名单——但这又回到有状态了)
Payload 不加密 敏感信息不能放进去,放了等于明文传输
体积较大 比 Session ID 长很多,每次请求都带着会增加带宽开销
过期时间难抉择 太短 → 用户频繁重新登录;太长 → 泄露风险高
密钥保护责任重大 密钥泄露 = 任何人都可以签发有效的 JWT
无状态也是无审计 你无法知道某个 token 是什么时候撤销的、被谁使用了

对比 Session

维度 Session JWT
存储位置 服务器 客户端
扩展性 差(需共享 Session) 好(无状态)
即时失效 可以(删 Session) 难(维护黑名单)
跨域 麻烦 简单
安全性 CSRF 风险 XSS 风险(token 被偷则全丢)

8. JWT 在 FastAPI 中的实战

理论说完了,来看一个 FastAPI 中使用 JWT 的真实例子。

8.1 安装依赖

1
2
3
pip install python-jose[cryptography]  # JWT 生成与验证
pip install passlib[bcrypt] # 密码哈希
pip install fastapi uvicorn # FastAPI 框架

python-jose 是 FastAPI 官方文档推荐的 JWT 库。passlib 用于安全地存储密码(永远不要存明文密码!)。

8.2 完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
from datetime import datetime, timedelta, timezone
from typing import Optional

from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from jose import JWTError, jwt
from passlib.context import CryptContext

# ====================
# 配置
# ====================
SECRET_KEY = "your-secret-key-here-must-be-very-long-random-string"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# 密码哈希工具
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

# 安全方案 —— 告诉 FastAPI 从 Authorization Header 取 token
security = HTTPBearer()

app = FastAPI()

# ====================
# 工具函数
# ====================

def verify_password(plain_password: str, hashed_password: str) -> bool:
"""验证明文密码和哈希密码是否匹配"""
return pwd_context.verify(plain_password, hashed_password)

def hash_password(password: str) -> str:
"""对密码进行哈希"""
return pwd_context.hash(password)

def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
"""
签发 JWT —— 核心函数
"""
to_encode = data.copy()

# 设置过期时间
if expires_delta:
expire = datetime.now(timezone.utc) + expires_delta
else:
expire = datetime.now(timezone.utc) + timedelta(minutes=15)

to_encode.update({"exp": expire}) # exp 是 JWT 标准字段
to_encode.update({"iat": datetime.now(timezone.utc)}) # 签发时间

# 签名生成 JWT
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt

def verify_token(token: str) -> dict:
"""
验证 JWT —— 核心函数
验证成功返回 Payload,失败抛出异常
"""
try:
# decode 会同时做三件事:
# 1. 拆解 token
# 2. 验证签名(用 SECRET_KEY 重新计算签名并对比)
# 3. 验证过期时间(exp)
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload
except JWTError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="无效的认证凭证",
headers={"WWW-Authenticate": "Bearer"},
)

# ====================
# 模拟用户数据(生产环境请用数据库)
# ====================
fake_users_db = {
"zhangsan": {
"username": "zhangsan",
"password": hash_password("123456"), # 存的是哈希,不是明文!
"role": "user",
}
}

# ====================
# API 接口
# ====================

@app.post("/login")
async def login(username: str, password: str):
"""
登录接口 —— 验证账号密码,签发 JWT
"""
# 1. 查找用户
user = fake_users_db.get(username)
if not user:
raise HTTPException(status_code=400, detail="用户名或密码错误")

# 2. 验证密码
if not verify_password(password, user["password"]):
raise HTTPException(status_code=400, detail="用户名或密码错误")

# 3. 生成 JWT
access_token = create_access_token(
data={
"sub": user["username"], # sub 标准字段,存用户标识
"role": user["role"], # 自定义字段,存角色
},
expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
)

return {
"access_token": access_token,
"token_type": "bearer"
}

@app.get("/users/me")
async def read_users_me(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""
获取当前用户信息 —— 需要 JWT 认证

流程:
1. FastAPI 从 Authorization Header 提取 token
2. 调用 verify_token 验证签名和过期时间
3. 返回用户信息
"""
token = credentials.credentials
payload = verify_token(token)

return {
"username": payload.get("sub"),
"role": payload.get("role"),
"msg": "认证通过,这是你的个人信息"
}

@app.get("/admin")
async def admin_only(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""
管理员专属接口 —— 演示基于角色的权限控制
"""
token = credentials.credentials
payload = verify_token(token)

if payload.get("role") != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="你没有管理员权限"
)

return {"msg": "欢迎管理员!"}

8.3 关键代码解析

签发 JWT (create_access_token)

1
2
3
4
5
6
7
8
9
10
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
to_encode = data.copy()
# 设置过期时间
expire = datetime.now(timezone.utc) + (expires_delta or timedelta(minutes=15))
to_encode.update({"exp": expire})
to_encode.update({"iat": datetime.now(timezone.utc)})

# 核心:jwt.encode() 负责做签名
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
return encoded_jwt

这里 jwt.encode() 帮我们做了所有底层工作:

  1. to_encode(Payload)转成 JSON
  2. 构造 Header {"alg": "HS256", "typ": "JWT"}
  3. 对 Header 和 Payload 做 Base64URL 编码
  4. 用密钥和指定算法计算签名
  5. 拼接成 Header.Payload.Signature 格式

验证 JWT (verify_token)

1
2
3
def verify_token(token: str) -> dict:
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
return payload

jwt.decode() 帮我们做:

  1. 拆解 token 的三部分
  2. 用 SECRET_KEY 重新计算签名,对比是否一致
  3. 检查 exp 字段是否过期
  4. 可选:检查 nbfissaud 等字段
  5. 如果一切 OK,返回 Payload

8.4 如何使用

1
2
3
4
5
6
7
# 1. 登录获取 JWT
curl -X POST "http://localhost:8000/login?username=zhangsan&password=123456"
# 返回: {"access_token": "eyJ...", "token_type": "bearer"}

# 2. 用 JWT 访问受保护的接口
curl -H "Authorization: Bearer eyJ..." http://localhost:8000/users/me
# 返回: {"username": "zhangsan", "role": "user", "msg": "认证通过,这是你的个人信息"}

9. 常见安全问题与防范

9.1 密钥泄露

问题:密钥被泄露 = 任何人都可以签发有效的 JWT。

防范

  • 密钥要用足够长、足够随机的字符串(至少 256 位)
  • 存入环境变量,不要硬编码在代码里
  • 定期轮换密钥
  • 使用密钥管理服务(如 AWS KMS、HashiCorp Vault)

9.2 Token 被盗

问题:XSS 攻击窃取存在 localStorage 的 token。

防范

  • 使用 HttpOnly Cookie 存储 token(不能通过 JS 读取)
  • 做好 XSS 防护(输入过滤、CSP 策略)
  • 使用短期 token + refresh token 机制
  • HTTPS 传输,防止中间人攻击

9.3 无法主动失效

问题:签发后的 JWT 在过期前无法主动撤销。

解决方案(几种常见做法):

方案 优点 缺点
短期 token + 刷新 token token 几分钟过期,危害有限 架构复杂一些
维护黑名单 可以在 Redis 存已撤销的 jti 回到有状态
版本号 用户密码修改时递增版本号,使旧 token 失效 每次请求都要查版本号
直接缩短过期时间 简单 用户频繁登录体验差

9.4 alg:none 攻击

问题:部分 JWT 库在实现时可能允许 alg: "none" 的 token(无签名)。

防范:在 decode 时显式指定允许的算法,不要接受 none

1
2
3
4
5
# ❌ 不安全 —— 可能接受 alg: none 的 token
payload = jwt.decode(token, options={"require": []})

# ✅ 安全 —— 明确指定算法
payload = jwt.decode(token, SECRET_KEY, algorithms=["HS256"])

9.5 算法混淆攻击

问题:攻击者把 algRS256 改成 HS256,然后用公钥(公钥是公开的)作为 HMAC 的密钥来签名。

防范:始终验证算法与预期一致,且服务端 decode 时绑定算法和密钥。

9.6 Too Much in Payload

问题:把密码、信用卡号等敏感信息放进 Payload。

防范:记住 Payload 只是编码不是加密。需要敏感信息?要么用 JWE(JSON Web Encryption)加密整个 JWT,要么只放非敏感信息。


10. 总结与脑图

一句话总结

JWT 就是一个带签名的 JSON 数据包,服务器用它来证明”你确实是你说的人”,而且不需要在服务器存任何东西。

核心记忆点

1
2
3
4
JWT = Header + Payload + Signature
│ │ │
声明算法 存放数据 防篡改保证
(HS256) (用户信息) (用密钥签名)

使用流程

1
2
3
4
5
6
7
8
9
10
11
12
登录 ──→ 服务器签发 JWT ──→ 前端存储

每次请求放 Header


服务器验证签名

┌───────┴───────┐
通过 不通过
│ │
读取 Payload 返回 401
处理请求 拒绝访问

适用场景

适合 不适合
分布式系统 / 微服务 需要即时撤销 token 的场景
单页应用(SPA) 对 token 大小敏感的物联网/低带宽场景
移动 App 超长会话(如”记住我”30天)
API 对外提供 高度敏感数据(银行、医疗)—— 需要额外加密
跨域 / 跨平台认证

继续学习的方向

  • OAuth 2.0 —— JWT 常作为 access token 用在 OAuth 2.0 中
  • Refresh Token 机制 —— 解决 JWT 过期问题的最佳实践
  • JWE(JSON Web Encryption) —— 对 JWT 整体加密
  • JWK(JSON Web Key) —— 使用公钥体系签名和验证
  • 双 token 模式 —— access token(短期)+ refresh token(长期)的组合方案

写在最后:JWT 不是银弹,它有自己的优势和局限。理解它的工作原理和适用场景,才能在项目中做出正确的技术选择。希望这篇文章能帮你彻底搞懂 JWT,在写 FastAPI 应用时能游刃有余!