一个 jwt 头部字段引发的血案
共 2962字,需浏览 6分钟
·
2023-09-21 13:44
这里分享一个由于 JWT 头部引起的系统问题,以及如何解决的案例。
问题是这样的,在一个微服务环境里,身份认证中心采用了 Duende IdentityServer,而接入该身份认证中心的微服务都是基于 SpringBoot 搭建的 Java 项目。在使用 JWT 作为身份认证的令牌时,IdentityServer 生成的 JWT 头部如下:
json
{
"alg": "RS256",
"kid": "1",
"typ": "at+jwt"
}
即其 typ 默认是 at+jwt,这并不被默认的 SpringBoot 项目识别,从而要么需要改众多的微服务应用,要么需要改 Duende IdentityServer 在颁发 JWT 时的行为,即只颁发 typ 为 jwt 的令牌。由于改多处不如改一处来得简单,所以我们选择了改 Duende IdentityServer 的行为。相关代码如下:
csharp
namespace IdentityServer;
internal static class HostingExtensions
{
public static WebApplication ConfigureServices(this WebApplicationBuilder
builder)
{
// uncomment if you want to add a UI
builder.Services.AddRazorPages();
builder.Services.AddIdentityServer(options =>
{
// https://docs.duendesoftware.com/identityserver/v6/fundamentals/resources/api_scopes#authorization-based-on-scopes
options.EmitStaticAudienceClaim = true;
// 将默认的 at+jwt 修改为 jwt
options.AccessTokenJwtType = "jwt";
} )...
问题解决了,再来详细了解一下 jwt。
jwt 结构化令牌详解
JWT(JSON Web Token)是一种轻量级的、基于JSON的令牌格式,常用于在身份认证过程中传递信息。它由三个部分组成:头部(Header)、载荷(Payload)和签名(Signature)。
头部(Header)
JWT的头部部分包含了两个信息:令牌的类型(typ)和所使用的签名算法(alg)。这些信息以JSON对象的形式进行编码,然后通过Base64URL编码变成一个字符串。
载荷(Payload)
JWT的载荷部分用于存储令牌所携带的信息,可以包含一些标准的声明(例如:iss、sub、exp)以及自定义的声明。这些声明以JSON对象的形式进行编码,然后通过Base64URL编码变成一个字符串。
字段名 |
描述 |
iss (Issuer) |
令牌的发行者,其值应为大小写敏感的字符串或者 Uri |
sub (Subject) |
令牌的主题,可以用来鉴别一个用户,比如可以是一个用户的公开 ID(PUID) |
exp (Expiration Time) |
令牌的过期时间,其值必须是一个数值,代表从 1970-01-01T00:00:00Z UTC 开始计算的秒数 |
aud |
(Audience) |
令牌的受众,其值必须是大小写敏感的字符串或者 Uri,或者是字符串数组或者 Uri 数组。一般可以是特定的 App、服务或者模块。服务器端的安全策略在签发和验证令牌时,需要比较其 aud 是一致的 |
iat (Issued At) |
令牌的签发时间,其值必须是一个数值,代表从 1970-01-01T00:00:00Z UTC 开始计算的秒数 |
nbf (Not Before) |
令牌的生效时间,其值必须是一个数值,代表从 1970-01-01T00:00:00Z UTC 开始计算的秒数 |
jti (JWT ID) |
令牌的唯一标识符,其值必须是大小写敏感的字符串。一般用于一次性消费的令牌,用来防止重放攻击 |
除了标准的声明之外,还可以添加自定义的声明,以满足特定的业务需求。自定义的声明可以是公共的,也可以是私有的。公共的声明可以添加任何需要的信息,一般添加用户的相关信息或者其他业务需要的信息,注意不要添加敏感信息;私有声明是客户端和服务端所共同定义的声明,尽管这里名称是私有声明,但要注意仍然不能在这里添加敏感信息,因为前面提到过,jwt 只是将信息进行了 base64 编码,所以它可以很容易地被解码(比如使用 jwt.io 就能方便地查看 jwt 的明文信息)。
签名(Signature)
JWT的签名部分用于验证令牌的真实性和完整性。它使用了头部和载荷部分的内容,以及一个密钥,通过指定的签名算法进行签名生成。签名的目的是防止令牌被篡改。签名通常由头部、载荷和密钥通过Base64URL编码后的字符串进行组合生成。
密钥一定不能泄露,否则会被入侵者利用它来签发伪造的令牌。
密钥需要安全地存储在服务端,或者是一个服务端可以安全获取的存储位置,服务端签发令牌时先将头部、载荷分别 base64 编码,用.号连接,再使用头部中声明的加密方式,利用密钥对连接后的字符串进行加密,得到签名。签名会再次用.号拼接在头部和载荷后面,形成最终的 jwt 颁发给客户端。客户端后续请求时会携带该令牌,服务端收到令牌后会解码出头部和载荷,再次使用头部中声明的加密方式,利用密钥对头部和载荷进行加密,得到签名,然后将签名与令牌中的签名进行比较,如果一致,则说明令牌是合法的,否则说明令牌被篡改。