ASP.NET Core JWT 的兩種實作:官方 IdentityModel vs jwt-dotnet
ASP.NET Core JWT 的兩種實作:官方 IdentityModel vs jwt-dotnet
前言
JWT 大家都在用,但「怎麼產生」這件事其實有不只一種寫法。
早期我手刻過一版——自己組 header、payload,自己做 Base64Url 編碼,自己用 HMACSHA256 算簽名。
能動是能動,但每次要加個 claim、換個演算法都得回去改一堆字串拼接,心裡總是毛毛的。

後來改用套件,又面臨一個選擇題:
要用微軟官方的 Microsoft.IdentityModel,還是社群很受歡迎的 jwt-dotnet?
這篇就把這兩套放在一起比。重點是——它們在我的專案裡實作的是同一份介面 IJsonWebTokenService,
所以可以靠 DI 一行切換引擎,呼叫端完全無感。
同一份合約
兩種實作都遵守同一個介面,這是整篇能比較的前提:
public interface IJsonWebTokenService
{
Task<string> GenerateAccessToken(Dictionary<string, object> claims, int expireMinutes = 3000);
Task<JwtParseResult> ParseAccessToken(string accessToken);
Task<JwtParseResult> ValidateAccessToken(string accessToken, bool verify = true);
Task<string> GenerateRefreshToken();
// ...
}要換引擎,只要改 DI 註冊那一行:
// 用微軟官方套件
services.AddSingleton<IJsonWebTokenService, JwtIdentityService>();
// 想換成 jwt-dotnet,改成這行即可,呼叫端零修改
// services.AddSingleton<IJsonWebTokenService, JwtDotNetService>();這就是抽象介面的價值:底層套件是可以抽換的實作細節,不該污染到上層商業邏輯。
產生 Token
::: code-group
public async Task<string> GenerateAccessToken(Dictionary<string, object> claims, int expireMinutes = 3000)
{
var encodedSecretKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(_authenticationConfigModel.SecretKey));
var signingCredentials = new SigningCredentials(
encodedSecretKey, SecurityAlgorithms.HmacSha256);
var expireTime = DateTimeOffset.UtcNow.AddMinutes(expireMinutes).ToUnixTimeSeconds();
var payload = new Dictionary<string, object>(claims)
{
[JwtRegisteredClaimNames.Iss] = _authenticationConfigModel.Issuer,
[JwtRegisteredClaimNames.Exp] = expireTime,
[JwtRegisteredClaimNames.Jti] = Guid.NewGuid().ToString(),
};
var tokenDescriptor = new SecurityTokenDescriptor
{
Claims = payload,
SigningCredentials = signingCredentials
};
var tokenHandler = new JsonWebTokenHandler();
return tokenHandler.CreateToken(tokenDescriptor);
}public async Task<string> GenerateAccessToken(Dictionary<string, object> claims, int expireMinutes = 3000)
{
var secretKey = _authenticationConfigModel.SecretKey;
var expireTime = DateTimeOffset.UtcNow.AddMinutes(expireMinutes).ToUnixTimeSeconds();
var payload = new Dictionary<string, object>(claims)
{
[ClaimName.Issuer.GetDescription()] = _authenticationConfigModel.Issuer,
[ClaimName.ExpirationTime.GetDescription()] = expireTime,
[ClaimName.JwtId.GetDescription()] = Guid.NewGuid().ToString(),
};
IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
IJsonSerializer serializer = new JsonNetSerializer();
IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
return encoder.Encode(payload, secretKey);
}:::
差別一眼就看得出來:
| 面向 | 官方 IdentityModel | jwt-dotnet |
|---|---|---|
| 產生方式 | SecurityTokenDescriptor + JsonWebTokenHandler,框架包好 | 自己組 JwtEncoder(algorithm, serializer, urlEncoder) |
| Claim 名稱 | JwtRegisteredClaimNames.Exp 強型別常數 | ClaimName.ExpirationTime.GetDescription() |
| 抽象程度 | 高,細節藏在 descriptor 裡 | 低,演算法/序列化器/編碼器全攤在眼前 |
jwt-dotnet 的好處是「每一塊都看得見」,學習 JWT 結構時很直覺;
官方版則是「交給框架,少寫少錯」,但你得相信它的預設行為。
驗證 Token
驗證才是兩套設計哲學差最多的地方。
::: code-group
var result = await handler.ValidateTokenAsync(accessToken, validationParameters);
if (result.IsValid)
{
// 成功,從 jsonToken 取出各欄位
}
else
{
// 失敗——但「為什麼失敗」要再自己從 result 挖
return JwtParseResult.Failure(JwtStatus.InvalidSignature);
}try
{
var json = decoder.Decode(accessToken, secretKey, verify);
return JwtParseResult.Success(json, ...);
}
catch (TokenExpiredException) { return JwtParseResult.Failure(JwtStatus.TokenExpired); }
catch (TokenNotYetValidException) { return JwtParseResult.Failure(JwtStatus.TokenNotYetValid); }
catch (SignatureVerificationException){ return JwtParseResult.Failure(JwtStatus.InvalidSignature); }
catch (InvalidTokenPartsException) { return JwtParseResult.Failure(JwtStatus.InvalidTokenParts); }
catch (FormatException) { return JwtParseResult.Failure(JwtStatus.InvalidFormat); }:::
| 面向 | 官方 IdentityModel | jwt-dotnet |
|---|---|---|
| 失敗回饋 | 一個 IsValid 布林 | 每種失敗一個例外型別 |
| 失敗原因粒度 | 粗,要自己判讀 | 細,過期 / 未生效 / 簽章錯 / 格式錯分得清清楚楚 |
對應到自家 JwtStatus | 要手動補判斷 | 例外型別幾乎一對一對應 |
如果你的 API 需要對前端回「token 是過期了還是根本是假的」這種精準訊息,
jwt-dotnet 的例外分流會讓 code 乾淨很多。
踩雷紀錄:exp 的單位
比較的時候發現一個容易出包的點,順手記下來。
JWT 規範裡 exp(過期時間)的單位是 Unix 時間戳的「秒」,不是毫秒。
換套件時如果不小心寫成:
// ❌ 用了毫秒,token 的有效期會被解讀成幾萬年後
var expireTime = DateTimeOffset.UtcNow.AddMinutes(30).ToUnixTimeMilliseconds();
// ✅ 正確:用秒
var expireTime = DateTimeOffset.UtcNow.AddMinutes(30).ToUnixTimeSeconds();毫秒值會比實際時間大上 1000 倍,驗證端若嚴格照規範解讀,token 幾乎等於永不過期——
這是安全性問題,不只是 bug。換實作時務必對齊兩邊的單位。
關於假非同步
兩種實作的方法都標了 async,但簽 token 本質上是 CPU 運算,沒有真正的 I/O 等待。
所以你會看到 await Task.Delay(0) 或 Task.Run(...) 這類「為了符合 async 介面而存在」的包裝。
老實說這不是漂亮的寫法。簽章是同步的 CPU 工作,硬包成 async 只是為了讓介面一致。
知道它「假」就好,不要誤以為它真的有非同步的好處。
小結
官方 Microsoft.IdentityModel | jwt-dotnet | |
|---|---|---|
| 來源 | 微軟官方,與 ASP.NET Core 認證整合最好 | 社群套件,輕量直觀 |
| 風格 | 框架幫你包好,少寫少錯 | 元件攤開,結構透明 |
| 驗證失敗 | 布林 IsValid | 精細的例外型別 |
| 適合 | 一般專案、想跟內建 JwtBearer 一致 | 想精準掌控、需要細緻錯誤回饋 |
最後我留了官方版作為預設(跟 AddJwtBearer 的驗證行為天生一致),
jwt-dotnet 則保留為可隨時抽換的備案——這也正是把它們收斂到同一份介面的意義。