一個 jwt 頭部字段引發(fā)的血案
這里分享一個由于 JWT 頭部引起的系統(tǒng)問題,以及如何解決的案例。
問題是這樣的,在一個微服務環(huán)境里,身份認證中心采用了 Duende IdentityServer,而接入該身份認證中心的微服務都是基于 SpringBoot 搭建的 Java 項目。在使用 JWT 作為身份認證的令牌時,IdentityServer 生成的 JWT 頭部如下:
json
{
"alg": "RS256",
"kid": "1",
"typ": "at+jwt"
}
即其 typ 默認是 at+jwt,這并不被默認的 SpringBoot 項目識別,從而要么需要改眾多的微服務應用,要么需要改 Duende IdentityServer 在頒發(fā) JWT 時的行為,即只頒發(fā) typ 為 jwt 的令牌。由于改多處不如改一處來得簡單,所以我們選擇了改 Duende IdentityServer 的行為。相關(guān)代碼如下:
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 結(jié)構(gòu)化令牌詳解
JWT(JSON Web Token)是一種輕量級的、基于JSON的令牌格式,常用于在身份認證過程中傳遞信息。它由三個部分組成:頭部(Header)、載荷(Payload)和簽名(Signature)。
頭部(Header)
JWT的頭部部分包含了兩個信息:令牌的類型(typ)和所使用的簽名算法(alg)。這些信息以JSON對象的形式進行編碼,然后通過Base64URL編碼變成一個字符串。
載荷(Payload)
JWT的載荷部分用于存儲令牌所攜帶的信息,可以包含一些標準的聲明(例如:iss、sub、exp)以及自定義的聲明。這些聲明以JSON對象的形式進行編碼,然后通過Base64URL編碼變成一個字符串。
字段名 |
描述 |
iss (Issuer) |
令牌的發(fā)行者,其值應為大小寫敏感的字符串或者 Uri |
sub (Subject) |
令牌的主題,可以用來鑒別一個用戶,比如可以是一個用戶的公開 ID(PUID) |
exp (Expiration Time) |
令牌的過期時間,其值必須是一個數(shù)值,代表從 1970-01-01T00:00:00Z UTC 開始計算的秒數(shù) |
aud |
(Audience) |
令牌的受眾,其值必須是大小寫敏感的字符串或者 Uri,或者是字符串數(shù)組或者 Uri 數(shù)組。一般可以是特定的 App、服務或者模塊。服務器端的安全策略在簽發(fā)和驗證令牌時,需要比較其 aud 是一致的 |
iat (Issued At) |
令牌的簽發(fā)時間,其值必須是一個數(shù)值,代表從 1970-01-01T00:00:00Z UTC 開始計算的秒數(shù) |
nbf (Not Before) |
令牌的生效時間,其值必須是一個數(shù)值,代表從 1970-01-01T00:00:00Z UTC 開始計算的秒數(shù) |
jti (JWT ID) |
令牌的唯一標識符,其值必須是大小寫敏感的字符串。一般用于一次性消費的令牌,用來防止重放攻擊 |
除了標準的聲明之外,還可以添加自定義的聲明,以滿足特定的業(yè)務需求。自定義的聲明可以是公共的,也可以是私有的。公共的聲明可以添加任何需要的信息,一般添加用戶的相關(guān)信息或者其他業(yè)務需要的信息,注意不要添加敏感信息;私有聲明是客戶端和服務端所共同定義的聲明,盡管這里名稱是私有聲明,但要注意仍然不能在這里添加敏感信息,因為前面提到過,jwt 只是將信息進行了 base64 編碼,所以它可以很容易地被解碼(比如使用 jwt.io 就能方便地查看 jwt 的明文信息)。
簽名(Signature)
JWT的簽名部分用于驗證令牌的真實性和完整性。它使用了頭部和載荷部分的內(nèi)容,以及一個密鑰,通過指定的簽名算法進行簽名生成。簽名的目的是防止令牌被篡改。簽名通常由頭部、載荷和密鑰通過Base64URL編碼后的字符串進行組合生成。
密鑰一定不能泄露,否則會被入侵者利用它來簽發(fā)偽造的令牌。
密鑰需要安全地存儲在服務端,或者是一個服務端可以安全獲取的存儲位置,服務端簽發(fā)令牌時先將頭部、載荷分別 base64 編碼,用.號連接,再使用頭部中聲明的加密方式,利用密鑰對連接后的字符串進行加密,得到簽名。簽名會再次用.號拼接在頭部和載荷后面,形成最終的 jwt 頒發(fā)給客戶端。客戶端后續(xù)請求時會攜帶該令牌,服務端收到令牌后會解碼出頭部和載荷,再次使用頭部中聲明的加密方式,利用密鑰對頭部和載荷進行加密,得到簽名,然后將簽名與令牌中的簽名進行比較,如果一致,則說明令牌是合法的,否則說明令牌被篡改。
