< BACK_TO_INDEX

JWT + HMAC 双层认证机制

PUBLISHED:

JWT + HMAC 双层认证机制

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

JWTSession
状态无状态,服务端不存数据有状态,服务端存 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)


为什么需要两层

JWTHMAC 签名
保护的是用户身份(谁在请求)请求内容本身(参数有没有被改)
粒度一个 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 + 通知用户重新登录;长期应做密钥轮换(新旧密钥过渡期)