在前後端分離、Web API、App Backend 或系統對系統 API 中,JWT(JSON Web Token) 是非常常見的驗證方式。
ASP.NET Core 8 本身已經提供完整的 Authentication / Authorization 架構,只要搭配:
Microsoft.AspNetCore.Authentication.JwtBearer
就可以讓 API 驗證 JWT Bearer Token。
Microsoft 官方文件也建議 API 在驗證 JWT 時至少完整檢查:
- Token Signature
- Issuer
- Audience
- Expiration
只要其中任一項驗證失敗,就應視為無效 Token。
這篇文章將從零開始建立一個:
使用者登入
↓
產生 JWT
↓
Client 保存 Token
↓
Authorization: Bearer {token}
↓
ASP.NET Core 驗證 JWT
↓
[Authorize]
↓
允許呼叫 API
的完整 ASP.NET Core 8 JWT 範例。
一、JWT 是什麼?
JWT 全名是:
JSON Web Token
一個 JWT 通常看起來像:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxIiwidW5pcXVlX25hbWUiOiJhZG1pbiJ9
.
xxxxxxxxxxxxxxxxxxxxxxxx
它由三個部分組成:
Header.Payload.Signature
也就是:
Header
Payload
Signature
Header
記錄 Token 使用的演算法,例如:
{
"alg": "HS256",
"typ": "JWT"
}
Payload
放置 Claims,例如:
{
"sub": "10001",
"unique_name": "admin",
"role": "Admin",
"permission": "parking.read",
"exp": 1780000000
}
Signature
Signature 用來驗證:
這個 Token 是不是由我們信任的系統簽發,而且內容是否被修改過。
例如使用:
HMAC SHA256
搭配 Server 上保存的 Secret Key。
二、一個很重要的觀念:JWT 不是加密
很多人第一次使用 JWT 時會誤以為:
Payload 放進 JWT 之後別人就看不到。
這是錯的。
JWT Payload 通常只是經過 Base64Url 編碼,而不是加密,因此拿到 Token 的人基本上可以讀取 Payload。
所以不要把以下資訊直接放進 JWT:
Password
信用卡號
API Key
資料庫密碼
個資敏感資料
JWT 的 Signature 保證的是:
Token 沒有被竄改
而不是:
Token 裡面的資料沒有人看得到
三、建立 ASP.NET Core 8 Web API
假設建立一個專案:
dotnet new webapi -n JwtDemo
進入專案:
cd JwtDemo
安裝 JWT Bearer 套件:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer --version 8.0.30
本文撰寫時,NuGet 的 .NET 8 套件線已有 8.0.30;實際建立專案時,建議使用當時最新且符合 .NET 8 的 8.0.x 安全更新版本。
四、專案結構
這篇文章不會把所有 JWT 邏輯全部塞進 Controller。
比較推薦的結構是:
JwtDemo
│
├─ Controllers
│ ├─ AuthController.cs
│ └─ SecureController.cs
│
├─ Models
│ ├─ LoginRequest.cs
│ └─ LoginResponse.cs
│
├─ Options
│ └─ JwtOptions.cs
│
├─ Services
│ ├─ IJwtTokenService.cs
│ └─ JwtTokenService.cs
│
├─ appsettings.json
│
└─ Program.cs
也就是:
Controller
↓
IJwtTokenService
↓
JwtTokenService
再利用 ASP.NET Core DI 注入 Controller。
這樣 Controller 不需要知道 JWT 到底怎麼簽章,也不需要自己讀取 Secret Key。
五、設定 appsettings.json
首先加入 JWT 設定:
{
"Jwt": {
"Issuer": "JwtDemoApi",
"Audience": "JwtDemoClient",
"Key": "ThisIsMySuperSecretJwtKey2026-PleaseChangeIt",
"ExpireMinutes": 30
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
各欄位的意思:
| 設定 | 說明 |
|---|---|
| Issuer | 誰簽發這個 Token |
| Audience | Token 預計給誰使用 |
| Key | JWT Signature 使用的 Secret Key |
| ExpireMinutes | Token 有效時間 |
例如:
Issuer = JwtDemoApi
Audience = JwtDemoClient
代表:
這顆 Token 是 JwtDemoApi 簽發
而且預計提供給 JwtDemoClient 使用
六、正式環境不要直接把 Key 放進 appsettings.json
文章為了方便測試才直接寫:
"Key": "ThisIsMySuperSecretJwtKey2026-PleaseChangeIt"
正式環境不要把 Secret Key Commit 到 Git。
比較好的方式可以使用:
Environment Variable
User Secrets
Azure Key Vault
AWS Secrets Manager
其他 Secret Management 系統
例如 ASP.NET Core Environment Variable:
Jwt__Key
對應:
{
"Jwt": {
"Key": "..."
}
}
另外,如果使用 HS256,Secret Key 建議至少使用足夠長度的隨機值,不要使用:
123456
abcdefg
companyname
password
這種可預測的字串。
七、建立 JwtOptions
建立:
Options/JwtOptions.cs
程式碼:
namespace JwtDemo.Options
{
/// <summary>
/// JWT 相關設定。
/// 對應 appsettings.json 中的 Jwt 區段。
/// </summary>
public class JwtOptions
{
/// <summary>
/// appsettings.json 的 Section 名稱。
/// </summary>
public const string SectionName = "Jwt";
/// <summary>
/// JWT 簽發者。
/// </summary>
public string Issuer { get; set; } = string.Empty;
/// <summary>
/// JWT 使用者/接收者。
/// </summary>
public string Audience { get; set; } = string.Empty;
/// <summary>
/// JWT HMAC 簽章 Secret Key。
/// 正式環境請勿直接存放於原始碼。
/// </summary>
public string Key { get; set; } = string.Empty;
/// <summary>
/// JWT 有效時間,單位為分鐘。
/// </summary>
public int ExpireMinutes { get; set; } = 30;
}
}
這樣 Service 就不需要到處寫:
_configuration["Jwt:Issuer"]
_configuration["Jwt:Audience"]
_configuration["Jwt:Key"]
而是可以透過:
IOptions<JwtOptions>
取得 Strongly Typed Configuration。
八、建立 LoginRequest
建立:
Models/LoginRequest.cs
namespace JwtDemo.Models
{
/// <summary>
/// 登入 Request。
/// </summary>
public class LoginRequest
{
/// <summary>
/// 使用者帳號。
/// </summary>
public string UserName { get; set; } = string.Empty;
/// <summary>
/// 使用者密碼。
/// </summary>
public string Password { get; set; } = string.Empty;
}
}
九、建立 LoginResponse
建立:
Models/LoginResponse.cs
namespace JwtDemo.Models
{
/// <summary>
/// 登入成功後回傳的 JWT 資訊。
/// </summary>
public class LoginResponse
{
/// <summary>
/// JWT Access Token。
/// </summary>
public string AccessToken { get; set; } = string.Empty;
/// <summary>
/// Token 類型。
/// JWT API 通常使用 Bearer。
/// </summary>
public string TokenType { get; set; } = "Bearer";
/// <summary>
/// Token 過期時間。
/// 使用 UTC 時間。
/// </summary>
public DateTime ExpiresAt { get; set; }
}
}
十、建立 IJwtTokenService
建立:
Services/IJwtTokenService.cs
namespace JwtDemo.Services
{
/// <summary>
/// JWT Token Service 介面。
/// </summary>
public interface IJwtTokenService
{
/// <summary>
/// 建立 JWT Access Token。
/// </summary>
/// <param name="userId">使用者 ID。</param>
/// <param name="userName">使用者名稱。</param>
/// <param name="role">使用者角色。</param>
/// <param name="permissions">使用者權限清單。</param>
/// <returns>JWT Token 與過期時間。</returns>
(string Token, DateTime ExpiresAt) GenerateToken(
string userId,
string userName,
string role,
IEnumerable<string>? permissions = null);
}
}
為什麼需要 Interface?
因為 Controller 只需要知道:
IJwtTokenService
而不需要知道實際實作。
未來如果要改成:
RSA
ECDSA
IdentityServer
Microsoft Entra ID
其他 Token Service
Controller 不需要跟著大改。
十一、建立 JwtTokenService
建立:
Services/JwtTokenService.cs
完整程式碼如下:
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
using JwtDemo.Options;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
namespace JwtDemo.Services
{
/// <summary>
/// 負責產生 JWT Access Token。
/// </summary>
public class JwtTokenService : IJwtTokenService
{
private readonly JwtOptions _jwtOptions;
/// <summary>
/// 建構式。
/// 透過 ASP.NET Core Options Pattern 取得 JWT 設定。
/// </summary>
/// <param name="jwtOptions">JWT 設定。</param>
public JwtTokenService(IOptions<JwtOptions> jwtOptions)
{
_jwtOptions = jwtOptions.Value;
}
/// <summary>
/// 建立 JWT Access Token。
/// </summary>
/// <param name="userId">使用者 ID。</param>
/// <param name="userName">使用者名稱。</param>
/// <param name="role">使用者角色。</param>
/// <param name="permissions">使用者權限。</param>
/// <returns>JWT Token 與過期時間。</returns>
public (string Token, DateTime ExpiresAt) GenerateToken(
string userId,
string userName,
string role,
IEnumerable<string>? permissions = null)
{
// JWT 建議使用 UTC 時間。
DateTime now = DateTime.UtcNow;
// 計算 Token 過期時間。
DateTime expiresAt = now.AddMinutes(_jwtOptions.ExpireMinutes);
// 將 Secret Key 轉成 byte[]。
byte[] keyBytes = Encoding.UTF8.GetBytes(_jwtOptions.Key);
// 建立對稱式 Secret Key。
var securityKey = new SymmetricSecurityKey(keyBytes);
// 指定 JWT Signature 使用 HMAC SHA256。
var signingCredentials = new SigningCredentials(
securityKey,
SecurityAlgorithms.HmacSha256);
// 建立 JWT Claims。
var claims = new List<Claim>
{
// sub:
// Subject,通常用來保存使用者唯一識別碼。
new Claim(
JwtRegisteredClaimNames.Sub,
userId),
// unique_name:
// 保存使用者名稱。
new Claim(
JwtRegisteredClaimNames.UniqueName,
userName),
// jti:
// 每一顆 JWT 都產生不同的唯一識別碼。
new Claim(
JwtRegisteredClaimNames.Jti,
Guid.NewGuid().ToString()),
// iat:
// Token 建立時間。
// JWT 時間格式使用 Unix Time Seconds。
new Claim(
JwtRegisteredClaimNames.Iat,
new DateTimeOffset(now)
.ToUnixTimeSeconds()
.ToString(),
ClaimValueTypes.Integer64),
// role:
// ASP.NET Core 可以利用這個 Claim 做 Role Authorization。
new Claim(
"role",
role)
};
// 如果有 Permission,將每一個 Permission 都加入 Claim。
if (permissions != null)
{
foreach (string permission in permissions)
{
claims.Add(new Claim("permission", permission));
}
}
// 建立 JWT。
var token = new JwtSecurityToken(
issuer: _jwtOptions.Issuer,
audience: _jwtOptions.Audience,
claims: claims,
notBefore: now,
expires: expiresAt,
signingCredentials: signingCredentials);
// 將 JwtSecurityToken 轉成實際的 JWT 字串。
string tokenString =
new JwtSecurityTokenHandler().WriteToken(token);
return (tokenString, expiresAt);
}
}
}
這裡 JWT 最核心的地方就是:
var token = new JwtSecurityToken(
issuer: _jwtOptions.Issuer,
audience: _jwtOptions.Audience,
claims: claims,
notBefore: now,
expires: expiresAt,
signingCredentials: signingCredentials);
它會負責建立:
Header
Payload
Signature
最後:
new JwtSecurityTokenHandler().WriteToken(token);
才會產生我們平常看到的:
xxxxx.yyyyy.zzzzz
十二、Claims 到底是什麼?
Claims 可以理解成:
這顆 Token 對「目前身分」所宣告的資訊。
例如:
{
"sub": "10001",
"unique_name": "admin",
"role": "Admin",
"permission": [
"parking.read",
"parking.write"
]
}
我們可以把:
sub
當作 UserId。
把:
role
當作角色。
把:
permission
當作細部權限。
因此 API 可以做到:
只有登入者可以進入
只有 Admin 可以進入
只有 parking.read 可以進入
只有 parking.write 可以進入
ASP.NET Core Authorization 本身就支援 Role 與 Policy 模型,而 Policy 可以透過 Claims 建立更細緻的授權條件。
十三、Program.cs 設定 JWT Authentication
這裡是整個 JWT 驗證最重要的地方。
完整 Program.cs:
using System.Text;
using JwtDemo.Options;
using JwtDemo.Services;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
var builder = WebApplication.CreateBuilder(args);
// ============================================================
// Controller
// ============================================================
builder.Services.AddControllers();
// ============================================================
// JWT Options
// ============================================================
builder.Services
.AddOptions<JwtOptions>()
.Bind(builder.Configuration.GetSection(JwtOptions.SectionName))
// Issuer 不可為空。
.Validate(
options => !string.IsNullOrWhiteSpace(options.Issuer),
"Jwt:Issuer 不可為空。")
// Audience 不可為空。
.Validate(
options => !string.IsNullOrWhiteSpace(options.Audience),
"Jwt:Audience 不可為空。")
// Secret Key 不可為空。
.Validate(
options => !string.IsNullOrWhiteSpace(options.Key),
"Jwt:Key 不可為空。")
// HS256 建議至少準備 256 bits,也就是 32 bytes。
.Validate(
options => Encoding.UTF8.GetByteCount(options.Key) >= 32,
"Jwt:Key 長度至少需要 32 bytes。")
// Token 有效時間必須大於 0。
.Validate(
options => options.ExpireMinutes > 0,
"Jwt:ExpireMinutes 必須大於 0。")
// Application 啟動時立即驗證設定。
.ValidateOnStart();
// ============================================================
// JWT Token Service
// ============================================================
// JwtTokenService 透過 DI 注入 Controller。
// 不建議 Controller 自己 new JwtTokenService。
builder.Services.AddScoped<IJwtTokenService, JwtTokenService>();
// ============================================================
// Authentication
// ============================================================
// 從 Configuration 取得 JWT 設定。
JwtOptions jwtOptions =
builder.Configuration
.GetSection(JwtOptions.SectionName)
.Get<JwtOptions>()
?? throw new InvalidOperationException("找不到 JWT 設定。");
// 建立 JWT Secret Key。
var signingKey = new SymmetricSecurityKey(
Encoding.UTF8.GetBytes(jwtOptions.Key));
builder.Services
.AddAuthentication(options =>
{
// 指定預設 Authentication Scheme 為 Bearer。
options.DefaultAuthenticateScheme =
JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme =
JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
// 不自動將 JWT Claim 名稱轉換成 Microsoft ClaimTypes。
// 例如 role 就維持 role。
options.MapInboundClaims = false;
// JWT 驗證規則。
options.TokenValidationParameters =
new TokenValidationParameters
{
// ====================================================
// Issuer
// ====================================================
// 驗證 JWT 簽發者。
ValidateIssuer = true,
// Token 的 iss 必須等於此值。
ValidIssuer = jwtOptions.Issuer,
// ====================================================
// Audience
// ====================================================
// 驗證 JWT Audience。
ValidateAudience = true,
// Token 的 aud 必須等於此值。
ValidAudience = jwtOptions.Audience,
// ====================================================
// Lifetime
// ====================================================
// 驗證 Token 是否過期。
ValidateLifetime = true,
// 不保留額外時間誤差。
//
// TokenValidationParameters 預設 ClockSkew 為 5 分鐘。
// 如果希望 Token 到期後立即失效,可以設定為 Zero。
ClockSkew = TimeSpan.Zero,
// ====================================================
// Signature
// ====================================================
// 驗證 JWT Signature。
ValidateIssuerSigningKey = true,
// 用來驗證 Signature 的 Secret Key。
IssuerSigningKey = signingKey,
// ====================================================
// Claims Mapping
// ====================================================
// User.Identity.Name 使用 unique_name。
NameClaimType = "unique_name",
// [Authorize(Roles = "...")] 使用 role。
RoleClaimType = "role"
};
});
// ============================================================
// Authorization
// ============================================================
builder.Services.AddAuthorization(options =>
{
// 建立只有 Admin 可以使用的 Policy。
options.AddPolicy(
"AdminOnly",
policy =>
{
policy.RequireRole("Admin");
});
// 建立停車場讀取權限。
options.AddPolicy(
"ParkingRead",
policy =>
{
policy.RequireClaim(
"permission",
"parking.read");
});
// 建立停車場寫入權限。
options.AddPolicy(
"ParkingWrite",
policy =>
{
policy.RequireClaim(
"permission",
"parking.write");
});
});
// ============================================================
// Application
// ============================================================
var app = builder.Build();
app.UseHttpsRedirection();
// Authentication 一定要在 Authorization 前面。
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
ASP.NET Core 是透過 AddAuthentication() 註冊 Authentication Scheme,再利用 AddJwtBearer() 加入 JWT Bearer Handler;Middleware 部分則必須讓 Authentication 在需要使用登入資訊的 Authorization 之前執行。
十四、為什麼要 ValidateIssuerSigningKey?
這段:
ValidateIssuerSigningKey = true,
IssuerSigningKey = signingKey
非常重要。
因為 JWT Payload 本身任何人都可以自己建立。
攻擊者完全可以自己產生:
{
"sub": "1",
"unique_name": "hacker",
"role": "Admin"
}
問題是:
這顆 Token 並沒有我們 Server 的 Secret Key,因此產生不出正確 Signature。
所以 Server 驗證:
Signature
失敗之後,就會拒絕 Token。
這也是為什麼 Secret Key 絕對不能外洩。
十五、為什麼要驗證 Issuer?
設定:
ValidateIssuer = true,
ValidIssuer = jwtOptions.Issuer
代表 API 會檢查:
"iss": "JwtDemoApi"
是不是符合我們所信任的 Token 簽發者。
避免接受來自其他不受信任系統簽發的 Token。
十六、為什麼要驗證 Audience?
設定:
ValidateAudience = true,
ValidAudience = jwtOptions.Audience
代表:
就算 Token 是真的,也必須確認這顆 Token 本來就是要給「這個 API」使用。
例如:
Token A
Audience = HR-System
Token B
Audience = Parking-System
停車場 API 不應該因為 Token A Signature 正確,就接受原本發給 HR System 的 Token。
因此 Audience 也是 JWT 驗證中很重要的一環。
十七、ClockSkew 為什麼設定 TimeSpan.Zero?
我們設定:
ClockSkew = TimeSpan.Zero
因為 IdentityModel 的 TokenValidationParameters 預設 ClockSkew 是:
300 秒
也就是五分鐘。
假設 Token:
10:30:00 到期
如果使用預設 ClockSkew,驗證時間可能會容許一些時間誤差。
如果你的需求是:
10:30:00 到期
10:30:01 就必須失效
就可以設定:
ClockSkew = TimeSpan.Zero;
不過在多台 Server 的環境,也要確保系統時間有正常同步。
十八、建立 AuthController
接下來建立:
Controllers/AuthController.cs
using JwtDemo.Models;
using JwtDemo.Services;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace JwtDemo.Controllers
{
/// <summary>
/// Authentication API。
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IJwtTokenService _jwtTokenService;
/// <summary>
/// 建構式。
/// JwtTokenService 由 ASP.NET Core DI Container 注入。
/// </summary>
/// <param name="jwtTokenService">JWT Token Service。</param>
public AuthController(IJwtTokenService jwtTokenService)
{
_jwtTokenService = jwtTokenService;
}
/// <summary>
/// 使用帳號密碼登入並取得 JWT。
///
/// 注意:
/// 這裡為了示範 JWT,因此直接使用固定帳號密碼。
/// 正式環境必須改成資料庫、AD、LDAP、
/// ASP.NET Core Identity 或其他真正的驗證機制。
/// </summary>
/// <param name="request">登入資料。</param>
/// <returns>JWT Access Token。</returns>
[AllowAnonymous]
[HttpPost("login")]
public IActionResult Login([FromBody] LoginRequest request)
{
// ========================================================
// DEMO ONLY
// ========================================================
//
// 正式環境絕對不要直接這樣寫帳號密碼。
// 應該查詢資料庫並驗證 Password Hash。
//
// ========================================================
bool isValidUser =
request.UserName == "admin" &&
request.Password == "123456";
if (!isValidUser)
{
return Unauthorized(new
{
code = "INVALID_CREDENTIALS",
message = "帳號或密碼錯誤。"
});
}
// 模擬資料庫查出的使用者資訊。
string userId = "10001";
string userName = request.UserName;
string role = "Admin";
string[] permissions =
{
"parking.read",
"parking.write"
};
// 透過 Service 產生 JWT。
(string token, DateTime expiresAt) =
_jwtTokenService.GenerateToken(
userId,
userName,
role,
permissions);
// 回傳 Token。
var response = new LoginResponse
{
AccessToken = token,
TokenType = "Bearer",
ExpiresAt = expiresAt
};
return Ok(response);
}
}
}
這裡有一個很重要的架構觀念。
不要在 Controller 裡面寫:
new JwtSecurityToken(...)
new SymmetricSecurityKey(...)
new SigningCredentials(...)
Controller 的責任應該只是:
收到 Request
↓
驗證資料
↓
呼叫 Service
↓
回傳 Response
真正的 JWT 產生邏輯放:
JwtTokenService
再利用 DI:
private readonly IJwtTokenService _jwtTokenService;
這會比所有邏輯全部塞 Controller 更容易維護。
十九、測試登入 API
呼叫:
POST /api/auth/login
Content-Type: application/json
Body:
{
"userName": "admin",
"password": "123456"
}
成功後:
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"tokenType": "Bearer",
"expiresAt": "2026-09-07T03:30:00Z"
}
Client 接下來呼叫需要驗證的 API 時,必須把 Token 放到:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
注意中間有一個空白:
Bearer {Token}
二十、建立需要登入才能使用的 API
建立:
Controllers/SecureController.cs
using System.IdentityModel.Tokens.Jwt;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
namespace JwtDemo.Controllers
{
/// <summary>
/// JWT 驗證測試 API。
/// </summary>
[ApiController]
[Route("api/[controller]")]
public class SecureController : ControllerBase
{
/// <summary>
/// 只要 JWT 驗證成功即可呼叫。
/// </summary>
[Authorize]
[HttpGet("profile")]
public IActionResult GetProfile()
{
// 取得 JWT sub。
string? userId =
User.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
// 因為 Program.cs 設定:
//
// NameClaimType = "unique_name"
//
// 所以可以直接使用 User.Identity.Name。
string? userName = User.Identity?.Name;
// 取得 Role。
string? role =
User.FindFirst("role")?.Value;
// 取得所有 Permission。
string[] permissions =
User.FindAll("permission")
.Select(claim => claim.Value)
.ToArray();
return Ok(new
{
userId,
userName,
role,
permissions
});
}
/// <summary>
/// 只有 Admin Role 可以呼叫。
/// </summary>
[Authorize(Roles = "Admin")]
[HttpGet("admin")]
public IActionResult AdminOnly()
{
return Ok(new
{
message = "你擁有 Admin 權限。"
});
}
/// <summary>
/// 使用 Policy 驗證 Admin。
/// </summary>
[Authorize(Policy = "AdminOnly")]
[HttpGet("admin-policy")]
public IActionResult AdminPolicy()
{
return Ok(new
{
message = "AdminOnly Policy 驗證成功。"
});
}
/// <summary>
/// 必須擁有 parking.read Claim。
/// </summary>
[Authorize(Policy = "ParkingRead")]
[HttpGet("parking")]
public IActionResult GetParking()
{
return Ok(new
{
message = "你擁有 parking.read 權限。"
});
}
/// <summary>
/// 完全不需要 JWT。
/// </summary>
[AllowAnonymous]
[HttpGet("public")]
public IActionResult Public()
{
return Ok(new
{
message = "這支 API 不需要 JWT。"
});
}
}
}
二十一、[Authorize] 是什麼?
最基本的方式:
[Authorize]
代表:
只要 Authentication 成功就可以進入。
也就是 JWT 必須:
Signature 正確
Issuer 正確
Audience 正確
尚未過期
二十二、[AllowAnonymous] 是什麼?
如果 API 不需要登入:
[AllowAnonymous]
例如:
[AllowAnonymous]
[HttpPost("login")]
public IActionResult Login(...)
{
}
登入 API 本身當然不能要求 JWT。
不然會變成:
要登入
↓
必須先有 JWT
↓
但是要拿 JWT
↓
又必須先登入
直接陷入無限輪迴。
二十三、使用 Role 控制權限
JWT 裡面加入:
{
"role": "Admin"
}
Program.cs 設定:
RoleClaimType = "role";
Controller 就可以使用:
[Authorize(Roles = "Admin")]
例如:
[Authorize(Roles = "Admin")]
[HttpDelete("{id}")]
public IActionResult Delete(int id)
{
return Ok();
}
ASP.NET Core 支援以 Role 進行宣告式 Authorization。
二十四、不要什麼權限都塞 Role
小系統可能只有:
Admin
User
Role 就很好用。
但大型系統如果出現:
ParkingReadAdmin
ParkingWriteAdmin
ParkingDeleteAdmin
ParkingExportAdmin
VisitorReadAdmin
VisitorWriteAdmin
...
Role 很快就會爆炸。
這時比較推薦:
Role
+
Permission
例如:
{
"role": "User",
"permission": [
"parking.read",
"parking.write"
]
}
二十五、使用 Policy + Claim 做權限
Program.cs:
builder.Services.AddAuthorization(options =>
{
options.AddPolicy(
"ParkingRead",
policy =>
{
policy.RequireClaim(
"permission",
"parking.read");
});
});
Controller:
[Authorize(Policy = "ParkingRead")]
[HttpGet]
public IActionResult GetParking()
{
return Ok();
}
只有 JWT 裡面存在:
{
"permission": "parking.read"
}
才會通過。
官方的 Policy-Based Authorization 也是以 Requirements 組成 Policy,再由框架進行授權判定。
二十六、如何從 Controller 取得目前登入者
JWT 通過 Authentication 之後,ASP.NET Core 會建立:
HttpContext.User
也就是:
User
因此可以直接:
string? userName = User.Identity?.Name;
取得 UserId:
string? userId =
User.FindFirst(JwtRegisteredClaimNames.Sub)?.Value;
取得 Role:
string? role =
User.FindFirst("role")?.Value;
取得 Permission:
string[] permissions =
User.FindAll("permission")
.Select(x => x.Value)
.ToArray();
也就是說,不需要每一支 Controller 自己 Decode JWT。
這種寫法:
var handler = new JwtSecurityTokenHandler();
var token = handler.ReadJwtToken(...);
不應該在每支 API 裡一直重複。
JWT Middleware 已經幫我們驗證並建立:
HttpContext.User
直接使用即可。
二十七、Authentication 與 Authorization 不一樣
這兩個概念很容易混淆。
Authentication
回答:
你是誰?
例如:
JWT 是否有效?
這是不是 User 10001?
ASP.NET Core:
AddAuthentication()
AddJwtBearer()
UseAuthentication()
主要就是處理 Authentication。
Authorization
回答:
你可以做什麼?
例如:
你是不是 Admin?
有沒有 parking.read?
可不可以刪除停車資料?
ASP.NET Core:
AddAuthorization()
[Authorize]
Policy
Role
Claim
主要處理 Authorization。
官方文件同樣將 Authentication 與 Authorization 視為兩個不同概念:Authentication 確認身分,而 Authorization 決定該身分可以執行什麼操作。
二十八、401 與 403 有什麼不同?
這也是開發 JWT API 時非常重要的觀念。
401 Unauthorized
通常代表:
Authentication 失敗
例如:
沒有 Bearer Token
Token 格式錯誤
Signature 錯誤
Token 過期
Issuer 錯誤
Audience 錯誤
API 回:
401 Unauthorized
意思比較接近:
我無法確認你的身分。
403 Forbidden
代表:
Authentication 成功
但是 Authorization 失敗
例如 Token:
{
"role": "User"
}
但 API:
[Authorize(Roles = "Admin")]
這時使用者確實已登入:
Authentication = Success
但是沒有 Admin 權限:
Authorization = Failed
所以:
403 Forbidden
換句話說:
401 = 你是誰我不知道
403 = 我知道你是誰,但你不能進來
二十九、JWT API 的完整流程
最後把整個流程串起來。
Step 1:登入
Client:
POST /api/auth/login
送:
{
"userName": "admin",
"password": "123456"
}
Step 2:Server 驗證帳號
實際系統可能查:
SQL Server
Oracle
AD
LDAP
ASP.NET Core Identity
其他 Identity Provider
Step 3:建立 Claims
例如:
sub = 10001
unique_name = admin
role = Admin
permission = parking.read
permission = parking.write
Step 4:產生 JWT
JwtTokenService
使用:
Secret Key
+
HMAC SHA256
簽署 Token。
Step 5:Client 保存 Access Token
取得:
eyJhbGciOiJIUzI1Ni...
Step 6:呼叫 API
Client:
GET /api/secure/profile
Authorization: Bearer eyJhbGciOiJIUzI1Ni...
Step 7:JwtBearer Middleware 驗證
ASP.NET Core 驗證:
Signature
Issuer
Audience
Lifetime
Microsoft 對 JWT Bearer API 的建議也是完整驗證 Signature、Issuer、Audience 與 Expiration。
Step 8:建立 HttpContext.User
JWT Claims 轉成:
ClaimsPrincipal
Controller 可以:
User.Identity
User.Claims
User.IsInRole(...)
Step 9:[Authorize] 判斷
例如:
[Authorize]
或:
[Authorize(Roles = "Admin")]
或:
[Authorize(Policy = "ParkingRead")]
最後決定是否可以執行 API。
三十、內部系統也可以使用 API Key 換 JWT
有些情境不是:
User + Password
而是:
System A
↓
ClientId + ApiKey
↓
Token API
↓
JWT
↓
Protected API
例如:
停車系統
訪客系統
HR 系統
MES
內部 Server-to-Server API
可以設計:
POST /api/auth/generate-token
X-Client-Id: Parking-System
X-Api-Key: xxxxxxxxx
Controller 驗證 ClientId 與 ApiKey 成功之後:
(string token, DateTime expiresAt) =
_jwtTokenService.GenerateToken(
userId: "Parking-System",
userName: "Parking-System",
role: "System",
permissions: new[]
{
"parking.read"
});
回傳:
{
"accessToken": "eyJhbGciOi...",
"tokenType": "Bearer",
"expiresAt": "2026-09-07T03:30:00Z"
}
後續再:
Authorization: Bearer {token}
呼叫真正的 API。
這種架構的好處是:
API Key
只用於:
取得短效 JWT
而不是每一支業務 API 都一直傳 API Key。
不過如果系統規模較大、涉及第三方或需要標準化授權流程,應優先評估 OAuth 2.0 / OpenID Connect 與專門的 Identity Provider,而不是自行發明一套認證協定。
三十一、JWT Token 有效時間應該設定多久?
沒有一個所有系統都適用的答案。
例如內部 API 可以考慮:
15 分鐘
30 分鐘
60 分鐘
不建議為了方便直接:
30 天
90 天
365 天
因為 JWT 一旦被竊取,在 Token 過期之前,攻擊者也可能使用它。
通常會搭配:
短效 Access Token
+
Refresh Token
例如:
Access Token = 30 分鐘
Refresh Token = 7 天
Access Token 過期後:
Client
↓
Refresh Token
↓
Server
↓
新的 Access Token
這會比一顆超長效 JWT 更容易控制風險。
三十二、JWT 可以直接登出嗎?
這是 JWT 很常被問到的問題。
傳統 Session 可以:
SessionId
↓
Server 刪除 Session
↓
立刻失效
但 JWT 本身通常是:
Stateless
例如:
{
"sub": "10001",
"exp": 1780000000
}
只要:
Signature 正確
而且還沒過期
Server 原則上就會接受。
因此如果系統需要:
強制登出
立即撤銷 Token
帳號停權後 Token 立即失效
就需要額外設計,例如:
Token Blacklist
jti Blacklist
User TokenVersion
Redis Revocation List
Refresh Token Rotation
JWT 並不代表系統完全不需要任何 Server-side 狀態。
三十三、為什麼 JWT 裡加入 jti?
我們建立 Token 時加入:
new Claim(
JwtRegisteredClaimNames.Jti,
Guid.NewGuid().ToString())
jti 可以理解成:
JWT ID
每一顆 Token 都有不同 ID。
例如:
Token A
jti = 111111
Token B
jti = 222222
未來如果要做:
Token Blacklist
Token Revocation
Token 使用記錄
資安稽核
就可以利用 jti 識別某一顆 Token。
三十四、常見錯誤:Token 明明過期卻還能用
例如設定:
ExpireMinutes = 1
結果一分鐘後 Token 看起來還能使用。
其中一個常見原因就是:
ClockSkew
預設存在時間容錯。
因此如果需求是嚴格到期:
ClockSkew = TimeSpan.Zero;
IdentityModel 官方 API 文件記載 DefaultClockSkew 為 300 秒,也就是五分鐘。
三十五、常見錯誤:一直收到 401
如果:
401 Unauthorized
可以依序檢查:
1. Authorization Header 有沒有送?
2. 是否為:
Authorization: Bearer {token}
3. Bearer 後面有沒有空白?
4. Token 是否已過期?
5. Issuer 是否一致?
6. Audience 是否一致?
7. API 與發 Token 的地方是否使用同一組 Key?
8. Signature Algorithm 是否一致?
9. UseAuthentication() 是否有設定?
10. UseAuthentication() 是否在 UseAuthorization() 前面?
三十六、常見錯誤:一直收到 403
403 通常不是 JWT 壞掉。
反而代表:
JWT 多半已經驗證成功
應該檢查:
Role
Permission
Policy
Claims
例如:
[Authorize(Roles = "Admin")]
但 Token:
{
"role": "User"
}
就會得到:
403 Forbidden
三十七、常見錯誤:Controller 自己解析 JWT
不建議每支 API 都:
string token = Request.Headers.Authorization
.ToString()
.Replace("Bearer ", "");
var handler = new JwtSecurityTokenHandler();
var jwtToken = handler.ReadJwtToken(token);
因為:
ReadJwtToken
只是讀取 Token,不代表:
Token 已經安全驗證成功。
正確方式應該讓:
JwtBearer Authentication Handler
統一驗證 Token。
Controller 使用:
User
即可。
Microsoft 的 JwtBearerHandler 本來就是負責驗證 Bearer Token 並從 Claims 建立使用者 Identity。
三十八、正式環境 JWT 安全檢查表
最後整理幾個實務上很重要的項目。
1. 一定使用 HTTPS
不要讓 JWT 在:
HTTP
明文網路中傳輸。
2. Secret Key 不要 Commit Git
不要:
"Jwt": {
"Key": "ProductionSecretKey"
}
然後 Push GitHub / GitLab。
3. Secret Key 要有足夠強度
不要使用:
123456
company
password
secret
4. 驗證 Signature
ValidateIssuerSigningKey = true;
5. 驗證 Issuer
ValidateIssuer = true;
6. 驗證 Audience
ValidateAudience = true;
7. 驗證 Expiration
ValidateLifetime = true;
8. Access Token 不要設定過長
建議採取:
短效 Access Token
+
必要時搭配 Refresh Token
9. JWT 不放敏感資訊
因為 Payload 可以被讀取。
10. 權限不要只依賴前端
這種做法:
if (user.role === "Admin") {
showDeleteButton();
}
只是在控制 UI。
真正安全性一定要由 Backend:
[Authorize(Roles = "Admin")]
或:
[Authorize(Policy = "ParkingWrite")]
再次判斷。
三十九、總結
ASP.NET Core 8 建立 JWT Authentication,核心可以分成六個部分:
1. appsettings.json
↓
JWT Issuer / Audience / Key
2. JwtTokenService
↓
產生 JWT
3. Claims
↓
UserId / Role / Permission
4. AddAuthentication + AddJwtBearer
↓
驗證 JWT
5. UseAuthentication
↓
建立 HttpContext.User
6. [Authorize]
↓
控制 API 權限
整體架構:
Client
│
│ Login / ClientId + ApiKey
▼
AuthController
│
▼
IJwtTokenService
│
▼
JwtTokenService
│
│ Generate JWT
▼
Access Token
│
│ Authorization: Bearer {token}
▼
ASP.NET Core JwtBearer
│
├─ Verify Signature
├─ Verify Issuer
├─ Verify Audience
└─ Verify Expiration
│
▼
HttpContext.User
│
▼
[Authorize]
│
├─ Role
├─ Claim
└─ Policy
│
▼
Controller / API
如果只是要記住最重要的一件事:
Controller 不需要自己解析 JWT,也不建議自己建立 JWT;產生 Token 抽成 Service,驗證 Token 交給 ASP.NET Core JwtBearer Middleware。
這樣整個 JWT 架構會乾淨很多:
Authentication 負責「你是誰」
Authorization 負責「你能做什麼」
JWT Service 負責「如何產生 Token」
Controller 負責「業務 API」
把這幾個責任分開之後,未來不管要加入:
Refresh Token
Redis Token Blacklist
API Key 換 JWT
多系統 Audience
Role / Permission
Microsoft Entra ID
OAuth 2.0
OpenID Connect
都會比較容易擴充與維護。