跳到内容

JWT 安全最佳实践|alg none 攻击 / 过期 / 签名验证

分类:认证·安全

JWT(JSON Web Token)是现代 Web API 身份验证的事实标准,但在误解规范的情况下实现它很容易引入漏洞。本文解释了常见的 JWT 误用模式和避免这些模式的最佳实践。

JWT 结构回顾

JWT 是一种用点连接三个 Base64URL 编码字符串的格式。

eyJhbGci...    .   eyJzdWIi...     .    SflKxwRJSM...
   header              payload                signature
  • header:签名算法(alg)和令牌类型(typ
  • payload:声明(sub / iss / aud / exp / iat / ...)的 JSON 对象
  • signature:对 header 和 payload 的 HMAC / RSA / ECDSA 签名

您可以使用 DevLab 的 JWT 解码器验证 JWT 的内容。请记住 Base64URL 是编码而不是加密,因此有效负载内容可以被任何人读取,而无需服务器签名验证。

威胁 1: alg=none 攻击

JWT 规范包括 "alg": "none",一个"无签名"算法指定。如果攻撃者利用这个用 {"alg":"none"} 头修改有效负载,并且库原样接受它,就会成为允许冒充任何用户的严重漏洞

对策:

  • 在验证时明确地将允许的算法列入白名单
  • jwt.verify(token, secret, { algorithms: ['HS256'] }) 一样作为数组传递
  • 2015年左右的库存在漏洞,但当前的主流库已经进行了修复。不过,除非自己开发,否则应该始终使用最新版本。
// ✗ 悪い例 (アルゴリズム未指定 = ライブラリが alg ヘッダを信頼)
jwt.verify(token, secret);

// ✓ 良い例 (アルゴリズムを固定)
jwt.verify(token, secret, { algorithms: ['HS256'] });

威胁 2: 密钥混同攻击 (key confusion)

拥有 RSA 公钥的攻击者可以通过 将算法从 RS256 改为 HS256 来强制将公钥作为"秘密密钥"处理。如果 JWT 库在选择验证函数时信任 alg 字段,用公钥签署的 HMAC 伪造令牌将被接受。

对策:必须在验证端固定算法(与威胁 1 相同的对策)。此外,还要明确区分密钥类型:

  • 对于 HS256,以 Buffer 形式传递
  • 对于 RS256,以 PEM 格式传递公钥

威胁 3: 未验证的过期时间 / 无期限令牌

exp JWT 中的声明以 UNIX 秒表示过期时间,但在验证期间经常被忽略。如果一旦颁发的令牌可以无限期使用,则无法遏制泄露造成的损害。

对策:

  • 发布时将exp设置为较短的期限(访问令牌建议设置为15分钟到1小时)
  • 验证时必须检查 exp (主要库自动执行)
  • 对于长期会话,使用刷新令牌模式(短期访问令牌+长期刷新令牌+服务器端撤销列表)

威胁 4: 在 JWT 中存储敏感信息

JWT 有效负载只是 Base64 编码,任何人都可以对其进行解码。尽管如此,将密码或信用卡号放在有效负载中的实现仍在继续出现。

对策:

  • payload 中仅包含表示「这是该用户」的最少识别信息 (sub / user_id / role)
  • 将敏感信息存储在服务器端数据库中,从 JWT 使用 user_id 检索
  • 如果必须通过JWT发送敏感信息,请使用 JWE (加密JWT)

威胁 5: 无法撤销 (logout)

当 JWT 以无状态方式颁发时,无法撤销。即使用户注销,由于服务器没有令牌信息,令牌"可以使用到过期"。即使更改密码,已颁发的 JWT 仍然有效。

对策:

  • 使用短的 exp (15分钟或更少) 来最小化损害窗口
  • 登出时将 jti (JWT ID) 注册到服务器端的 失效列表 (denylist),验证时进行对照
  • 对于重要事件(密码更改、权限更改),将 token_version 存储在用户记录中,并在验证时确认匹配

威胁 6:弱秘密

如果 HS256 密钥太短,可以通过暴力破解。特别是像 "secret""password123" 这样的字符串非常脆弱。

对策:

  • HS256 应使用至少 256 位 (32 字节) 的随机值
  • 使用openssl rand -base64 32密码生成工具生成
  • 绝对不要向 Git 提交秘密。用环境变量或 Secrets Manager 管理

最佳实践总结

  1. 在验证端固定算法 (algorithms: ['HS256'])
  2. HS256 密钥应为 256 位或更长,RS256 应使用 2048 位或更长的 RSA
  3. 访问令牌的 exp 是15分钟到1小时
  4. 使用刷新令牌模式实现长期会话
  5. 不要在 payload 中包含敏感信息(Base64 不是加密)
  6. 登出时在失效列表中注册 jti
  7. 用环境变量或 Secrets Manager 管理秘密,严禁提交
  8. 在客户端将JWT存储在HttpOnly Cookie中而不是localStorage中(XSS防护措施)

用于调试的有用工具

总结

JWT 如果使用正确,是一个方便的身份验证令牌,但如果不了解规范陷阱和攻击模式,则会冒着向生产系统引入漏洞的风险。了解本文提出的 6 个威胁和对策,使用最新版本的 JWT 库作为基础。我们建议定期审查您的实现并监控安全公告。

❓ 常见问题

JWT 应该存放在浏览器的什么位置?
避免 localStorage——XSS 一旦得手就能立即读取。默认答案是使用带 HttpOnly、Secure、SameSite 的 Cookie。改用 Cookie 会引入 CSRF,但 SameSite=Lax 加上对状态变更请求的令牌校验即可封堵。并不需要在两种风险之间二选一。
alg: none 攻击如今还有现实威胁吗?
主流库现在默认拒绝,但只要你自己写代码从头部读取 alg 再据此挑选校验器,它就会复活。修法很简单:在校验时固定服务端期望的算法,绝不信任令牌自称的 alg。同一条规则也能防止把 RS256 公钥递给期望 HS256 之处的混淆。
把有效期设得很短会导致频繁登出。
标准做法是双令牌:短寿命的访问令牌(几分钟到十几分钟)加上长寿命的刷新令牌。访问令牌无法吊销,因此靠寿命自然失效;刷新令牌保存在服务端,随时可以作废。正是这种分离,才让登出或权限变更能够立即生效。