JWT 基础
JWT(JSON Web Token)是三段用 . 拼接的 Base64 字符串:
eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyMSIsIm5hbWUiOiJ6aGFuZyJ9.xK3m...
|------- Header -------|-------------- Payload ------------|--- Signature ---|
三段都不是加密的,只是 Base64 编码,任何人 atob() 就能看到内容。
- Header:声明算法和类型,如
{"alg": "HS256", "typ": "JWT"} - Payload:放业务数据,如
{"sub": "user1", "name": "zhang"}。只是编码不是加密,敏感信息不能放 - Signature:用服务端密钥对前两段做 HMAC-SHA256 签名,防篡改但防不了窃取
JWT vs Session
| JWT | Session | |
|---|---|---|
| 状态 | 无状态,服务端不存数据 | 有状态,服务端存 session |
| 扩展 | 天然支持多服务器 | 多服务器需要 Redis 等共享存储 |
| 安全 | Token 泄露无法主动失效 | 服务端可随时销毁 session |
| 适用 | API、微服务、移动端 | 传统 Web 应用 |
完整流程:登录到请求校验
场景:用户 zhang 登录,然后访问受保护接口 GET /auth/ping。
阶段一:登录
步骤 1 — 用户发送用户名密码
POST /auth/login
{"usernameOrEmail": "zhang", "password": "xxx"}
步骤 2 — 服务端验证密码通过(scrypt 比对哈希),然后做两件事:
2a. 签发 JWT:
Header: {"alg":"HS256","typ":"JWT"} → Base64 → eyJhbGci...
Payload: {"sub":"abc123","name":"zhang"} → Base64 → eyJzdWIi...
Signature: HMAC-SHA256(密钥, "Header.Payload") → Base64 → xK3m...
拼起来:eyJhbGci.eyJzdWIi.xK3m...
2b. 生成 MAC Key:
随机生成 32 字节密钥 → mac_key: "aBcDeFg..."
随机生成 key_id → mac_key_id: "V1StGXR8"
存进数据库,绑定到这个用户
步骤 3 — 返回给前端
{
"access_token": "eyJhbGci.eyJzdWIi.xK3m...",
"mac_key": "aBcDeFg...",
"mac_key_id": "V1StGXR8",
"mac_alg": "HMAC-SHA256",
"expires_in": 3600
}
前端把这些存到 sessionStorage。
阶段二:访问受保护接口
步骤 4 — 前端想请求 GET /auth/ping,先组装签名原文:
GET ← 请求方法
/auth/ping ← 请求路径
1744694400 ← 当前时间戳
rAnDoM123 ← 随机 nonce
← body hash(GET 没有 body,空字符串)
拼起来用 \n 连接:
"GET\n/auth/ping\n1744694400\nrAnDoM123\n"
步骤 5 — 用 MAC Key 对上面的字符串做 HMAC-SHA256 签名:
HMAC-SHA256(mac_key: "aBcDeFg...", 明文: "GET\n/auth/ping\n...") → "XyZ789..."
步骤 6 — 发请求,带两样东西:
GET /auth/ping
Authorization: Bearer eyJhbGci.eyJzdWIi.xK3m... ← JWT(证明身份)
X-MAC: XyZ789... ← HMAC 签名(证明请求完整)
X-MAC-KID: V1StGXR8 ← 告诉服务端用哪个 key
X-TS: 1744694400 ← 时间戳
X-Nonce: rAnDoM123 ← 随机数
X-Body-Hash: ← body 哈希
阶段三:服务端校验
步骤 7 — 第一层:验 JWT
收到 Authorization 头 → 取出 JWT
→ jwt.verify("eyJhbGci.eyJzdWIi.xK3m...", 密钥)
→ 签名合法 + 没过期 → 解出 {"sub":"abc123","name":"zhang"}
→ 知道这是 zhang
步骤 8 — 第二层:验 HMAC
用 X-MAC-KID "V1StGXR8" 查数据库 → 找到 mac_key "aBcDeFg..."
→ 确认这个 key 确实属于 zhang(不是别人的)
用同样的规则重新拼签名字符串:
"GET\n/auth/ping\n1744694400\nrAnDoM123\n"
→ HMAC-SHA256(mac_key, 签名字符串) → 算出预期签名
对比预期签名 和 请求里的 X-MAC:
一致 → 请求没被篡改,放行
不一致 → 有人改了请求参数,拒绝
步骤 9 — 额外安全检查
时间戳:|当前时间 - 1744694400| > 120秒?→ 拒绝(过期请求)
Nonce: "rAnDoM123" 之前见过?→ 拒绝(重放攻击)
全部通过 → 返回 Pong! Hello zhang (abc123)
为什么需要两层
| JWT | HMAC 签名 | |
|---|---|---|
| 保护的是 | 用户身份(谁在请求) | 请求内容本身(参数有没有被改) |
| 粒度 | 一个 Token 管所有请求 | 每个请求单独签名(含 body hash) |
| 防什么 | 伪造身份 | 篡改请求参数 |
JWT 证明”你是 zhang”,HMAC 证明”这个 GET /auth/ping 请求确实是你发的、没人改过”。
这是 AWS 风格的请求签名思路,比单用 JWT 安全得多。
JWT 的安全注意事项
- SECRET 不能硬编码:生产环境必须用环境变量
- Payload 是明文可读:密码等敏感信息绝不能放
- Token 无法主动失效:除非加黑名单机制,否则签发后只能等过期
- 单靠 JWT 不防重放:需要配合时间戳 + nonce
面试自测题
Q1:JWT 的三段分别叫什么?哪段是加密的?
答: Header : Payload : Signature,三段都是 Base64 编码不是加密。前两段是明文,任何人都能解码看到内容;最后一段是服务端用密钥对前两段做的 HMAC 签名,只能防篡改,防不了窃取。
Q2:用户拿到 JWT 后访问受保护接口,服务端需要查数据库验证这个用户吗?
答: 不需要。JWT 是无状态的,服务端只需用密钥验证 Signature 是否合法、是否过期,就能确认用户身份,不需要查数据库。
Q3:攻击者截获了用户的 JWT 直接拿去发请求,能用吗?怎么防范?
答: 能用,这叫重放攻击(replay attack)。单靠时间戳不够——在有效期内(如 1 小时)截获的 Token 照样能重放。完整方案是时间戳 + nonce:时间戳限制请求窗口期(如 2 分钟),nonce 保证同一个请求只能用一次。
Q4:登录成功后返回 JWT 和 MAC Key,各有什么用?为什么不只用一个?
答:
- JWT:证明”你是谁”(用户身份),一个 Token 管所有请求,防伪造身份
- HMAC Key:用于对每个请求单独签名(方法 + 路径 + 时间戳 + nonce + body hash),证明”这个请求没被篡改”,防篡改请求参数
JWT 是粗粒度的身份凭证,HMAC 是细粒度的请求完整性校验。两层配合是 AWS 风格的请求签名思路。
Q5:JWT_SECRET 泄露后攻击者能做什么?光换密钥够吗?
答:
- 攻击者能伪造任意用户的 JWT(冒充任何人登录,不需要密码)
- 但光有 JWT 还不够——第二层 HMAC 校验仍需要对应的 MAC Key 才能通过
- 光换密钥不够:换密钥后所有已签发的 JWT 立刻失效,所有合法用户也被迫重新登录;而且攻击者之前截获的 JWT + MAC Key 组合在 1 小时有效期内仍可利用
- 正确做法:换密钥 + 清空所有 MAC Key + 通知用户重新登录;长期应做密钥轮换(新旧密钥过渡期)