JWT 安全研究:从本质到攻防
一、引言
在现代 Web 应用的身份认证体系中,JWT(JSON Web Token)几乎成为事实上的标准。前端工程师喜欢它的“无状态”,后端工程师喜欢它的“轻便”,架构师喜欢它的“分布式友好”。然而,作为安全研究员,我们更需要问一个问题:JWT 本质上是什么,它真的安全吗?
从本质上说,JWT 不是加密技术,而是 一种自包含的、基于签名的身份凭证格式。这一点决定了它的安全边界和潜在风险。
二、JWT 的本质
要理解 JWT 的安全问题,首先必须抓住它的本质。JWT = Header + Payload + Signature。
- Header(头部)
描述 Token 的类型和所使用的签名算法,例如:
{ "alg": "HS256", "typ": "JWT" } - Payload(载荷)
携带用户信息和声明(claims),如用户 ID、角色、过期时间等:
{ "sub": "alice", "role": "admin", "exp": 1693918080 } - Signature(签名)
用密钥和算法(如 HMAC-SHA256)对 Header 和 Payload 的组合进行签名,用来防止数据篡改。
这三个部分经过 Base64URL 编码后拼接为:header.payload.signature
1. 签名 ≠ 加密
一个常见的误区是“JWT 里的数据是安全的”。事实上,JWT 的 Payload 只是 Base64URL 编码,任何人都能解码查看。签名仅保证数据完整性,不能保证数据保密性。
2. 自包含的设计
JWT 的设计理念是“无状态”。服务端不再保存 Session,而是直接把身份和权限信息放进 Token。这样,任何一个服务只要能验证签名,就能确认用户身份。
这意味着:
- JWT 本质上是一个“不可更改的身份证”。
- 一旦有人能伪造 Token,就等于拿到了系统的万能钥匙。
三、JWT 与传统 Session 的对比
理解 JWT 的本质,还需要把它与传统 Session 机制做对比。
特性 |
Session |
JWT |
存储位置 |
服务端存储(Redis/DB/内存) |
客户端存储(Cookie/LocalStorage) |
状态管理 |
有状态,需查 Session ID |
无状态,直接验证签名 |
分布式支持 |
需共享 Session 数据 |
天然支持分布式 |
撤销机制 |
随时删除 Session ID |
难以撤销,必须等过期 |
泄露风险 |
Session ID 被窃取 |
JWT 泄露或伪造 |
可以看到:
- Session 把安全性寄托在“服务端掌控一切”上;
- JWT 把安全性寄托在“签名正确性”上。
换句话说,JWT 的安全核心在于:签名密钥必须安全、签名算法必须正确实现、验证逻辑必须严格。
四、JWT 的安全弱点(本质层面)
从安全研究视角,JWT 的“方便”与“灵活”恰恰也是它的攻击面。以下是本质上的安全弱点:
1. Payload 明文可见
JWT 的 Payload 可被任何人解码。例如:
eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJzdWIiOiAiYWxpY2UiLCAicm9sZSI6ICJhZG1pbiJ9.abc123
解码后立刻暴露用户身份和角色。如果开发者错误地把敏感数据(如密码、银行卡号)放进去,直接就是信息泄露。
2. 不可撤销性
JWT 一旦签发,服务端无法单独作废。
- 如果 Token 有效期为 1 小时,被窃取后攻击者可以在这一小时内随意冒充用户。
- 除非引入黑名单机制(破坏了“无状态”),否则无法解决。
3. 依赖签名的单点安全
JWT 的所有安全性都依赖签名:
- 如果 secret 太弱,攻击者可字典爆破。
- 如果验证逻辑有误,攻击者可能绕过签名。
- 如果支持多算法,攻击者可进行算法混淆攻击。
4. 算法灵活导致攻击面
JWT 设计时支持多种签名算法,结果导致以下漏洞:
- none 算法漏洞:如果服务端允许 alg=none,攻击者可伪造 Token。
- RS/HS 混淆攻击:如果服务端错误地把公钥当作 HMAC secret 使用,就可能导致伪造签名。
5. 过期机制脆弱
JWT 的 exp、nbf、iat 等字段只是数字。
- 一些开发者只验证签名,而忽略时间字段。
- 这导致攻击者能长期使用过期 Token。
五、JWT 常见攻击与案例
作为安全研究员,在渗透测试和代码审计中,常见的 JWT 攻击手法包括:
1. none 算法攻击
早期一些库支持 alg=none,意味着不进行签名验证。攻击者只要把 Header 改成:
{ "alg": "none", "typ": "JWT" }
并删除 Signature,就能伪造任意用户身份。
2. 弱密钥爆破
如果服务端使用简单的密钥(如 secret、123456),攻击者可以使用工具(如 jwt-cracker)进行字典爆破,快速找到签名密钥,从而伪造 Token。
3. RS/HS 混淆攻击
如果系统支持 RS256(非对称加密),攻击者可以把 alg 改成 HS256,并用公钥作为 HMAC 密钥计算签名。由于服务端错误实现,可能导致验证通过。
4. KID 注入
一些 JWT 实现支持 kid(Key ID)字段,用于指定哪个密钥验证 Token。攻击者可能通过目录遍历、SQL 注入等方式在 kid 上做文章,进而绕过签名验证。
5. 重放攻击
JWT 缺乏内置的防重放机制。如果攻击者截获了合法 Token,可以在其过期前无限使用,造成会话劫持。
六、JWT 的安全设计原则
从安全研究角度,我们可以总结出以下设计与防御原则:
- 强密钥
- 使用至少 256 位的随机密钥,避免字典爆破。
- 定期更换密钥。
- 禁用 none 算法
- 在服务端明确指定只接受特定算法(如 HS256 或 RS256)。
- 避免算法混淆
- 如果使用非对称加密(RS256),严格区分公钥和私钥,不要让公钥参与 HMAC。
- 验证时间字段
- 必须验证 exp、nbf、iat,避免过期或提前使用。
- 最小化 Payload 信息
- 不要在 JWT 中放敏感数据(密码、银行卡、邮箱验证状态等)。
- 仅放必要的 ID、角色等。
- 引入撤销机制
- 可以维护黑名单(虽然牺牲“无状态”),或者缩短 JWT 的过期时间并配合 Refresh Token 机制。
- 传输安全
- 必须使用 HTTPS 传输,防止中间人窃取 Token。
- 存储安全
- 避免把 Token 存在 LocalStorage(容易被 XSS 窃取)。
- 更推荐存放在 HttpOnly Cookie 中。
七、JWT 安全研究的本质认识
经过以上分析,我们可以回到 JWT 的“本质”:
- JWT 并不是一种加密机制,而是一个 被签名的 JSON 数据结构。
- 它的安全性依赖:
- 签名算法的正确实现
- 密钥的强度和保密性
- 开发者是否正确校验时间、算法等
换句话说:
- JWT 自身并不保证安全,它只是一个便利的载体。
- 真正决定安全性的,是开发者的实现与使用方式。
- 在攻击者眼里,JWT 的价值在于:它是“单点突破”的目标,一旦拿到密钥或找到逻辑缺陷,整个系统将被完全接管。
八、结语
JWT 作为现代 Web 应用广泛使用的认证方式,其便利性无可替代。但从安全研究的视角来看,它的本质——“自包含的签名 JSON”——注定让它背负诸多风险。
它不像 Session 那样可控,而是把安全问题转移到“签名与密钥”上。
它的灵活性让开发者高效,但同时也给攻击者留下了丰富的攻击面。
因此,JWT 不应被视为一种“安全技术”,而应被视为一种“便捷机制”。安全研究员需要始终提醒开发者:
- 不要过度信任 JWT;
- 不要把敏感数据放进 JWT;
- 不要忽略签名验证的每一个细节。
最终,JWT 安全的核心并不是“JWT 本身”,而是“开发者能否正确理解并使用它”。
PHP 的轻量级 JWT 演示项目
项目功能设计
- 登录页面
- 用户输入用户名 + 密码
- 验证成功后,生成一个 JWT 返回给用户
- 访问受保护页面
- 用户访问时必须带 JWT
- 服务端验证 JWT 的签名 + 过期时间
- 如果合法,显示用户信息,否则拒绝
项目结构
/jwt_demo
├── index.php # 登录表单
├── login.php # 登录处理 & JWT 生成
├── protected.php # 需要 JWT 才能访问的页面
├── jwt.php # JWT 工具函数(生成 & 验证)
代码实现
1. jwt.php —— JWT 工具函数
<?php // Secret key(生产环境要设置复杂的随机值) // 生成 JWT $signature = hash_hmac('sha256', $base64UrlHeader . "." . $base64UrlPayload, $secret, true); return $base64UrlHeader . "." . $base64UrlPayload . "." . $base64UrlSignature; // 验证 JWT $header = base64_decode(strtr($tokenParts[0], '-_', '+/')); // 重新计算签名 // 检查签名是否匹配 // 检查过期时间 return $payloadArray; |
2. index.php —— 登录页面
<!DOCTYPE html> |
3. login.php —— 登录处理 & JWT 签发
<?php // 模拟用户数据库 $username = $_POST['username']; if (isset($users[$username]) && $users[$username] === $password) { echo "登录成功!你的 JWT:<br><br>"; |
4. protected.php —— 受保护页面
<?php // 获取 token(通过 GET 或 Header) if (!$jwt) { $payload = verify_jwt($jwt, $secret); if ($payload) { |
运行流程
- 访问 http://127.0.0.1/jwt_demo/index.php
- 输入用户名 alice,密码 123456
- 登录成功 → 获得 JWT
- 点击链接访问 protected.php?token=xxxx
- 如果 token 正确且未过期 → 显示用户信息,否则报错
JWT 安全问题与攻击技术
1. none 算法漏洞
安全问题:
JWT Header 的 alg 字段被设置为 none 时,服务端可能跳过签名验证,Payload 可被任意篡改。风险包括任意权限提升和绕过认证机制。
攻击方式:
- 抓取合法 JWT Token
- 修改 Header 为 {"alg":"none","typ":"JWT"}
- 修改 Payload(如 "role":"admin")
- 删除 Signature 后发送给受保护接口
样本:
原 Token:
eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJ1c2VyIjogImFs
aWNlIiwgInJvbGUiOiAidXNlciJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
修改后的 Token:
eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJ1c2VyIjogImFs
aWNlIiwgInJvbGUiOiAiYWRtaW4ifQ.
示例:
使用 Burp Suite 拦截请求,替换 Token 并删除 Signature,即可直接访问 /admin 接口。
防御策略:
- 服务端明确禁止 none 算法
- 只接受指定算法(如 HS256 或 RS256)
- 使用安全库进行 JWT 验证,避免自实现签名逻辑
2. 弱密钥 / 字典爆破
安全问题:
JWT 使用简单或默认密钥(如 secret123),容易被攻击者通过字典或暴力破解伪造 Token。
攻击方式:
- 收集 JWT Token
- 使用字典或工具如 jwt-cracker 尝试生成与原 Token 相同 Signature
- 破解后生成任意 Payload Token
样本:
假设原 Token 使用密钥 secret:
Header: {"alg":"HS256","typ":"JWT"}
Payload: {"user":"bob","role":"user"}
攻击者用字典工具生成密钥:
密钥: secret
生成 Token: eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJ1c2Vy
IjogImJvYiIsICJyb2xlIjogImFkbWluIn0.DR7QmNbd6Ux5vXqC-6XgDbk5xvE
示例:
使用 jwt-cracker -t <token> -d wordlist.txt 破解,成功后发送生成的 Token 即可访问管理员接口。
防御策略:
- 使用高强度随机密钥(至少 256 位)
- 定期更换密钥
- 避免使用默认或常用字符串作为 JWT secret
3. 算法混淆攻击(RS/HS 混用)
安全问题:
服务端使用 RS256 验证,但逻辑错误允许攻击者将 Header alg 修改为 HS256 并使用公钥生成签名。
攻击方式:
- 抓取 RS256 Token
- 修改 Header 为 HS256
- 使用公钥生成 HMAC 签名
- 发送请求到接口
样本:
原 Token:
Header: {"alg":"RS256","typ":"JWT"}
Payload: {"user":"alice","role":"user"}
Signature: <RS256签名>
攻击者修改:
Header: {"alg":"HS256","typ":"JWT"}
Payload: {"user":"alice","role":"admin"}
Signature: <HMAC公钥生成的签名>
示例:
Python 演示:
import jwt
public_key = open('public.pem').read()
payload = {"user":"alice","role":"admin"}
token = jwt.encode(payload, public_key, algorithm="HS256")
print(token)
防御策略:
- 不允许客户端指定算法
- 严格区分对称和非对称密钥
- 使用成熟库校验 JWT
4. KID 注入漏洞
安全问题:
JWT Header 中的 kid(Key ID)用于指定验证密钥,如果服务端未严格校验,攻击者可指定任意密钥,伪造合法 Token。
风险:攻击者可冒充任意用户,包括管理员。
攻击方式:
- 修改 Header 中 kid 为攻击者控制的密钥路径或值
- 生成对应 Payload + Signature
- 发送给服务端验证
样本:
原 Token:
Header: {"alg":"HS256","typ":"JWT","kid":"key1"}
Payload: {"user":"bob","role":"user"}
Signature: <HS256签名>
攻击者修改:
Header: {"alg":"HS256","typ":"JWT","kid":"attacker_key"}
Payload: {"user":"bob","role":"admin"}
Signature: <使用attacker_key生成的签名>
示例:
Python 示例:
import jwt
attacker_key = "my_fake_key"
payload = {"user":"bob","role":"admin"}
header = {"alg":"HS256","typ":"JWT","kid":"attacker_key"}
token = jwt.encode(payload, attacker_key, algorithm="HS256", headers=header)
print(token)
防御策略:
- 严格验证 kid 是否在允许列表
- 不使用可控文件路径或用户输入作为密钥
- 对每个密钥使用独立管理,限制访问
5. 过期时间绕过
安全问题:
JWT 的 exp(过期时间)、nbf(生效时间)字段未严格验证,攻击者可使用过期或未生效 Token。
风险:会话劫持,长期访问受保护资源。
攻击方式:
- 修改 Payload 中 exp 或 nbf
- 使用抓取的 Token 重放请求
样本:
原 Payload:
{"user":"alice","role":"user","exp":1693700000}
攻击者修改:
{"user":"alice","role":"admin","exp":1893700000}
示例:
Python 演示:
import jwt
secret = "mysecret"
payload = {"user":"alice","role":"admin","exp":1893700000}
token = jwt.encode(payload, secret, algorithm="HS256")
print(token)
防御策略:
- 服务端必须验证 exp、nbf
- 使用短期 Token + Refresh Token
- 结合黑名单机制,支持 Token 撤销
6. Payload 明文信息泄露
安全问题:
JWT Payload 仅 Base64URL 编码,不加密,攻击者可直接查看敏感信息。
风险:泄露用户角色、ID、邮箱等,辅助其他攻击(横向渗透、社工)。
攻击方式:
- 抓取 Token
- Base64URL 解码 Payload
样本:
Token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYm9iIiwicm9sZSI6ImFkbWluIiwiZW1haWwiOiJib2JAbWFpbC5jb20ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
解码 Payload:
{"user":"bob","role":"admin","email":"bob@mail.com"}
示例:
Linux 命令:
echo 'eyJ1c2VyIjoiYm9iIiwicm9sZSI6ImFkbWluIn0' | base64 -d
防御策略:
- 最小化 Payload,避免存储敏感信息
- 对敏感信息使用加密或服务器端存储
- 使用 HTTPS 传输,防止抓包泄露
7. Token 重放
安全问题:
JWT 无内置防重放机制,攻击者可重复使用抓取的 Token。
风险:冒充用户访问接口,造成会话劫持。
攻击方式:
- 使用抓包工具获取合法 Token
- 重放 HTTP 请求至受保护接口
样本:
Token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UifQ.<Signature>
攻击者直接在请求 Header 中替换 Authorization 即可访问接口。
示例:
使用 curl 重放:
curl -H "Authorization: Bearer <token>" https://target.com/api/admin
防御策略:
- 引入 jti(JWT ID)或 nonce,实现一次性 Token
- 对短期 Token 使用黑名单
- 强化 HTTPS,防止抓包
8. Token 劫持(存储或传输不安全)
安全问题:
JWT 存储在 LocalStorage 或通过 HTTP 明文传输,容易被 XSS 或 MITM 攻击者窃取。
风险:攻击者可获取用户身份,执行非法操作。
攻击方式:
- 利用浏览器 XSS 漏洞获取 LocalStorage 中的 Token
- 或抓取 HTTP 明文 Token
样本:
// 获取 LocalStorage 中 Token
let token = localStorage.getItem("jwt_token");
console.log(token);
示例:
攻击者在浏览器控制台或通过恶意脚本获取 Token,并发送请求:
curl -H "Authorization: Bearer <stolen_token>" https://target.com/api
防御策略:
- 使用 HttpOnly + Secure Cookie 存储 Token
- 全站启用 HTTPS
- 防止 XSS,严格内容安全策略(CSP)
9. 签名算法降级
安全问题:
允许客户端指定算法,攻击者可指定已知弱算法(如 MD5、SHA1)进行伪造。
风险:绕过强算法验证,实现权限提升。
攻击方式:
- 修改 Header 中 alg 为弱算法
- 使用该算法生成签名 Token
样本:
Header:
{"alg":"HS1","typ":"JWT"}
Payload:
{"user":"alice","role":"admin"}
示例:
Python:
import jwt
secret = "weaksecret"
payload = {"user":"alice","role":"admin"}
token = jwt.encode(payload, secret, algorithm="HS1")
print(token)
防御策略:
- 服务端只接受强算法(HS256, RS256)
- 不允许客户端指定算法
- 使用成熟库进行签名验证
10. 过度依赖客户端验证
安全问题:
服务端未独立验证 Token,权限逻辑放在客户端,攻击者可修改 Payload 绕过前端判断。
风险:可访问受保护资源,造成权限提升。
攻击方式:
- 修改 Payload 中权限字段
- 直接调用 API 接口,无需前端限制
样本:
Payload:
{"user":"bob","role":"admin"}
示例:
Python:
import requests
token = "<modified_token>"
headers = {"Authorization": f"Bearer {token}"}
response = requests.get("https://target.com/api/admin", headers=headers)
print(response.text)
防御策略:
- 服务端独立验证 Token 和权限
- 不依赖前端逻辑
- 结合角色白名单、最小权限原则