ASP.NET Core 8 JWT 驗證完整教學

在前後端分離、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 88.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
AudienceToken 預計給誰使用
KeyJWT Signature 使用的 Secret Key
ExpireMinutesToken 有效時間

例如:

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

都會比較容易擴充與維護。