Utilo

如何使用JWT以及如何安全解密代码

了解JSON Web Tokens的结构,如何创建和验证签名,常见的索赔,安全陷以及如何解码JWT。

· 6 分钟阅读

JSON Web Token (JWT) 遍布各地:OAuth和OpenID Connect登录,API网关,微服务之间的单一登录,以及移动应用程序会话。它们也被广泛误解。开发人员解码一个令牌,看到可读JSON,并想知道这是否是安全问题;其他人将敏感数据存储在令牌中,假设它们是加密的。这篇文章解释了JWT是什么,如何验证,以及如何在不造成风险的情况下检查。

一个JWT的三个部分

一个JWT看起来像三个随机串连接的点:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsImV4cCI6MTkwMDAwMDAwMH0.Kx8...

每个部分都是Base64URL编码:

  1. 头条 JSON描述令牌类型和签名算法,例如{"alg":"HS256","typ":"JWT"}。
  2. 付费负载 JSON 包含索赔:关于用户和代币的声明,如 {"sub":"42","exp":1900000000}。
  3. 签名 从头部和有效载荷中计算的字节,使用秘密或私钥。

Base64URL是[Base64] (/blog/base64-explained) 的一种变体,它使用 - 和 _ 而不是 + 和 / 并放弃填充,因此可以在 URL 和标题中放置代币而不会逃脱。

关键是,头条和有效载荷只有编码,而不是加密。任何持有这个代币的人都能读到它们。粘贴一个令牌到一个JWT解码器 并且有效载荷会立即出现。

签名是如何工作的

签名是JWT值得信赖的。发行人把加密的头和有效载荷,通过点连接它们,

在HS256 (HMAC与SHA-256) 中,发行人和验证者共享一个密钥:

signature = HMAC-SHA256(secret, base64url(header) + "." + base64url(payload))

通过RS256或ES256,发行人使用私钥签名,任何人都可以使用匹配的公钥验证,通常在JWKS终端点 (如https://login.example.com/.well-known/jwks.json) 发布。当许多服务需要验证代币,但只有一个应该发行代币时,不对称算法更受欢迎。

当服务器接收一个令牌时,它会重新计算或验证签名。如果有效载荷中的一个字符改变了,例如攻击者编辑了"role":"user"到"role":"admin",签名不再匹配,令牌被拒绝。

您应该知道的登记索赔

JWT规范 (RFC 7519) 定义了标准索赔名称:

  • iss (发行人):谁创建了代币,例如https://login.example.com。
  • sub (主题):代币是关于谁的,通常是一个用户ID。
  • aud (观众):该代币是为谁设计的,例如api.example.com。
  • exp (到期时间):该代币必须被拒绝后的时间,以[Unix秒] ((/blog/unix-timestamp-explained)。
  • nbf (不是之前):令牌不有效之前的时间。
  • iat (发行时间):当代币创建时。
  • jti (JWT ID):用于撤销清单的唯一标识符。

申请添加自己的权利要求:scope,roles,tenant_id,email。让他们少;每一个要求都伴随着每一个请求。

正确验证一个令牌

服务器必须按照下列顺序检查:

  1. The algorithm is one you expect. Configure an allow-list such as ["RS256"]。永远不要让代币的头部自由决定;历史漏洞让攻击者切换到none或欺骗服务器使用公钥作为HMAC秘密。
  2. 签名是有效的 用正确的密钥,由kid从您所信任的密钥集中选择。
  3. exp在未来,nbf在过去,允许小时钟偏差3060秒。
  4. iss matches your identity provider exactly。
  5. aud包含您服务的标识符,因此另一个API发行的代币不能与您的代币进行重播。

使用一个维护的库 jose用于JavaScript,PyJWT用于Python,golang-jwt用于Go,而不是自己编写验证。

解码与验证

解码意味着Base64URL解码前两个部分并解析JSON。它不需要钥匙,也不证明其真实性。它可用于:

  • 为什么API返回401:aud是正确的?代币已经过期了吗?
  • 检查登录流实际授予哪些范围或角色。
  • 证实一个令牌更新产生了一个新的exp。

验证是指检查上述签名和权利要求。只有服务器才能做出授权决定,而且只有经过验证。

前端代码可能会解码一个令牌,以显示用户的名字或在exp之前安排更新,但它绝不能依赖解码的安全要求,因为用户可以修改他们的浏览器中运行的任何东西。

在浏览器中存储令牌的地方

没有完美的答案,只有权衡:

  • HttpOnly,Secure,SameSite cookie不能被JavaScript读取,这可以通过跨站点脚本来防止代币被盗。它们需要CSRF保护,尽管SameSite=Lax或Strict涵盖大多数情况。
  • 内存 (JavaScript变量) 是安全的,但在重新加载时会丢失;将其与HttpOnly Cookie中的刷新令牌结合起来。
  • localStorage是方便的,但可由页面上的任何脚本读取,因此一个XSS漏洞泄露每个代币。

对于大多数Web应用程序来说,存储在内存中的短期访问令牌加上HttpOnly Cookie中的刷新令牌是一个很好的平衡。

失效和撤销

JWT是无状态的:服务器可以在没有数据库查询的情况下验证它们。另一方面,有效的代币在到期之前,。缓解措施:

  • 存储访问令牌的时间很短,5至15分钟是常见的。
  • 使用更新令牌,安全存储并与数据库进行检查,以发行新的访问令牌。
  • 对于高风险的行动,请检查jti值的撤销列表或每个用户的"代币有效后"时间。

常见的错误

  • 密码,API密钥和个人数据可以由任何拥有代币的人读取。如果您需要保密,请使用JWE (加密代币) 或将数据保留在服务器端。
  • 非常持久的代币. 一个有效一年的被盗代币是一年的违规。
  • 跳过观众检查. 如果没有aud验证,低特权服务的代币可以用于高特权服务。
  • **弱HMAC密码.**HS256密码必须长且随机,至少为256位。简短的秘密可以通过单个代币进行离线强迫。
  • **日志令牌.**访问日志和错误追踪器通常捕获Authorization头。编辑它们。

安全检查一个代币

在调试时,使用在浏览器中本地运行的解码器,以便令牌永远不会发送给第三方。在共享截图或粘贴到门票时更喜欢过期或测试环境令牌。检查:

  • 标题中的alg和kid与您的供应商的配置相匹配。
  • iss和aud符合您的API预期。
  • exp,iat和nbf相对于当前时间是有意义的。
  • 范围和角色包含操作所需要的内容。

总结

一个JWT是一个签名,未加密的JSON索赔捆绑。它的安全性完全来自服务器上的签名验证和索赔验证。解码是无害的,并且对调试有用;信任未经验证的解码数据是真正的风险。保持代币的使用寿命短,保持小的有效负载和没有秘密,每次验证算法,签名,到期,发行人和受众,

本页内容由英文自动翻译而来,如发现错误,欢迎告诉我们。

相关指南