JWT 驗證:驗證權杖以進行身分驗證與授權
實務指南,說明 JWT 驗證——檢查 JSON Web Token 的簽章、宣告與結構,以確認請求確實已通過身分驗證與授權——涵蓋簽章驗證、標準宣告檢查、金鑰輪替、ASP.NET Core 中的驗證,以及最常導致驗證失效或被繞過的錯誤。
目錄
- 簡介
- JWT 結構
- 簽署演算法
- 「驗證」實際檢查的項目
- 簽章驗證與金鑰輪替
- 標準宣告驗證
- 在 ASP.NET Core 中驗證 JWT
- 自訂驗證邏輯
- 權杖撤銷:JWT 的根本限制
- 跨服務驗證 JWT
- 常見弱點
- 除錯驗證失敗
- 快速參考表
- 結論
簡介
出現在 Authorization: Bearer <token> 標頭中的 JWT 只是一串字串,除非真正經過驗證——而驗證所做的工作遠比乍看之下複雜。它不只是「看起來像 JWT」,甚至不只是「簽章有效」——正確的驗證會確認權杖是由受信任的來源發行、專門針對此 API、仍在有效時間範圍內,且未被任何方式竄改。若其中任一檢查有誤或被略過,就可能導致 API 接受不該接受的權杖。
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0";
options.Audience = "api://my-api";
});
進入全螢幕模式 離開全螢幕模式
這兩行程式碼看似簡單,卻在背後配置了一個真正完整的驗證管線——本指南將詳細說明該管線實際檢查的項目、每項檢查的重要性,以及當驗證設定錯誤或因壓力而被繞過時,最常出現的問題。
1. JWT 結構
三個以點分隔的部分
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL2F1dGhzZXJ2ZXIuY29tIiwic3ViIjoiMTIzNDU2Nzg5MCIsImF1ZCI6ImFwaTovL215LWFwaSIsImV4cCI6MTcyMTQwNDgwMH0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
└──────────────── header ────────────────┘└──────────────────────── payload ────────────────────────┘└──────── signature ────────┘
進入全螢幕模式 離開全螢幕模式
標頭
{ "alg": "RS256", "typ": "JWT", "kid": "abc123" }
進入全螢幕模式 離開全螢幕模式
-
alg— 使用的簽署演算法(第 2 節)。 -
typ— 識別此為 JWT。 -
kid(金鑰 ID)— 識別用來簽署此權杖的特定金鑰(可能有數把目前有效的金鑰),對金鑰輪替至關重要(第 4 節)。
承載(宣告)
{
"iss": "https://authserver.com",
"sub": "1234567890",
"aud": "api://my-api",
"exp": 1721404800,
"iat": 1721401200,
"nbf": 1721401200,
"scp": "products.read products.write"
}
進入全螢幕模式 離開全螢幕模式
關於權杖、主體及其有效期的宣告集合——詳見第 5 節。
簽章
signature = Sign(base64url(header) + "." + base64url(payload), private_key)
進入全螢幕模式 離開全螢幕模式
使用發行者的私鑰,對標頭與承載一起計算出的密碼學簽章——這讓驗證者能確認權杖確實來自聲稱的發行者,且內容自簽署後未被修改,只需使用發行者對應的公鑰即可(第 4 節)。
重要:承載並未加密
echo "eyJpc3MiOiJodHRwczovL2F1dGhzZXJ2ZXIuY29tIn0" | base64 -d
# {"iss":"https://authserver.com"}
進入全螢幕模式 離開全螢幕模式
JWT 的標頭與承載僅為base64url 編碼,並未加密——任何攔截到權杖的人(或使用者自行檢視自己的權杖)都能輕易解碼並讀取其中的所有宣告。這是一個常見且嚴重的誤解:JWT 提供完整性與真實性(可信任其未被竄改且確實來自發行者),但完全不提供機密性。切勿將真正敏感的資料(密碼、機密或其他不應被權杖持有人或任何攔截者看到的内容)直接放入 JWT 的宣告中。
2. 簽署演算法
非對稱式(OAuth2/OIDC 存取權杖與 ID 權杖的標準)
RS256 — RSA 搭配 SHA-256 的簽章(最常見)
ES256 — ECDSA 搭配 SHA-256 的簽章(簽章較小,採用率上升中)
進入全螢幕模式 離開全螢幕模式
使用非對稱式演算法時,授權伺服器以其保密的私鑰簽署權杖,而任何數量的資源伺服器(API)都能使用對應的公鑰驗證簽章——該公鑰由授權伺服器公開發布(透過本系列 OAuth2/OpenID Connect 指南中提及的 jwks_uri 探索端點)。這種非對稱性正是 JWT 適合分散式系統的原因:數十個獨立的 API 都能驗證權杖,而無需持有任何能發行新有效權杖的機密。
對稱式(很少適合 OAuth2/OIDC 情境)
HS256 — HMAC 搭配 SHA-256,使用單一共享密鑰同時進行簽署與驗證
進入全螢幕模式 離開全螢幕模式
使用對稱式演算法時,同一個密鑰同時用於簽署與驗證權杖——這表示每個需要驗證權杖的參與者,也必須持有能建立有效權杖的密鑰。這對於單一、自包含的應用程式在其內部發行與驗證自己的權杖是可行的,但對於典型的 OAuth2/OIDC 情境(一個授權伺服器與多個獨立資源伺服器)來說並不適合,因為將簽署密鑰散發給每個需要驗證權杖的 API,等於讓其中任何一個 API 都能偽造權杖。
alg: none 攻擊——以及為什麼演算法確認很重要
{ "alg": "none", "typ": "JWT" }
進入全螢幕模式 離開全螢幕模式
JWT 規範在技術上允許 alg 為 "none",表示完全沒有簽章——如果驗證者盲目信任權杖標頭所聲稱的演算法(而非強制使用預期的演算法),就可能被誘騙接受完全未簽署、可任意偽造的權杖。第 10 節會深入探討此攻擊以及相關的演算法替換攻擊——簡言之:驗證者必須始終強制執行所期望的特定演算法,絕不能單純信任權杖自行回報的 alg 標頭。
3. 「驗證」實際檢查的項目
正確實作的 JWT 驗證程序會檢查數個截然不同的項目,略過其中任何一項都會破壞整個驗證:
1. 結構有效性 — 這是否確實是格式正確的 JWT(三個 base64url 區段)?
2. 簽章驗證 — 是否由我們信任的金鑰簽署,且自簽署後內容未被竄改?
3. 發行者 (iss) — 這是否來自我們真正信任的授權伺服器?
4. 對象 (aud) — 這枚權杖是否專門發行給「這個」API,而非其他 API?
5. 過期時間 (exp) — 這枚權杖的有效時間視窗是否已過?
6. 生效時間 (nbf) — 這枚權杖是否在生效時間之前就被使用?
7. 演算法確認 — 簽署演算法是否確實為我們所期望,而非攻擊者選擇的?
進入全螢幕模式 離開全螢幕模式
現代 JWT 函式庫(第 6 節)在正確設定時會自動處理所有七項檢查——但了解每項檢查的個別意義,對於正確設定函式庫以及辨識自訂或手寫驗證實作是否遺漏重要項目,都至關重要。
4. 簽章驗證與金鑰輪替
取得公鑰
GET https://authserver.com/.well-known/jwks.json
進入全螢幕模式 離開全螢幕模式
{
"keys": [
{
"kid": "abc123",
"kty": "RSA",
"use": "sig",
"n": "0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4cbbfAAt...",
"e": "AQAB"
}
]
}
進入全螢幕模式 離開全螢幕模式
JWKS(JSON Web Key Set) 端點會發布授權伺服器目前用於簽署的公鑰——驗證者會抓取此文件(通常會快取一段合理時間,而非每次請求都抓取),並使用與權杖 kid 標頭相符的金鑰來驗證簽章。
金鑰輪替存在的原因,以及為什麼 kid 很重要
舊金鑰 (kid: "abc123"): 正在逐步淘汰,對於輪替前發行的權杖仍有效,直到其自然過期
新金鑰 (kid: "def456"): 正在主動簽署新權杖
進入全螢幕模式 離開全螢幕模式
授權伺服器會定期輪替其簽署金鑰,這是一項安全最佳實務——限制單一金鑰的使用時間,並在金鑰遭到洩漏時提供乾淨的復原途徑。由於使用舊金鑰簽署的權杖在其自然過期前仍有效,因此在輪替期間,JWKS 端點通常會同時發布多把目前有效的金鑰,而每個權杖中的 kid 標頭則會告訴驗證者,應該使用已發布金鑰中的哪一把來驗證該特定權杖。
快取金鑰,但不要永久快取
options.TokenValidationParameters.ConfigurationManager =
new ConfigurationManager<OpenIdConnectConfiguration>(metadataAddress, retriever)
{
AutomaticRefreshInterval = TimeSpan.FromHours(24),
};
進入全螢幕模式 離開全螢幕模式
在每次收到請求時都抓取 JWKS 文件會浪費資源並增加不必要的延遲——驗證者會將抓取的金鑰快取一段合理時間,但需要定期重新整理(且最好能在權杖參考到未辨識的 kid 時立即重新整理),以免授權伺服器端的合法金鑰輪替導致 API 端出現大量驗證失敗。設計良好的 OIDC 用戶端函式庫(第 6 節)會自動處理此重新整理邏輯——這正是手寫實作中容易出錯的細節。
5. 標準宣告驗證
發行者 (iss)
options.TokenValidationParameters.ValidIssuer = "https://login.microsoftonline.com/{tenant-id}/v2.0";
進入全螢幕模式 離開全螢幕模式
確認權杖確實是由應用程式信任的授權伺服器所發行——若無此檢查,僅驗證簽章的驗證者可能會被誘騙接受來自不同、不受信任發行者的完全有效簽署權杖,只要該發行者的公鑰能以某種方式取得即可(這在多租戶或設定錯誤的情境中是真實存在的風險,詳見第 10 節)。
對象 (aud)
options.TokenValidationParameters.ValidAudience = "api://my-api";
進入全螢幕模式 離開全螢幕模式
確認權杖是專門為「這個 API」發行,而非為其他恰好信任同一個授權伺服器的 API 或用戶端應用程式發行。這是必須正確設定的最重要檢查之一——若無此檢查,原本合法發行給其他內部 API 的權杖(但由同一個受信任的授權伺服器簽署)可能被重放攻擊到你的 API,並通過簽章/發行者驗證,因為這兩項檢查都會真正成功;只有對象檢查能捕捉到這種特定情況。
過期時間 (exp) 與生效時間 (nbf)
exp: 1721404800 — 此 Unix 時間戳記之後權杖即失效
nbf: 1721401200 — 此 Unix 時間戳記之前權杖即失效(較少見,但用於為未來使用而發行的權杖)
進入全螢幕模式 離開全螢幕模式
直接的時間視窗檢查——但值得注意的是,大多數驗證函式庫會套用小的時鐘偏差容忍度(通常為數分鐘),以適應發行伺服器與驗證伺服器之間的微小時鐘漂移,而非要求時鐘精確同步到秒。
options.TokenValidationParameters.ClockSkew = TimeSpan.FromMinutes(5); // .NET 預設值
進入全螢幕模式 離開全螢幕模式
過度寬鬆的時鐘偏差容忍度會大幅延長已過期(或尚未生效)權杖仍可能被接受的實際時間視窗——在真正高安全性的情境中,值得刻意調低此值,而非在未經考量的情況下保留過度寬鬆的預設值。
主體 (sub)
var userId = context.Principal?.FindFirstValue(ClaimTypes.NameIdentifier);
進入全螢幕模式 離開全螢幕模式
通常不會像發行者/對象那樣針對預期值進行「驗證」,但這是應用程式應該用來作為內部已驗證使用者穩定、標準識別碼的宣告——切勿使用 email 或 username 宣告,因為這些在使用者生命週期中可能會改變,而 sub 通常不會。
6. 在 ASP.NET Core 中驗證 JWT
JWT Bearer 驗證處理常式
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://login.microsoftonline.com/{tenant-id}/v2.0";
options.Audience = "api://my-api";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ClockSkew = TimeSpan.FromMinutes(2),
};
});
app.UseAuthentication();
app.UseAuthorization();
進入全螢幕模式 離開全螢幕模式
設定 Authority 會觸發 ASP.NET Core 自動抓取 OIDC 探索文件與 JWKS 端點(第 4 節)並保持其更新——第 3 節中的七項檢查隨後會在每次收到請求時自動執行,無需手寫任何簽章驗證或宣告檢查邏輯。
在端點上要求驗證
app.MapGet("/products", () => GetProducts()).RequireAuthorization();
進入全螢幕模式 離開全螢幕模式
[Authorize]
public class ProductsController : ControllerBase { }
進入全螢幕模式 離開全螢幕模式
RequireAuthorization()(minimal API)或 [Authorize](MVC 控制器)才是真正強制要求特定端點必須存在有效且通過驗證的權杖——僅設定 AddJwtBearer 處理常式本身不會拒絕未驗證的請求;必須在應要求驗證的特定端點上搭配這些授權要求。
明確強制執行預期演算法
options.TokenValidationParameters.ValidAlgorithms = new[] { "RS256" };
進入全螢幕模式 離開全螢幕模式
如第 2 節與第 10 節所述,明確限制可接受的簽署演算法(而非信任權杖標頭聲稱的任何演算法)是一項低成本但值得的安全強化措施,特別是在驗證程式碼路徑可能跨多個身分提供者或具有不同演算法預期的設定共用時。
7. 自訂驗證邏輯
新增超出標準檢查的應用程式特定檢查
options.Events = new JwtBearerEvents
{
OnTokenValidated = async context =>
{
var tenantId = context.Principal?.FindFirstValue("tid");
if (tenantId != _expectedTenantId)
{
context.Fail("Token issued for an unexpected tenant.");
return;
}
var userId = context.Principal?.FindFirstValue(ClaimTypes.NameIdentifier);
if (await _userService.IsUserSuspendedAsync(userId!))
{
context.Fail("User account is suspended.");
}
}
};
進入全螢幕模式 離開全螢幕模式
OnTokenValidated 事件會在所有標準密碼學與宣告檢查成功後觸發,提供一個掛鉤來進行額外的、應用程式特定的驗證——確認多租戶權杖確實屬於預期的租戶、根據自己的資料庫檢查使用者的目前帳戶狀態(這是純粹的密碼學權杖驗證無法得知的)、或從其他來源查詢額外宣告來豐富產生的 ClaimsPrincipal。
為什麼這很重要:密碼學有效性不等於「此請求是否應被允許」
權杖可能通過第 3 節中的所有檢查——確實由受信任的發行者簽署、對象正確、未過期——但仍可能代表不應被接受的請求,因為使用者的帳戶在權杖發行後五分鐘就被停權,或因為授權伺服器不知道的其他業務規則。自訂驗證邏輯正是用來填補這些缺口的地方,值得刻意思考哪些應用程式特定檢查應放在這裡,而不是假設「權杖已驗證」等同於「此請求應被允許」。
8. 權杖撤銷:JWT 的根本限制
核心衝突
JWT 是以無狀態方式驗證——這正是其吸引力所在,讓 API 能在每次請求時本地驗證權杖的真實性,而無需回傳至授權伺服器進行網路往返(詳見本系列的 OAuth2/OpenID Connect 指南)。但相同的特性也意味著,一旦發行,JWT 就會在其自然過期前保持密碼學有效,即使授權伺服器非常希望立即撤銷它(使用者登出、管理員停用帳戶、偵測到權杖被竊)。
權杖已發行,exp:1 小時後
5 分鐘後:使用者帳戶被停權
...但權杖在剩餘的 55 分鐘內仍保持密碼學有效,除非採取額外措施
進入全螢幕模式 離開全螢幕模式
緩解措施 1:保持存取權杖生命週期短
存取權杖生命週期:15 分鐘(常見且合理的預設值)
進入全螢幕模式 離開全螢幕模式
最常見且最簡單的緩解措施就是不要讓這個視窗太大——短生命週期的存取權杖能限制「無法再信任,但尚未技術性過期」的權杖可被使用的時間,代價是需要更頻繁地更新權杖(雖然對使用者不可見,會透過本系列 OAuth2/OpenID Connect 指南中提及的更新權杖來處理)。
緩解措施 2:在自訂驗證中檢查撤銷/拒絕清單
OnTokenValidated = async context =>
{
var jti = context.Principal?.FindFirstValue("jti"); // 唯一權杖識別碼
if (await _revocationStore.IsRevokedAsync(jti!))
{
context.Fail("Token has been revoked.");
}
}
進入全螢幕模式 離開全螢幕模式
對於無法容忍等待即使是短的過期視窗的情境(例如偵測到帳戶遭到入侵後立即撤銷存取權限),維護一個明確的撤銷清單——在自訂驗證期間(第 7 節)透過權杖的唯一 jti(JWT ID)宣告進行檢查——會重新引入 JWT 原本設計要避免的有狀態、每次請求檢查,但僅針對這個特定、較窄的需求,而非權杖中的每個宣告。
緩解措施 3:使用不透明權杖滿足真正即時撤銷的需求
如本系列 OAuth2/OpenID Connect 指南所述,不透明權杖(透過每次請求呼叫授權伺服器的 introspection 端點進行驗證)完全避開了這個限制,因為授權伺服器可以立即停止辨識已撤銷的權杖——直接代價是重新引入 JWT 原本就是要避免的網路往返與授權伺服器可用性依賴。在 JWT 與不透明權杖之間做選擇,在很大程度上就是關於此權衡的選擇:具有有界撤銷延遲的本地、快速、無狀態驗證, versus 具有每次請求依賴的集中式、立即可撤銷驗證。
9. 跨服務驗證 JWT
gRPC/微服務情境
// 每個內部服務都使用相同的共享 Authority 獨立驗證 JWT
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://internal-idp.mycompany.com";
options.Audience = "api://internal-services";
});
進入全螢幕模式 離開全螢幕模式
在微服務架構中(連結至本系列的 gRPC 與背景服務指南),每個獨立服務都可以使用相同的共享授權伺服器的公鑰獨立驗證收到的 JWT——任何服務都不需要直接信任其他服務,或維護自己獨立的憑證存放區;它們都針對同一個中央真實來源進行驗證,這正是 JWT 型驗證適合大規模內部服務對服務通訊的特性之一。
服務之間的權杖傳遞與重新發行
用戶端 → API 閘道(驗證使用者權杖) → 服務 A(重新驗證?還是信任閘道?)
進入全螢幕模式 離開全螢幕模式
值得刻意做出的設計決策:內部服務是否會重新驗證已通過 API 閘道或其他上游服務的權杖,還是信任上游驗證已經發生?在每個躍點重新驗證更具防禦性(防範上游服務遭到入侵或設定錯誤),但會增加延遲與複雜度;信任上游閘道的驗證較簡單,但會將信任(與風險)集中在該閘道。許多組織使用服務網格(如本系列 Kubernetes/Helm 指南中所述),在服務之間使用相互 TLS,這樣內部服務對服務呼叫就會帶有自己的傳輸層信任,某種程度上獨立於每個躍點是否會分別重新驗證原始使用者的 JWT。
服務鏈的 On-Behalf-Of 流程
使用者權杖 → 服務 A 將其交換為新的權杖,範圍限定為代表使用者呼叫服務 B
進入全螢幕模式 離開全螢幕模式
對於中間服務需要以原始使用者身份呼叫下游服務的請求鏈(而非僅以自己身份),OAuth2 的On-Behalf-Of 流程允許服務將收到的權杖交換為適合下一個躍點的新權杖——將原始使用者的身分保留在整個鏈中,而不是中間服務僅使用自己的服務對服務憑證並失去原始請求實際針對的對象。
10. 常見弱點
alg: none 與演算法混淆攻擊
// ❌ 易受攻擊:盲目信任權杖自行宣告的演算法
var algorithm = tokenHeader["alg"];
// 若演算法 == "none",則完全跳過簽章檢查——災難性後果
// ✅ 安全:驗證者無論權杖聲稱為何,都會強制執行明確的預期演算法
options.TokenValidationParameters.ValidAlgorithms = new[] { "RS256" };
進入全螢幕模式 離開全螢幕模式
除了第 2 節提到的 alg: none 攻擊外,另一個相關的演算法混淆攻擊針對同時支援對稱式(HS256)與非對稱式(RS256)演算法的驗證者:知道 RS256 公鑰的攻擊者有時可以利用該公鑰作為 HMAC 密鑰,以 HS256 簽署偽造權杖——如果驗證者沒有嚴格強制執行所期望的特定演算法,它可能會錯誤地使用它視為共享密鑰的東西來驗證偽造的 HS256 簽章,而那個「密鑰」其實一直都是公開已知的值。現代、維護良好的 JWT 函式庫預設會防範這兩種攻擊,但這正是手寫 JWT 驗證邏輯比使用已建立、積極維護的函式庫更具風險的微妙問題。
缺少對象驗證
// ❌ 僅驗證簽章與發行者——接受原本發行給完全不同 API 的權杖
options.TokenValidationParameters.ValidateAudience = false;
進入全螢幕模式 離開全螢幕模式
如第 5 節所述,跳過對象驗證意味著原本合法發行給不同 API 的權杖(但由同一個受信任的授權伺服器發行)可能被重放攻擊到你的 API——這是一個非常常見的設定錯誤,有時是在除錯驗證問題時引入的(「讓我們先停用這個檢查以使其運作」),之後卻再也沒有重新啟用。
信任用戶端提供的宣告而未進行伺服器端驗證
// ❌ 切勿信任用戶端可以未經簽署/驗證而提供的角色/權限宣告
var isAdmin = Request.Headers["X-User-Role"] == "admin";
// ✅ 僅信任來自已密碼學驗證權杖本身的宣告
var isAdmin = User.HasClaim("role", "admin");
進入全螢幕模式 離開全螢幕模式
任何授權決策都必須基於來自已驗證、已簽署 JWT 的實際宣告——而非來自用戶端可以輕易設定為任何值的未簽署標頭或參數。這在孤立來看似乎顯而易見,但卻是在逐步演進的系統中非常常見的錯誤,其中「臨時」除錯標頭或用戶端提供的參數悄然影響到真正的授權決策。
未驗證權杖類型/用途
// 刷新權杖、ID 權杖與存取權杖都可以是形狀相似的 JWT——
// 沒有什麼能阻止用戶端錯誤地(或惡意地)將錯誤的權杖傳送給期望特定類型的端點
進入全螢幕模式 離開全螢幕模式
如本系列 OAuth2/OpenID Connect 指南中所述,ID 權杖與存取權杖服務於不同目的,且通常具有不同的預期對象——但如果 API 沒有特別檢查收到的權杖宣告是否符合其自身用途的預期(正確的對象、存在預期的範圍),它就有可能接受原本根本不是為該用途發行的權杖,即使該權杖在其他方面都是完全有效的簽署權杖。
在多租戶情境中過度寬鬆的 ValidIssuers/ValidAudiences
// ❌ 接受來自此身分提供者下任何租戶的權杖,而非僅接受來自你租戶的權杖
options.TokenValidationParameters.ValidIssuer = null;
options.TokenValidationParameters.ValidateIssuer = false;
進入全螢幕模式 離開全螢幕模式
在建構於共享身分提供者(如 Microsoft Entra ID 的多租戶應用程式註冊)之上的多租戶 SaaS 情境中,停用或過度擴大發行者驗證以「讓多租戶運作」可能會無意中允許來自任何租戶使用者的權杖對應用程式進行驗證,而不僅限於已實際佈建/同意的租戶——多租戶驗證需要一個經過深思熟慮的、明確的有效租戶發行者允許清單(或在自訂驗證中進行等效的租戶 ID 宣告檢查,第 7 節),而非單純停用檢查。
11. 除錯驗證失敗
讀取實際失敗原因
options.Events = new JwtBearerEvents
{
OnAuthenticationFailed = context =>
{
_logger.LogWarning(context.Exception, "JWT validation failed");
return Task.CompletedTask;
}
};
進入全螢幕模式 離開全螢幕模式
預設情況下,ASP.NET Core 的 JWT Bearer 處理常式不會將詳細原因提供給用戶端(這是一個良好的安全預設——你通常不希望告訴攻擊者他們的偽造權杖被拒絕的確切原因),但在伺服器端記錄實際例外狀況對於在開發期間與生產事件回應中診斷合法驗證問題至關重要。
常見失敗原因及其指示
| 錯誤 | 可能原因 |
|---|---|
IDX10223: Lifetime validation failed. The token is expired |
存取權杖自然過期——用戶端應已更新它 |
IDX10214: Audience validation failed |
權杖是為不同於驗證它的 API/用戶端發行 |
IDX10205: Issuer validation failed |
權杖來自非預期或不受信任的授權伺服器 |
IDX10501: Signature validation failed. Unable to match key |
權杖中的 kid 與任何目前快取的金鑰都不相符——可能是驗證者尚未重新整理的金鑰輪替 |
IDX10223 相關的時鐘錯誤 |
發行伺服器與驗證伺服器之間的時鐘偏差超過設定的容忍度 |
手動解碼權杖以進行檢查(絕非用於生產驗證)
# 僅為了在除錯期間進行人工檢查而分割並 base64 解碼承載區段
echo "<payload-segment>" | base64 -d | jq
進入全螢幕模式 離開全螢幕模式
手動解碼權杖的宣告(透過 jwt.io 等工具,或快速的 shell 單行指令)是在除錯期間確認權杖實際包含哪些宣告的有用、純粹診斷步驟——這絕對不能取代應用程式程式碼中的實際密碼學驗證,且任何生產程式碼路徑都不應將僅經解碼、未經驗證的權杖視為可信任。
快速參考表
| 概念 | 用途 |
|---|---|
| 標頭 / 承載 / 簽章 | JWT 的三個組成部分 |
kid |
識別使用哪把特定的簽署金鑰,以支援金鑰輪替 |
RS256/ES256(非對稱式) |
OAuth2/OIDC 的標準——一個發行者簽署,多個驗證者檢查 |
HS256(對稱式) |
共享密鑰簽署,一般不適合多方 OAuth2 情境 |
iss 驗證 |
確認權杖來自受信任的授權伺服器 |
aud 驗證 |
確認權杖是專門為此 API 發行 |
exp/nbf 驗證 |
確認權杖在其預期的有效時間視窗內 |
| JWKS 端點 | 發布驗證簽章所需的公鑰 |
OnTokenValidated |
在標準檢查之外進行自訂、應用程式特定驗證的掛鉤 |
撤銷清單 / jti
|
為需要即時撤銷的情境重新引入有狀態檢查 |
| 演算法混淆攻擊 | 利用不嚴格強制執行預期簽署演算法的驗證者 |
結論
JWT 驗證從外觀看來看似簡單——檢查簽章、讀取一些宣告——但正確實作的驗證者實際上正在悄悄執行七項不同且各自重要的檢查,其中幾項(對象驗證、演算法強制執行、多租戶情境中的發行者限制)正是那些在除錯壓力下意外被弱化或停用,且之後再也沒有恢復的檢查。與本系列其他安全相關指南一致的實務建議是:使用已建立、積極維護的函式庫(ASP.NET Core 的 AddJwtBearer,由 Microsoft.IdentityModel.Tokens 提供支援),而不是手寫簽章驗證或宣告檢查,讓它透過 Authority 自動處理金鑰輪替與探索,並使用其擴充點(OnTokenValidated)進行純粹密碼學驗證無法自行得知的真正應用程式特定檢查——例如帳戶狀態或租戶範圍。
了解在這兩三行設定底下實際發生的事情,是將「驗證中介軟體正在擲出錯誤」從謎團轉變為可解決問題的關鍵,也是辨識看似合理的驗證設定是否悄然停用了本不應停用的檢查的關鍵。
覺得這篇文章有用嗎?歡迎為儲存庫按星號、開啟議題提出更正,或分享讓你學到永遠不要為了讓錯誤消失而停用檢查的對象驗證錯誤。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.