本文展示了如何使用 JWT 进行跨服务认证/授权。
在单体架构中,所有子系统都在一个应用程序中:身份验证和授权、会话管理器、业务逻辑等。如果我们在谈论微服务架构,有些事情变得更加复杂。
微服务
在微服务架构中,通常,认证/授权是一个单独的服务。
要请求服务,您必须首先进行身份验证并获取访问令牌。 一个示例是 OAuth 2.0 客户端凭据流。 要获取令牌,您需要传递 client id
和 client secret
。 在下面的示例中,凭据作为 Authorization Basic 标头传递:
POST /token HTTP/1.1
Host: server.example.com
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
响应此请求,我们将获得一个访问令牌,可用于请求所需的服务。 但是我们如何在服务端验证这个令牌呢?
验证
最明显的方法是查询授权服务并自省令牌。 但是当我们的系统包含数百个服务时,这样的请求会给授权服务带来很大的负担,并且有时会成为瓶颈。 为避免这种情况,我们可以在服务中验证令牌。 为此,我们可以使用自包含令牌,其中包括 JWT(JSON Web 令牌)。
JWT是某种token,一般由header、body、signature三部分组成。 该标准在 RFC7519 中有更详细的描述。
这里 header 指定:
typ
– 一种令牌类型(我们正在考虑JWT)alg
– 签名算法(例如 HS256 – 带有 SHA-256 的 HMAC(标头 + 有效负载 + 密钥))kid
– 当有多个密钥时使用,您需要了解哪个密钥是已签名的令牌
body 由标准标记和自定义标记组成:
iss
– 发行人(发行令牌的人)aud
– 观众(令牌的对象)iat
– 合适签发exp
– 过期时间sub
– 主题jti
– JWT唯一ID
对称(例如,HS256)和非对称(例如,RS256)签名算法都可以用作签名。 在对称算法中,只有一个私钥,用于签名和验证。 在 非对称算法 中,签名者使用只有他自己保留的私钥,并验证 可以使用可以分发给每个人的公钥来完成签名。
事实证明,这样的签名令牌是自给自足的,不能在接收方不注意的情况下被操纵。 接收方收到token后,可以验证签名,确保token没有被修改。 这意味着令牌内的所有数据都可以信任。 这消除了通过调用远程服务来内省令牌的需要,从而减少了服务的负载。
如果服务 B 收到一个包含访问令牌的请求,它需要一个密钥来验证签名。 如果我们有一个包含几十上百个微服务的复杂系统,那么必要的服务就需要分发这个密钥。 如果使用对称算法,密钥很可能会泄漏。 拥有私钥,您可以签署任何内容的令牌。 因此,强烈建议使用非对称算法。 在这种情况下,通常会在身份验证/授权服务上放置一个端点,该服务返回一个用于检查签名的公钥列表。 必要的服务需要自己查询和缓存这些键。 使用这种方法,很容易组织密钥轮换——在某个时刻,auth-service 可以生成一组新的密钥并开始使用它。 接收到这样一个新令牌后,接收服务无法自行找到缓存的密钥,将再次请求 auth-service 并接收更新的密钥列表。
例子:
curl https://www.googleapis.com/oauth2/v3/certs
{
"keys": [
{
"alg": "RS256",
"kty": "RSA",
"use": "sig",
"n": "4DauU23AEpgBg3zJbqT8Fn-Zf817ru1moUjG75yJ-T0NpuQiggrXPn2YoKgo_qtnYloZh-RLjFfRv_Jb47riZhV5vsW7PiMR4MjlXgMWQlWG7kD9cIH5cTzBuEAzCkZZDu7XFkTfWUtRdWS5iKBjfQ465Qi5yFqfh7iHbQoKiN32pkWDI4MG8CUQC-YDbz77IRMpD39ZzNxkxYqbeJ226MrgKVGHFbmZLZPX8VX4r45NZifkPHa5-G5YDxaL622fkTqgPkyJtFOMy08X6K4BtVV0ZUJqi19bzEW970aI13seu0BzBsIspZ2NSPtljQqQFJTcW1EAmOCB5iNDi3J0mQ",
"e": "AQAB",
"kid": "fda1066453dc9dc3dd933a41ea57da3ef242b0f7"
},
{
"e": "AQAB",
"alg": "RS256",
"n": "yJdNun_DT8_krjOUFMk4UPb7KgOyoN2EIHVL77LFLUlzFwOLon1pEceYcWffNQnjdtzDCN5-q6DxlIiJyDgQhPPMpJzMcpZceo0tKd-Ve1RLEUVcbnbjyZ-inrxVWfYTOuWTsutt7EylFDIMfw1Dh14IccFG5loyLdtZX2yejhXmJzMCxTISE_lCxCIiIqu5filfc3AnnyNb66Mv_oyK5z22pc9f-dFAmT3e5IXA-0UkrEVtLl7lRGmWdBkAkEWzhh17aQ0BynxpcTX5efGyr2b5ktUObCNdKMwNE4_Berz4l7_Oz6-gWDlyjbROrHKx0B27SFHdtNHbYARJsfVsjw",
"kty": "RSA",
"kid": "1727b6b49402b9cf95be4e8fd38aa7e7c11644b1",
"use": "sig"
}
]
}
验证令牌签名后,我们必须检查 iss
和 aud
声明。 iss
必须包含身份验证服务的标识符或 URL。 aud
包含为其生成令牌的服务的标识符或标识符列表。 我们必须确保我们的系统出现在这个列表中,并且这个令牌是为我们准备的。
在所有检查之后,我们可以授权请求并执行必要的操作。
令牌撤销
重要的是要注意,自包含令牌的问题之一是无法简单地撤销它们。 例如,一个令牌被泄露,我们需要禁止它在我们系统中的所有服务上使用。 但我们的服务只进行签名验证——令牌仍然有效。
在这种情况下,我们会缩短令牌的生命周期(分钟),之后它将不再有效。 但是如果这还不够,并且需要立即撤销令牌,您可以使用令牌黑名单作为键值存储库,它将存储被撤销令牌的 ID (jti
)。 然后每个服务都应该查询该存储并确保令牌不在列表中。 没有必要将令牌永久存储在此存储库中,而只是在我们达到令牌中的 exp 值之前。 此外,令牌撤销事件本身非常少见,因此存储空间会很小且会被读取。
敏感的信息
应该理解,令牌的内容没有加密,令牌的正文是可以读取的。 如果您必须将令牌提供给第三方服务,那么您应该小心放入令牌正文中的数据。 但如果需要该数据,您可以使用加密版本的令牌 (JWE)。 另一种方法是不给出完整的令牌,而只给出它的标识符(jti
),将jti
——完整的令牌映射存储在存储库/缓存中。 在下一个请求中,将 jti
交换为完整令牌并在系统内使用完整令牌。
总结
在跨服务身份验证/授权实现中正确使用 JWT 可以让我们构建灵活、安全、高负载的应用程序。 了解它的工作原理将减少在构建系统架构时引入漏洞的可能性。