JWT 安全最佳实践|alg none 攻击 / 过期 / 签名验证
JWT(JSON Web Token)是现代 Web API 身份验证的事实标准,但<strong>在误解规范的情况下实现它很容易引入漏洞</strong>。本文解释了常见的 JWT 误用模式和避免这些模式的最佳实践。
JWT 结构回顾
JWT 是一种用点连接三个 Base64URL 编码字符串的格式。
eyJhbGci... . eyJzdWIi... . SflKxwRJSM...
header payload signature
- <strong>header</strong>:签名算法(<code>alg</code>)和令牌类型(<code>typ</code>)
- <strong>payload</strong>:声明(sub / iss / aud / exp / iat / ...)的 JSON 对象
- <strong>signature</strong>:对 header 和 payload 的 HMAC / RSA / ECDSA 签名
您可以使用 <a href="/ja/tools/jwt/">DevLab 的 JWT 解码器</a>验证 JWT 的内容。请记住 Base64URL 是编码而不是<strong>加密</strong>,因此有效负载内容可以被任何人读取,而无需服务器签名验证。
威胁 1: alg=none 攻击
JWT 规范包括 <code>"alg": "none"</code>,一个"无签名"算法指定。如果攻撃者利用这个用 <code>{"alg":"none"}</code> 头修改有效负载,并且库原样接受它,就会成为<strong>允许冒充任何用户的严重漏洞</strong>。
<strong>对策:</strong>
- 在验证时<strong>明确地将</strong>允许的算法<strong>列入白名单</strong>
- 像 <code>jwt.verify(token, secret, { algorithms: ['HS256'] })</code> 一样作为数组传递
- 2015年左右的库存在漏洞,但当前的主流库已经进行了修复。不过,除非自己开发,否则应该始终使用最新版本。
// ✗ 悪い例 (アルゴリズム未指定 = ライブラリが alg ヘッダを信頼)
jwt.verify(token, secret);
// ✓ 良い例 (アルゴリズムを固定)
jwt.verify(token, secret, { algorithms: ['HS256'] });
威胁 2: 密钥混同攻击 (key confusion)
拥有 RSA 公钥的攻击者可以通过 <strong>将算法从 RS256 改为 HS256</strong> 来强制将公钥作为"秘密密钥"处理。如果 JWT 库在选择验证函数时信任 <code>alg</code> 字段,用公钥签署的 HMAC 伪造令牌将被接受。
<strong>对策:</strong>必须在验证端固定算法(与威胁 1 相同的对策)。此外,还要明确区分密钥类型:
- 对于 HS256,以 <code>Buffer</code> 形式传递
- 对于 RS256,以 PEM 格式传递公钥
威胁 3: 未验证的过期时间 / 无期限令牌
<code>exp</code> JWT 中的声明以 UNIX 秒表示过期时间,但<strong>在验证期间经常被忽略</strong>。如果一旦颁发的令牌可以无限期使用,则无法遏制泄露造成的损害。
<strong>对策:</strong>
- 发布时将<code>exp</code>设置为较短的期限(访问令牌建议设置为15分钟到1小时)
- 验证时必须检查 <code>exp</code> (主要库自动执行)
- 对于长期会话,使用刷新令牌模式(短期访问令牌+长期刷新令牌+服务器端撤销列表)
威胁 4: 在 JWT 中存储敏感信息
JWT 有效负载只是 Base64 编码,任何人都可以对其进行解码。尽管如此,将<strong>密码或信用卡号放在有效负载中</strong>的实现仍在继续出现。
<strong>对策:</strong>
- payload 中仅包含表示「这是该用户」的最少识别信息 (sub / user_id / role)
- 将敏感信息存储在服务器端数据库中,从 JWT 使用 user_id 检索
- 如果必须通过JWT发送敏感信息,请使用 <strong>JWE</strong> (加密JWT)
威胁 5: 无法撤销 (logout)
当 JWT 以无状态方式颁发时,无法撤销。即使用户注销,由于服务器没有令牌信息,令牌"可以使用到过期"。即使更改密码,已颁发的 JWT 仍然有效。
<strong>对策:</strong>
- 使用短的 <code>exp</code> (15分钟或更少) 来最小化损害窗口
- 登出时将 <code>jti</code> (JWT ID) 注册到服务器端的 <strong>失效列表 (denylist)</strong>,验证时进行对照
- 对于重要事件(密码更改、权限更改),将 <code>token_version</code> 存储在用户记录中,并在验证时确认匹配
威胁 6:弱秘密
如果 HS256 密钥太短,可以通过暴力破解。特别是像 <code>"secret"</code> 和 <code>"password123"</code> 这样的字符串非常脆弱。
<strong>对策:</strong>
- HS256 应使用至少 <strong>256 位 (32 字节)</strong> 的随机值
- 使用<code>openssl rand -base64 32</code>或<a href="/ja/tools/password/">密码生成工具</a>生成
- 绝对不要向 Git 提交秘密。用环境变量或 Secrets Manager 管理
最佳实践总结
- 在验证端固定算法 (<code>algorithms: ['HS256']</code>)
- HS256 密钥应为 256 位或更长,RS256 应使用 2048 位或更长的 RSA
- 访问令牌的 <code>exp</code> 是15分钟到1小时
- 使用刷新令牌模式实现长期会话
- 不要在 payload 中包含敏感信息(Base64 不是加密)
- 登出时在失效列表中注册 <code>jti</code>
- 用环境变量或 Secrets Manager 管理秘密,严禁提交
- 在客户端将JWT存储在HttpOnly Cookie中而不是localStorage中(XSS防护措施)
用于调试的有用工具
- <a href="/ja/tools/jwt/">JWT 解码器</a>:可视化 header / payload / signature,并附带每个 claim 的含义说明
- <a href="/ja/tools/jwt-sign/">JWT 生成・签名工具</a>: 使用 Web Crypto API 在浏览器中用 HS256 / HS384 / HS512 进行签名。方便生成测试令牌
- <a href="/ja/tools/password/">密码生成工具</a>:可用于生成 secret
总结
JWT 如果使用正确,是一个方便的身份验证令牌,但如果不了解规范陷阱和攻击模式,则会冒着向生产系统引入漏洞的风险。了解本文提出的 6 个威胁和对策,使用最新版本的 JWT 库作为基础。我们建议定期审查您的实现并监控安全公告。