如何使用JWT以及如何安全解密代码
了解JSON Web Tokens的结构,如何创建和验证签名,常见的索赔,安全陷以及如何解码JWT。
· 6 分钟阅读
JSON Web Token (JWT) 遍布各地:OAuth和OpenID Connect登录,API网关,微服务之间的单一登录,以及移动应用程序会话。它们也被广泛误解。开发人员解码一个令牌,看到可读JSON,并想知道这是否是安全问题;其他人将敏感数据存储在令牌中,假设它们是加密的。这篇文章解释了JWT是什么,如何验证,以及如何在不造成风险的情况下检查。
一个JWT的三个部分
一个JWT看起来像三个随机串连接的点:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI0MiIsImV4cCI6MTkwMDAwMDAwMH0.Kx8...
每个部分都是Base64URL编码:
- 头条 JSON描述令牌类型和签名算法,例如
{"alg":"HS256","typ":"JWT"}。 - 付费负载 JSON 包含索赔:关于用户和代币的声明,如
{"sub":"42","exp":1900000000}。 - 签名 从头部和有效载荷中计算的字节,使用秘密或私钥。
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。让他们少;每一个要求都伴随着每一个请求。
正确验证一个令牌
服务器必须按照下列顺序检查:
- The algorithm is one you expect. Configure an allow-list such as
["RS256"]。永远不要让代币的头部自由决定;历史漏洞让攻击者切换到none或欺骗服务器使用公钥作为HMAC秘密。 - 签名是有效的 用正确的密钥,由
kid从您所信任的密钥集中选择。 exp在未来,nbf在过去,允许小时钟偏差3060秒。issmatches your identity provider exactly。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索赔捆绑。它的安全性完全来自服务器上的签名验证和索赔验证。解码是无害的,并且对调试有用;信任未经验证的解码数据是真正的风险。保持代币的使用寿命短,保持小的有效负载和没有秘密,每次验证算法,签名,到期,发行人和受众,
本页内容由英文自动翻译而来,如发现错误,欢迎告诉我们。