<font style=color:rgb(0, 0, 0);background-color:#FFFFFF;>这里分享一个由于 JWT 头部引起的系统问题,以及如何解决的案例。

<font style=color:rgb(0, 0, 0);>问题是这样的,在一个微服务环境里,身份认证中心采用了 Duende IdentityServer,而接入该身份认证中心的微服务都是基于 SpringBoot 搭建的 Java 项目。在使用 JWT 作为身份认证的令牌时,IdentityServer 生成的 JWT 头部如下:

<font style=color:rgb(0, 0, 0);>json

plain { alg: RS256, kid: 1, typ: at+jwt }

<font style=color:rgb(0, 0, 0);>即其 typ 默认是 at+jwt,这并不被默认的 SpringBoot 项目识别,从而要么需要改众多的微服务应用,要么需要改 Duende IdentityServer 在颁发 JWT 时的行为,即只颁发 typ 为 jwt 的令牌。由于改多处不如改一处来得简单,所以我们选择了改 Duende IdentityServer 的行为。相关代码如下:

<font style=color:rgb(0, 0, 0);>csharp

plain 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。

<font style=color:rgb(0, 0, 0);>jwt 结构化令牌详解

<font style=color:rgb(0, 0, 0);>JWT(JSON Web Token)是一种轻量级的、基于JSON的令牌格式,常用于在身份认证过程中传递信息。它由三个部分组成:头部(Header)、载荷(Payload)和签名(Signature)。

<font style=color:rgb(0, 0, 0);>

<font style=color:rgb(0, 0, 0);>头部(Header)

<font style=color:rgb(0, 0, 0);>JWT的头部部分包含了两个信息:令牌的类型(typ)和所使用的签名算法(alg)。这些信息以JSON对象的形式进行编码,然后通过Base64URL编码变成一个字符串。

<font style=color:rgb(0, 0, 0);>载荷(Payload)

<font style=color:rgb(0, 0, 0);>JWT的载荷部分用于存储令牌所携带的信息,可以包含一些标准的声明(例如:iss、sub、exp)以及自定义的声明。这些声明以JSON对象的形式进行编码,然后通过Base64URL编码变成一个字符串。

<font style=color:rgb(0, 0, 0);>字段名 <font style=color:rgb(0, 0, 0);>描述
<font style=color:rgb(0, 0, 0);>iss (Issuer) <font style=color:rgb(0, 0, 0);>令牌的发行者,其值应为大小写敏感的字符串或者 Uri
<font style=color:rgb(0, 0, 0);>sub (Subject) <font style=color:rgb(0, 0, 0);>令牌的主题,可以用来鉴别一个用户,比如可以是一个用户的公开 ID(PUID)
<font style=color:rgb(0, 0, 0);>exp (Expiration Time) <font style=color:rgb(0, 0, 0);>令牌的过期时间,其值必须是一个数值,代表从 1970-01-01T00:00:00Z UTC 开始计算的秒数
<font style=color:rgb(0, 0, 0);>aud<font style=color:rgb(0, 0, 0);>
<font style=color:rgb(0, 0, 0);>(Audience) <font style=color:rgb(0, 0, 0);>令牌的受众,其值必须是大小写敏感的字符串或者 Uri,或者是字符串数组或者 Uri 数组。一般可以是特定的 App、服务或者模块。服务器端的安全策略在签发和验证令牌时,需要比较其 aud 是一致的
<font style=color:rgb(0, 0, 0);>iat (Issued At) <font style=color:rgb(0, 0, 0);>令牌的签发时间,其值必须是一个数值,代表从 1970-01-01T00:00:00Z UTC 开始计算的秒数
<font style=color:rgb(0, 0, 0);>nbf (Not Before) <font style=color:rgb(0, 0, 0);>令牌的生效时间,其值必须是一个数值,代表从 1970-01-01T00:00:00Z UTC 开始计算的秒数
<font style=color:rgb(0, 0, 0);>jti (JWT ID) <font style=color:rgb(0, 0, 0);>令牌的唯一标识符,其值必须是大小写敏感的字符串。一般用于一次性消费的令牌,用来防止重放攻击

<font style=color:rgb(0, 0, 0);>除了标准的声明之外,还可以添加自定义的声明,以满足特定的业务需求。自定义的声明可以是公共的,也可以是私有的。公共的声明可以添加任何需要的信息,一般添加用户的相关信息或者其他业务需要的信息,注意不要添加敏感信息;私有声明是客户端和服务端所共同定义的声明,尽管这里名称是私有声明,但要注意仍然不能在这里添加敏感信息,因为前面提到过,jwt 只是将信息进行了 base64 编码,所以它可以很容易地被解码(比如使用 jwt.io 就能方便地查看 jwt 的明文信息)。

<font style=color:rgb(0, 0, 0);>签名(Signature)

<font style=color:rgb(0, 0, 0);>JWT的签名部分用于验证令牌的真实性和完整性。它使用了头部和载荷部分的内容,以及一个<font style=color:rgb(0, 0, 0);>密钥<font style=color:rgb(0, 0, 0);>,通过指定的签名算法进行签名生成。签名的目的是防止令牌被篡改。签名通常由头部、载荷和密钥通过Base64URL编码后的字符串进行组合生成。


密钥一定不能泄露,否则会被入侵者利用它来签发伪造的令牌。

<font style=color:rgb(0, 0, 0);>密钥需要安全地存储在服务端,或者是一个服务端可以安全获取的存储位置,服务端签发令牌时先将头部、载荷分别 base64 编码,用.号连接,再使用头部中声明的加密方式,利用密钥对连接后的字符串进行加密,得到签名。签名会再次用.号拼接在头部和载荷后面,形成最终的 jwt 颁发给客户端。客户端后续请求时会携带该令牌,服务端收到令牌后会解码出头部和载荷,再次使用头部中声明的加密方式,利用密钥对头部和载荷进行加密,得到签名,然后将签名与令牌中的签名进行比较,如果一致,则说明令牌是合法的,否则说明令牌<font style=color:rgb(0, 0, 0);>被篡改<font style=color:rgb(0, 0, 0);>。