Web安全——JWT

JWT 安全研究:从本质到攻防

一、引言

在现代 Web 应用的身份认证体系中,JWT(JSON Web Token)几乎成为事实上的标准。前端工程师喜欢它的"无状态",后端工程师喜欢它的"轻便",架构师喜欢它的"分布式友好"。然而,作为安全研究员,我们更需要问一个问题:JWT 本质上是什么,它真的安全吗?

从本质上说,JWT 不是加密技术,而是 一种自包含的、基于签名的身份凭证格式。这一点决定了它的安全边界和潜在风险。

二、JWT 的本质

要理解 JWT 的安全问题,首先必须抓住它的本质。JWT = Header + Payload + Signature

  1. Header(头部)
    描述 Token 的类型和所使用的签名算法,例如:
    { "alg": "HS256", "typ": "JWT" }
  2. Payload(载荷)
    携带用户信息和声明(claims),如用户 ID、角色、过期时间等:
    { "sub": "alice", "role": "admin", "exp": 1693918080 }
  3. 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 的安全设计原则

从安全研究角度,我们可以总结出以下设计与防御原则:

  1. 强密钥
    • 使用至少 256 位的随机密钥,避免字典爆破。
    • 定期更换密钥。
  2. 禁用 none 算法
    • 在服务端明确指定只接受特定算法(如 HS256 或 RS256)。
  3. 避免算法混淆
    • 如果使用非对称加密(RS256),严格区分公钥和私钥,不要让公钥参与 HMAC。
  4. 验证时间字段
    • 必须验证 exp、nbf、iat,避免过期或提前使用。
  5. 最小化 Payload 信息
    • 不要在 JWT 中放敏感数据(密码、银行卡、邮箱验证状态等)。
    • 仅放必要的 ID、角色等。
  6. 引入撤销机制
    • 可以维护黑名单(虽然牺牲"无状态"),或者缩短 JWT 的过期时间并配合 Refresh Token 机制。
  7. 传输安全
    • 必须使用 HTTPS 传输,防止中间人窃取 Token。
  8. 存储安全
    • 避免把 Token 存在 LocalStorage(容易被 XSS 窃取)。
    • 更推荐存放在 HttpOnly Cookie 中。

七、JWT 安全研究的本质认识

经过以上分析,我们可以回到 JWT 的"本质":

  • JWT 并不是一种加密机制,而是一个 被签名的 JSON 数据结构
  • 它的安全性依赖:
    1. 签名算法的正确实现
    2. 密钥的强度和保密性
    3. 开发者是否正确校验时间、算法等

换句话说:

  • JWT 自身并不保证安全,它只是一个便利的载体。
  • 真正决定安全性的,是开发者的实现与使用方式。
  • 在攻击者眼里,JWT 的价值在于:它是"单点突破"的目标,一旦拿到密钥或找到逻辑缺陷,整个系统将被完全接管。

八、结语

JWT 作为现代 Web 应用广泛使用的认证方式,其便利性无可替代。但从安全研究的视角来看,它的本质------"自包含的签名 JSON"------注定让它背负诸多风险。

它不像 Session 那样可控,而是把安全问题转移到"签名与密钥"上。

它的灵活性让开发者高效,但同时也给攻击者留下了丰富的攻击面。

因此,JWT 不应被视为一种"安全技术",而应被视为一种"便捷机制"。安全研究员需要始终提醒开发者:

  • 不要过度信任 JWT;
  • 不要把敏感数据放进 JWT;
  • 不要忽略签名验证的每一个细节。

最终,JWT 安全的核心并不是"JWT 本身",而是"开发者能否正确理解并使用它"。

PHP 的轻量级 JWT 演示项目

项目功能设计

  1. 登录页面
    • 用户输入用户名 + 密码
    • 验证成功后,生成一个 JWT 返回给用户
  2. 访问受保护页面
    • 用户访问时必须带 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(tokenParts0, '-', '+/')); payload = base64_decode(strtr(tokenParts1, '-', '+/')); signatureProvided = tokenParts2; // 重新计算签名 signature = hash_hmac('sha256', tokenParts0 . "." . 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 签发

|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| <?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:\\"; echo "\