什么是 JWT
OAuth2.0 体系中令牌分为两类,分别是透明令牌、不透明令牌。
不透明令牌则是令牌本身不存储任何信息,比如一串 UUID,因此资源服务拿到这个令牌必须调调用认证授权服务的接口进行令牌的校验,高并发的情况下延迟很高,性能很低。
透明令牌本身就存储这部分用户信息,比如 JWT,资源服务可以调用自身的服务对该令牌进行校验解析,不必调用认证服务的接口去校验令牌。
JWT 有三部分组成,分别是头部、载荷、签名。
Header 包含两部分信息
typ: 表示令牌类型,通常是 “JWT”。
alg: 表示使用的签名算法,例如 HS256(HMAC-SHA256)或 RS256(RSA-SHA256)。
{
"alg": "HS256",
"typ": "JWT"
}
这个头部数据会被 Base64Url 编码。
Payload 是存放实际需要传输的信息(声明,claims)的部分。声明分为三种类型
Registered Claims:预定义的标准声明,非强制。常见的声明包括:
iss(Issuer):签发人
sub(Subject):主题
aud(Audience):受众
exp(Expiration):过期时间
iat(Issued At):签发时间
Public Claims:自定义的声明,但需要避免冲突,通常使用 URI 作为命名空间。
Private Claims:应用中定义的声明,仅供双方使用。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"exp": 1618474796
}
负载部分同样会经过 Base64Url 编码。
Signature 签名是用来验证消息在传输过程中是否被篡改的部分。它由以下三部分组成:
编码后的头部
编码后的负载
一个密钥(用于对称加密,或使用私钥/公钥对,非对称加密)
在 JWT 的签名部分中,密钥本身并不包含在签名中。签名使用密钥进行生成,但密钥本身不会直接嵌入到 JWT 令牌中。这是出于安全考虑,因为密钥是用来验证令牌合法性的关键,而不是传递给客户端的。
将上述三部分通过指定算法进行签名计算。
生成签名的公式:
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
签名的生成使用了密钥(例如一个 secret key 或 private key),但这个密钥仅用于服务器端的签名生成和验证,客户端永远不会看到这个密钥。
如果使用对称加密(如 HS256),同一个密钥用于签名和验证。
如果使用非对称加密(如 RS256),私钥用于签名,公钥用于验证。
生成的签名将与头部和负载部分一起传输,用于验证令牌的完整性。
JWT 结构示例
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
JWT 的优势在于它是自包含的,包含了验证和信息传递所需的所有内容,不需要服务器在每个请求时进行存储,因此非常适合分布式系统中的身份认证。
使用阿里云 kms 对 JWT 进行签名(非对称方式)
在阿里云 KMS 控制台上创建一个密钥,记录下密钥 ID。
私钥用于服务器端签名,公钥用于客户端验证签名。私钥通常不会被直接获取,而是在签名操作中使用 KMS API。
在阿里云 KMS 中,公钥的使用与验签过程是通过 KMS API 的 VerifyRequest 来实现的,而不是直接在代码中传递公钥。这是因为阿里云 KMS 设计成一种安全的密钥管理服务,签名和验签的过程是通过调用 KMS API 完成的,因此在上面的验签代码中并没有涉及到公钥的获取,虽然在验签的代码中并没有直接引用公钥,但 KMS 会内部处理密钥的使用,这种设计确保了密钥不会直接暴露,增强了安全性。使用了阿里云,就没有必要再导出公钥了。
JWT 的存储
在 JWT(JSON Web Token)架构中,通常不需要将 ID Token 和 Access Token 存储在 Redis 中,原因如下:
- JWT 是自包含的:它们包含了必要的信息(如 sub 字段中的用户 ID、exp 过期时间、权限范围等)。通过 JWT 的签名机制,服务器可以在不存储 Token 的情况下验证它的有效性。
- Token 的无状态性:JWT 在无状态分布式系统中非常有用,服务器只需验证 Token 签名,无需在后端数据库或缓存中存储 Token。
尽管 JWT 是无状态的,有些情况下出于安全、控制、审计或撤销 Token 的需求,可能需要将 ID Token 和 Access Token 存储在 Redis 中。
- Token 撤销机制:如果系统需要在特定情况下(如用户注销、异常活动检测等)主动撤销某些 Token,则可以在 Redis 中存储 Token,并在需要时将其从 Redis 中删除或标记为无效。JWT 本身是无状态的,一旦签发,直到过期前都有效,因此无法通过无状态的方式主动撤销。如果使用 Redis 存储 Token,可以控制其生命周期。
- 黑名单或 Token 失效控制:为了实现更细粒度的 Token 控制(如在用户修改密码或注销后立即失效 Token),可以通过 Redis 维护一个黑名单或失效列表。如果某个 Token 被标记为无效,服务器可以在解析 Token 时检查 Redis 来判断其有效性。
- Refresh Token 旋转机制:虽然 Access Token 和 ID Token一般不需要存储,但如果启用了 Refresh Token 旋转(每次使用 Refresh Token 刷新时生成新的 Token),可能需要在 Redis 中存储旧的 Access Token 和 ID Token 以防止其被继续使用,尤其是当希望在生成新的 Token 后让旧 Token 失效时。
实际应用中的建议
- 无状态实现优先:在大多数情况下,利用 JWT 的无状态特性就足够了,无需将 Access Token 和 ID Token 存储在 Redis 中。只要保证签名正确、Token 未过期,服务器可以无状态地处理认证和授权。
- 引入状态控制的场景:在有特殊需求的场景下(如 Token 撤销、黑名单、主动失效等),可以考虑将 Token 存储在 Redis 中,并搭配其他机制(如 RefreshToken 的轮换机制、Token 的撤销机制)来增强安全性。
因此,除非有特定的安全需求需要控制 Token 的状态或失效,否则不建议将 JWT 的 Access Token 和 ID Token 存储在 Redis 中,尽量利用 JWT 的无状态设计来减少系统的复杂度。
在JWT(JSON Web Token)中,Refresh Token 放在 Redis 中保存是一个非常常见且推荐的做法。
- 生成 Refresh Token:用户登录后,服务器生成 Refresh Token,并将其存储在 Redis 中,设置相应的过期时间。
- 验证 Refresh Token:当 Access Token 过期时,客户端使用 Refresh Token 向服务器请求新的 Access Token。
- 更新 Redis 中的 Refresh Token:如果 Refresh Token 有效,服务器生成新的 Access Token 和(可选的)新的 Refresh Token,并更新Redis 中的记录。
- 自动过期管理:Redis 自动管理 Refresh Token 的过期,避免过期 Token 的滥用。
这里需要注意的是:Refresh Token 本身是有过期时间字段的,在判断其是否过期的时候需要进行双重判断,先判断 redis 中是否过期,然后再判断本身是否过期。
无缝刷新 JWT Token
在 Spring Cloud 微服务架构中,使用 JWT 进行用户认证时,Access Token 通常设置较短的有效期,而 Refresh Token 则用于在 Access Token 过期时生成新的 Token,以保证用户不需要重新登录。要实现 Access Token 和 ID Token 的无缝刷新,并确保客户端无感知,可以遵循以下流程和策略。
自动刷新 Token:当 Access Token 快要过期时,服务器端自动使用 Refresh Token 刷新,并重新生成新的 Access Token 和 ID Token。
客户端无感知:客户端无需主动刷新 Token,而是在发送请求时,系统自动判断 Access Token 的有效性,必要时自动刷新 Token。
最小化刷新延迟:确保在每个 API 请求中,如果 Access Token 即将过期或已过期,系统会透明地处理 Token 刷新并将新的 Access Token 返回给客户端。
自动刷新 Token 的机制,在系统微服务的网关或拦截器中实现 Token 的自动刷新逻辑。
在网关或过滤器中检查 Access Token 是否快要过期
每次客户端发起请求时,先通过一个拦截器或过滤器来检查 Access Token 的有效性。解析 JWT 中的过期时间(exp 字段),判断是否快要过期或已经过期。
如果 Access Token 还有几分钟才过期,则继续使用,不做任何操作。
如果 Access Token 已经过期或接近过期,则使用 Refresh Token 刷新。
权限动态变更
由于 JWT 中的权限是嵌入在令牌中的,如果用户的权限发生了变化,需要一种方式来确保新的权限能够生效。通常有两种方法来处理权限变更:
强制刷新令牌:当用户权限发生变更时,强制要求用户重新获取新的令牌。
外部权限检查:不依赖 JWT 中的权限信息,而是在每次请求时,从数据库或缓存中动态查询用户权限。