本文展示了如何使用 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 可以让我们构建灵活、安全、高负载的应用程序。 了解它的工作原理将减少在构建系统架构时引入漏洞的可能性。