- .NET RESTful API 組成詳解
一個典型的 .NET (更精確地說是 ASP.NET Core) RESTful API 主要由以下幾個核心部分組成:
這是應用程式的起點。在 .NET 6 及更新的版本中,這個檔案會設定並啟動 Web 伺服器、註冊服務、並配置 HTTP 請求管線 (Middleware)。
- 作用:接收傳入的 HTTP 請求並回傳 HTTP 回應。這是 API 的主要工作區域。
- 特徵:
- 通常繼承自
ControllerBase。 - 使用
[ApiController]屬性來啟用標準的 API 行為。 - 使用路由屬性 (如
[Route("api/[controller]")]) 來定義 URL 結構。 - 包含稱為 "Action 方法" 的公開方法,每個方法對應一個或多個 API 端點 (Endpoint)。
- Action 方法使用
[HttpGet],[HttpPost],[HttpPut],[HttpDelete]等屬性來對應 HTTP 動詞。
- 通常繼承自
範例 (ProductsController.cs):
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
// ... 查詢並回傳產品 ...
return Ok(new { ProductId = id, Name = "範例產品" });
}
}- 作用:定義資料的結構。這可以是你要接收的請求資料格式,或是你要回傳的回應資料格式。
- 特徵:
- 通常是簡單的 C# 類別 (POCO - Plain Old CLR Object)。
- 用於模型繫結 (Model Binding),自動將傳入的 JSON/XML 資料轉換為 .NET 物件。
- 也常用於定義與資料庫互動的實體 (Entity)。
範例 (Product.cs):
public class Product
{
public int Id { get; set; }
public string Name { get; set; }
public decimal Price { get; set; }
}- 作用:將傳入請求的 URL 對應到正確的控制器 Action 方法。
- 實現方式:主要透過在控制器或 Action 方法上加上屬性 (Attribute-based routing) 來實現,例如
[Route(...)],[HttpGet(...)]。
- 作用:它是一個處理請求和回應的軟體管線 (pipeline)。每個請求都會依序通過這個管線中的元件,每個元件都可以對請求進行處理,然後將其傳遞給下一個元件,或直接回傳回應。
- 常見用途:
- 驗證 (Authentication):確認使用者是誰。
- 授權 (Authorization):確認使用者有權限執行此操作。
- 記錄 (Logging):記錄請求資訊。
- 錯誤處理 (Exception Handling):捕捉全域的例外狀況。
- CORS (跨來源資源共用):允許來自不同網域的前端應用程式呼叫 API。
- 作用:雖然可以直接在控制器中撰寫所有邏輯,但最佳實踐是將核心的商業邏輯(例如:計算、資料庫操作、呼叫其他 API)抽離到獨立的 "服務" 類別中。
- 優點:
- 關注點分離 (Separation of Concerns):控制器只負責處理 HTTP 相關事務。
- 可重用性:多個控制器可以共用同一個服務。
- 可測試性:可以獨立測試商業邏輯,而不需要模擬 HTTP 請求。
- 作用:ASP.NET Core 內建了強大的 DI 容器。它負責建立和管理物件的生命週期(例如上面提到的服務或資料庫上下文)。
- 實現方式:在
Program.cs中註冊服務 (例如builder.Services.AddScoped<IProductService, ProductService>();),然後在需要它的地方(如控制器的建構函式)直接請求介面,DI 容器會自動提供實例。
- 作用:管理應用程式的設定值,例如資料庫連接字串、API 金鑰等。
- 來源:主要來自
appsettings.json和appsettings.Development.json等檔案。
一個 .NET RESTful API 的請求流程大致如下:
- HTTP 請求到達 Kestrel (內建的 Web 伺服器)。
- 請求進入 Middleware 管線 (執行驗證、記錄等)。
- 路由系統根據 URL 找到對應的 Controller Action。
- 模型繫結將請求內容 (如 JSON) 轉換為 Model 物件。
- Controller 透過 DI 取得所需的 Service 實例。
- Controller 呼叫 Service 來執行商業邏輯。
- Service 可能會與資料庫或其他外部資源互動。
- 執行結果傳回 Controller。
- Controller 將結果包裝成
IActionResult(如Ok(),NotFound(),BadRequest()) 回傳。 - 結果物件被序列化成 JSON,並作為 HTTP 回應傳回給客戶端。
時序圖 (Sequence Diagram)
sequenceDiagram
participant Client as 客戶端
participant ASP_NET_Core as ASP.NET Core 框架 <br> (Kestrel, Middleware, Routing)
participant Controller as 控制器
participant Service as 服務 (商業邏輯)
participant DB as 資料庫
Client->>+ASP_NET_Core: 1. HTTP 請求
Note over ASP_NET_Core: 2. 中介軟體管線處理 <br> 3. 路由解析
ASP_NET_Core->>+Controller: 4. 呼叫對應的 Action 方法 <br> (含模型繫結)
Note over Controller, Service: 5. 透過 DI 取得 Service
Controller->>+Service: 6. 執行商業邏輯
Service->>+DB: 7. 與資料庫互動
DB-->>-Service: 回傳資料
Service-->>-Controller: 8. 回傳執行結果
Controller-->>-ASP_NET_Core: 9. 回傳 IActionResult (例如 Ok(data))
ASP_NET_Core->>-Client: 10. 將結果序列化為 JSON 並回傳
我們可以將一個 RESTful API 想像成一家餐廳,這有助於區分哪些部分是給客戶(對外)看的,哪些是支撐餐廳運作(對內)的。
- 對外 (Public-Facing): 這是客人(Client 端)能看到和互動的部分,像是菜單 (API 合約) 和 服務生 (Controller)。
- 對內 (Internal): 這是餐廳的後場,客人看不到,但支撐著整個運作,像是廚房 (業務邏輯)、廚師 (Service) 和 儲藏室 (資料庫)。
這些是 API 的消費者(例如前端網頁、手機 App 或其他後端服務)會直接接觸到的部分。它們定義了 API 的「長相」和「如何使用」。外部世界只應該知道「有哪些網址可以用」以及「傳遞的 JSON 格式長什麼樣子」。
| 元件 (Component) | 說明 | 在專案中的位置 (典型) |
|---|---|---|
| 控制器 (Controllers) | API 的入口。它接收 HTTP 請求 (GET, POST, PUT, DELETE),並決定如何回應。Controller 應該保持輕薄,像個交通警察,只負責接收請求、呼叫內部的服務來處理,然後回傳結果。 | Controllers/ 資料夾 |
| 路由 (Routing) | 定義了哪個 URL 對應到哪個 Controller 的哪個方法 (Action)。例如 GET /api/products/123 會對應到 ProductsController 的 GetById(123) 方法。 |
通常使用屬性 (Attribute) 直接定義在 Controller 的方法上,例如 [HttpGet("{id}")]。 |
| 資料傳輸物件 (DTOs) | Data Transfer Objects。這是專門為了在 API 的請求 (Request) 和回應 (Response) 中傳遞資料而設計的類別。使用 DTOs 可以避免將內部的資料庫結構 (Entity) 直接暴露給外部,增加安全性與彈性。 | Models/ 或 DTOs/ 資料夾 |
| API 文件 (Swagger/OpenAPI) | 自動產生的 API 文件,讓前端或使用者能清楚地知道有哪些 API、如何呼叫、需要什麼參數。 | 在 Program.cs 中設定,通常網址是 /swagger。 |
這些是實現 API 功能的核心,外部使用者不需要(也不應該)知道它們的存在與細節。
| 元件 (Component) | 說明 | 在專案中的位置 (典型) |
|---|---|---|
| 業務邏輯層 (Business Logic Layer) | 處理所有核心商業規則的地方。通常會建立所謂的 服務 (Services) 或 管理員 (Managers) 類別來封裝這些邏輯。例如,計算訂單總價、檢查庫存、產生報告等複雜操作都在這一層完成。Controller 會呼叫這些 Service 來執行任務。 | 通常會建立一個 Services/ 或 Business/ 資料夾。 |
| 資料存取層 (Data Access Layer) | 負責與資料庫溝通。它包含了所有讀取、寫入、更新、刪除資料庫資料的程式碼。最常見的是使用 Entity Framework Core,它會包含: - DbContext: 資料庫連線的上下文。 - 實體 (Entities): 直接對應到資料庫資料表的類別。 |
Data/ 或 DataAccess/ 資料夾。Entities 可能在 Models/Entities/。 |
| 組態設定 (Configuration) | 存放應用程式的設定值,例如資料庫連線字串、第三方服務的 API Key、功能開關等。這些是絕對不能對外洩漏的敏感資訊。 | appsettings.json 和 appsettings.Development.json 檔案。 |
| 中介軟體 (Middleware) | 在請求到達 Controller 之前或回應送出之前執行的程式碼片段。常用於處理驗證 (Authentication)、授權 (Authorization)、記錄 (Logging)、錯誤處理等橫切關注點 (Cross-cutting concerns)。 | 在 Program.cs 中透過 app.Use...() 的方式設定。 |
| 依賴注入 (Dependency Injection) | .NET 的核心機制,用來解耦合。在 Program.cs 中註冊服務 (例如 builder.Services.AddScoped<IMyService, MyService>();),然後在需要的地方 (如 Controller) 的建構函式中注入使用,而不用自己 new 一個物件。 |
在 Program.cs 中設定。 |
良好的 API 設計會嚴格區分這兩者。Controller 像一個乾淨的門面,只處理進出的人流與溝通;真正的複雜工作全部交由內部的廚房(業務邏輯層與資料存取層)來完成。這樣的結構讓你的 API 更安全、更容易維護與擴充。
當需要提供 RESTful API 給甲方(第三方合作夥伴)串接時,核心目標是清晰、完整、無歧義,讓對方的開發者可以獨立完成整合工作,盡可能減少來回溝通的成本。
以下是您必須提供的資訊清單,可以把它當作一個交付 checklist:
這是最基本的入口資訊。
- API 基礎 URL (Base URL)
- 測試環境 (Staging/UAT):
https://api.staging.yourcompany.com/ - 正式環境 (Production):
https://api.production.yourcompany.com/ - 務必提供一個功能完整的測試環境,讓甲方可以在不影響正式資料的情況下進行開發與測試。
- 測試環境 (Staging/UAT):
這是確保 API 安全的關鍵,必須描述得非常清楚。
- 驗證方式: 明確告知是哪一種驗證機制。
- API Key: 金鑰要放在 HTTP Header (
X-API-Key: <your_key>) 還是 Query String (?api_key=<your_key>)? - OAuth 2.0: 是哪種流程 (Flow)?例如
Client Credentials Grant。需要提供 Token 的獲取端點 (/oauth/token)、client_id和client_secret。 - JWT Bearer Token: 如何獲取 Token (例如透過登入 API),以及 Token 的有效期限是多久。
- API Key: 金鑰要放在 HTTP Header (
- 如何獲取憑證: 告知甲方需要提供什麼資訊來申請他們的 API Key 或
client_id/secret。 - 如何使用憑證: 提供一個明確的請求範例,展示
Authorization或X-API-Key標頭的確切格式。GET /api/v1/orders/123 Host: api.staging.yourcompany.com Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
一個 JWT (JSON Web Token) 的內容是由三個部分組成的,這三個部分之間用點 (.) 來分隔,形成 xxxxx.yyyyy.zzzzz 這樣的結構。
這三個部分依序是:
- Header (標頭)
- Payload (酬載)
- Signature (簽章)
每一部分都是經過 Base64Url 編碼的字串。重要的是要記住,Base64Url 是編碼,不是加密,所以前兩部分的內容是可以輕易被解碼讀取的。
目的:描述這個 JWT 的元數據(metadata),主要是關於簽章演算法的資訊。
它是一個 JSON 物件,通常包含兩個欄位:
alg(Algorithm): 必須欄位。指定用來產生簽章的演算法,例如HS256(HMAC using SHA-256) 或RS256(RSA using SHA-256)。typ(Type): 建議欄位。宣告這是一個 JWT,其值固定為JWT。
範例 JSON:
{
"alg": "HS256",
"typ": "JWT"
}將這個 JSON 物件進行 Base64Url 編碼後,就形成了 JWT 的第一部分。
目的:存放實際需要傳遞的資料,這些資料被稱為「聲明 (Claims)」。
它也是一個 JSON 物件,包含描述實體(通常是使用者)和其他相關資訊的聲明。聲明可以分為三種類型:
-
註冊聲明 (Registered Claims): 這些是 IETF 官方建議的一組預先定義好的聲明,以提供一組共通的、可互通的標準。
iss(Issuer): Token 的發行者。sub(Subject): Token 的主體,通常是使用者的唯一識別碼(例如 User ID)。aud(Audience): Token 的接收者(你的 API)。exp(Expiration Time): Token 的過期時間戳,這是確保安全性的關鍵聲明。nbf(Not Before): Token 的生效時間戳。iat(Issued At): Token 的發行時間戳。jti(JWT ID): Token 的唯一識別碼,可用於防止重放攻擊。
-
公開聲明 (Public Claims): 由使用 JWT 的人自行定義,但為了避免衝突,應該在 IANA JSON Web Token Registry 中註冊。
-
私有聲明 (Private Claims): 這是最常見的自訂聲明,由發行者和接收者雙方共同協商定義,用來傳遞非標準化但必要的資訊。例如:
role: 使用者角色 (e.g., "Admin", "User")username: 使用者名稱userId: 使用者 ID (如果sub不敷使用)
範例 JSON (混合了註冊聲明和私有聲明):
{
"sub": "1234567890",
"name": "John Doe",
"role": "Admin",
"iat": 1516239022,
"exp": 1516242622
}
⚠️ 安全警告: Payload 僅僅是經過 Base64Url 編碼,任何拿到 Token 的人都可以輕易地將其解碼並讀取內容。因此,絕對不要在 Payload 中存放任何敏感資訊,例如使用者的密碼、信用卡號等。
將這個 Payload JSON 物件進行 Base64Url 編碼後,就形成了 JWT 的第二部分。
目的:驗證 Token 的來源是否可信,並確保訊息在傳輸過程中沒有被竄改。這是 JWT 安全性的基石。
簽章是透過以下方式產生的:
- 取前面已經過 Base64Url 編碼的
Header。 - 加上一個點 (
.)。 - 取前面已經過 Base64Url 編碼的
Payload。 - 將這個組合起來的字串,使用
Header中指定的演算法 (alg) 和一個存放在伺服器端的密鑰 (Secret Key) 進行加密計算。
公式示意:
Signature = HASH_ALGORITHM(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
your_secret_key
)
這個 your_secret_key 是存放在伺服器端的,客戶端絕對不應該知道。當伺服器收到 JWT 時,會用同樣的方式重新計算一次簽章,如果計算結果與收到的簽章一致,就證明 Token 是合法且未被修改的。
將這三部分編碼後的字串用點 (.) 連接起來,就構成了一個完整的 JWT。您可以將範例字串貼到 jwt.io 網站上,它會幫您解碼並展示這三個部分的內容,這是一個學習和偵錯 JWT 的絕佳工具。
JWT (JSON Web Token) 是目前最流行的跨域驗證解決方案之一。它是一個緊湊且獨立的標準,用於在各方之間安全地傳輸資訊 (稱為 "claims")。以下是在 ASP.NET Core 中實作它的完整步驟。
核心流程:
- 使用者登入: 使用者提供帳號密碼。
- 伺服器驗證: 伺服器驗證帳密是否正確。
- 核發 Token: 驗證成功後,伺服器產生一個包含使用者資訊 (Claims) 的 JWT,並將其回傳給客戶端。
- 客戶端儲存 Token: 客戶端 (如瀏覽器) 儲存此 Token。
- 發送請求: 未來所有對受保護資源的請求,客戶端都必須在 HTTP Header 的
Authorization欄位中攜帶此 Token。 - 伺服器驗證 Token: 伺服器收到請求後,會驗證 Token 的簽章是否有效、是否過期,如果驗證通過,則允許存取。
時序圖 (Sequence Diagram):
sequenceDiagram
participant Client as 客戶端 (Client)
participant AuthEndpoint as 認證端點 (/api/auth/login)
participant Middleware as 驗證中介軟體 (Auth Middleware)
participant ProtectedEndpoint as 受保護的端點 (/api/products)
Note over Client, ProtectedEndpoint: 1. 認證與取得 Token
Client->>AuthEndpoint: POST 請求 (帳號/密碼)
activate AuthEndpoint
AuthEndpoint->>AuthEndpoint: 驗證使用者身份
alt 驗證成功
AuthEndpoint->>AuthEndpoint: 產生 JWT
AuthEndpoint-->>Client: 回傳 JWT
else 驗證失敗
AuthEndpoint-->>Client: 回傳 401 Unauthorized
end
deactivate AuthEndpoint
Client->>Client: 儲存 JWT
%% -- 存取階段 --
Note over Client, ProtectedEndpoint: 2. 攜帶 Token 存取資源
Client->>Middleware: GET 請求 (Header: Authorization: Bearer JWT)
activate Middleware
Middleware->>Middleware: 驗證 JWT (簽章、過期時間)
alt JWT 有效
Middleware->>ProtectedEndpoint: 請求合法,轉發至目標端點
activate ProtectedEndpoint
ProtectedEndpoint-->>Middleware: 回傳資源
deactivate ProtectedEndpoint
Middleware-->>Client: 200 OK (回傳資源)
else JWT 無效
Middleware-->>Client: 401 Unauthorized
end
deactivate Middleware
步驟 1: 安裝 NuGet 套件
首先,在你的專案中安裝 JWT Bearer 的官方套件。
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer步驟 2: 設定 appsettings.json
在 appsettings.json 中加入 JWT 相關的設定。金鑰 (Key) 絕對不能外洩,且長度應該足夠長以確保安全。
{
"Jwt": {
"Issuer": "https://yourdomain.com",
"Audience": "https://yourapi.yourdomain.com",
"Key": "THIS IS A SUPER SECRET KEY THAT SHOULD BE LONG AND COMPLEX"
}
}Issuer: Token 的核發者。Audience: Token 的接收者 (你的 API)。Key: 用於簽署 Token 的密鑰。
步驟 3: 在 Program.cs 中設定驗證服務
這是最核心的步驟,我們需要告訴 ASP.NET Core 如何驗證傳入的 JWT。
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
// 1. 從 appsettings.json 讀取 JWT 設定
var jwtSettings = builder.Configuration.GetSection("Jwt");
var key = Encoding.ASCII.GetBytes(jwtSettings["Key"]);
// 2. 加入驗證服務
builder.Services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
// 驗證發行者
ValidateIssuer = true,
ValidIssuer = jwtSettings["Issuer"],
// 驗證接收者
ValidateAudience = true,
ValidAudience = jwtSettings["Audience"],
// 驗證生命週期
ValidateLifetime = true,
// 驗證簽署金鑰
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key)
};
});
// 加入授權服務 (如果需要用到 Roles)
builder.Services.AddAuthorization();
var app = builder.Build();
// 3. 將驗證和授權中介軟體加入管線
// !! 注意:UseAuthentication() 必須在 UseAuthorization() 之前 !!
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();步驟 4: 建立一個端點來核發 Token
我們需要一個 API 端點 (例如 POST /api/auth/login) 來讓使用者登入並獲取 Token。
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly IConfiguration _config;
public AuthController(IConfiguration config)
{
_config = config;
}
[HttpPost("login")]
public IActionResult Login([FromBody] UserLogin model)
{
// **此處應替換為真實的資料庫驗證邏輯**
if (model.Username == "test" && model.Password == "password")
{
var token = GenerateJwtToken(model.Username);
return Ok(new { token });
}
return Unauthorized("Invalid credentials");
}
private string GenerateJwtToken(string username)
{
// 讀取 appsettings.json 中的 JWT 設定
var jwtSettings = _config.GetSection("Jwt");
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings["Key"]));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
// 定義 Claims (Token 中包含的資訊)
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Sub, username),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
// 你可以加入自訂的 Claim,例如角色
new Claim(ClaimTypes.Role, "Admin"),
new Claim("UserId", "12345")
};
// 建立 Token
var token = new JwtSecurityToken(
issuer: jwtSettings["Issuer"],
audience: jwtSettings["Audience"],
claims: claims,
expires: DateTime.Now.AddMinutes(120), // Token 過期時間
signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
}
// 簡單的登入模型
public class UserLogin
{
public string Username { get; set; }
public string Password { get; set; }
}步驟 5: 保護你的 API 端點
現在,你可以使用 [Authorize] 屬性來保護需要驗證才能存取的 Controller 或 Action。
[ApiController]
[Route("api/[controller]")]
public class ProductsController : ControllerBase
{
[HttpGet]
[Authorize] // <-- 加在需要保護的 Action 上
public IActionResult GetProducts()
{
// 因為有 [Authorize],只有攜帶有效 Token 的請求才能進入這裡
// 你還可以從 User Claims 中獲取資訊
var userId = User.FindFirst("UserId")?.Value;
return Ok(new { Message = $"Hello, user {userId}! Here are the products." });
}
[HttpGet("admin")]
[Authorize(Roles = "Admin")] // <-- 只有角色為 Admin 的使用者才能存取
public IActionResult GetAdminData()
{
return Ok(new { Message = "This is admin-only data." });
}
[HttpGet("public")]
public IActionResult GetPublicData()
{
// 這個端點沒有 [Authorize],任何人都可以存取
return Ok(new { Message = "This is public data." });
}
}當您設定好 AddJwtBearer 中介軟體後,每一次帶有 [Authorize] 屬性的 API 請求進來時,該中介軟體都會自動執行一連串的驗證步驟。以下是詳細的驗證流程:
- 是否存在 Token?: 請求的 Header 中是否有
Authorization欄位,且其值是否以Bearer開頭? - Token 結構是否正確?: Token 本身是否為
xxxxx.yyyyy.zzzzz的三段式結構(Header.Payload.Signature),且每段都是合法的 Base64Url 編碼?
如果這些基本檢查失敗,請求會立即被拒絕。
這是確保 Token 未被竄改且來源可信的核心步驟。
- 解析 Token: 中介軟體解析出 Token 的
Header和Payload。 - 取得演算法與密鑰:
- 從
Header中得知簽署時使用的演算法(例如HS256)。 - 從伺服器自身的設定檔 (
appsettings.json) 中讀取那個絕對不能外洩的密鑰 (Secret Key)。
- 從
- 重新計算簽章: 使用相同的演算法和伺服器端的密鑰,對
Header和Payload重新計算一次簽章。 - 比對簽章: 將重新計算出的簽章與 Token 中第三部分的原始簽章 (
Signature) 進行比對。
結果:
- 如果兩者完全一致: 證明這個 Token 確實是由持有正確密鑰的伺服器所簽發的,並且從簽發到現在,其內容(Header 和 Payload)完全沒有被修改過。
- 如果兩者不一致: 表示 Token 要嘛是偽造的,要嘛是被竄改過。請求會立即被拒絕,並回傳
401 Unauthorized。
即使簽章正確,中介軟體還需要檢查 Payload 中的「聲明 (Claims)」是否符合伺服器設定的規則。這確保了 Token 在正確的時間和場景下被使用。
-
a. 過期時間 (
exp):- 檢查
ValidateLifetime = true: 這個設定是否開啟? - 驗證
exp: 取得 Payload 中的exp(Expiration Time) 聲明,確認目前時間是否早於過期時間。如果 Token 已過期,驗證失敗。
- 檢查
-
b. 發行者 (
iss):- 檢查
ValidateIssuer = true: 這個設定是否開啟? - 驗證
iss: 取得 Payload 中的iss(Issuer) 聲明,確認其值是否與伺服器設定的ValidIssuer(例如"https://yourdomain.com") 完全相符。這能確保 Token 是由預期的伺服器發行的。
- 檢查
-
c. 接收者 (
aud):- 檢查
ValidateAudience = true: 這個設定是否開啟? - 驗證
aud: 取得 Payload 中的aud(Audience) 聲明,確認其值是否與伺服器設定的ValidAudience(例如"https://yourapi.yourdomain.com") 完全相符。這能確保這個 Token 是要給「這個 API」使用的,而不是給其他 API 用的,防止 Token 被誤用。
- 檢查
這些驗證規則都是在 Program.cs 中設定的:
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
// 對應步驟 3b: 驗證發行者
ValidateIssuer = true,
ValidIssuer = jwtSettings["Issuer"],
// 對應步驟 3c: 驗證接收者
ValidateAudience = true,
ValidAudience = jwtSettings["Audience"],
// 對應步驟 3a: 驗證生命週期 (過期時間)
ValidateLifetime = true,
// 對應步驟 2: 驗證簽署金鑰
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key) // 這裡提供了驗證簽章時所需的密鑰
};
});整個驗證流程就像是機場的海關查驗:
- 格式檢查: 護照(Token)看起來是真的嗎?格式對不對?
- 簽章驗證: 護照上的防偽標籤和鋼印(Signature)是真的嗎?還是偽造的?
- 聲明驗證:
- 護照是否過期了?(
exp) - 這本護照是由哪個國家發的?(
iss) - 你是不是要用這本護照進入我國?(
aud)
- 護照是否過期了?(
只有當所有檢查都通過,中介軟體才會認為這次請求是合法的,並將 Token 中的 Claims 解析出來賦值給 HttpContext.User。任何一個環節失敗,流程就會被中斷,並回傳 4.0.1 Unauthorized。
客戶端如何使用
- POST
https://yourapi.yourdomain.com/api/auth/login並附上帳號密碼的 JSON。 - 從回應中取得
token。 - 將
token儲存起來。 - 當要請求受保護的資源 (例如
GET /api/products) 時,在 HTTP Header 中加入:Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... (很長的 token 字串)
這樣就完成了一個標準的 JWT Bearer Token 驗證與授權流程。
這是最重要的部分,描述了 API 的所有功能。最佳實踐是提供一個互動式的 Swagger (OpenAPI) 文件連結。
如果沒有 Swagger,則文件中必須包含以下每一個 API 端點 (Endpoint) 的詳細資訊:
- 功能描述: 這個 API 是做什麼的?(例如:"查詢使用者資料"、"建立一筆新訂單")。
- 路徑與方法 (Path & Method): 例如
GET /api/v1/users/{userId}。 - 參數說明 (Parameters):
- 路徑參數 (Path Parameters): 如
{userId},說明其型別 (整數、字串) 和意義。 - 查詢參數 (Query Parameters): 如
?page=1&pageSize=10,說明每個參數的名稱、型別、是否為必填、預設值和作用。 - 請求主體 (Request Body): 對於
POST或PUT請求,提供完整的 JSON 格式範例,並逐一說明每個欄位的名稱、資料型別 (string,number,boolean,array,object)、是否必填,以及格式限制(例如日期格式為YYYY-MM-DD)。
- 路徑參數 (Path Parameters): 如
- 成功回應 (Success Response):
- 提供成功時的 HTTP 狀態碼 (例如
200 OK,201 Created)。 - 提供成功時的回應主體 (Response Body) JSON 範例,並說明每個欄位的意義。
- 提供成功時的 HTTP 狀態碼 (例如
- 失敗回應 (Error Response):
- 說明可能發生的錯誤情境及其對應的 HTTP 狀態碼 (例如
400 Bad Request,401 Unauthorized,404 Not Found,500 Internal Server Error)。 - 提供一個統一的錯誤回應格式,這非常重要。
{ "errorCode": "INVALID_PARAMETER", "message": "The 'email' field is not a valid email address.", "details": "..." } - 說明可能發生的錯誤情境及其對應的 HTTP 狀態碼 (例如
- 程式碼範例 (Code Samples): 提供至少一種常見語言的呼叫範例(例如
cURL,JavaScript (fetch),Python (requests))會非常有幫助。Swagger 通常能自動產生。
這部分幫助甲方更穩定、更有效率地使用您的 API。
- 速率限制 (Rate Limiting): 明確告知允許的請求頻率(例如:每分鐘 100 次請求),以及超過時會收到的 HTTP 狀態碼(通常是
429 Too Many Requests)。 - 版本管理 (Versioning): 說明您的 API 版本策略。版本號是放在 URL (
/api/v1/) 還是 Header 中?當 API 更新時,舊版本會維護多久? - 分頁 (Pagination): 如果有列表類型的 API,請說明如何進行分頁(例如使用
page和pageSize參數)。 - 資料格式: 強調所有請求與回應的
Content-Type都是application/json。
- 技術支援聯絡窗口: 提供一個 email 或支援平台,讓甲方在遇到問題時可以尋求協助。
- 變更日誌 (Changelog): (可選,但很專業) 提供一個頁面記錄 API 的更新與變更歷史。
最理想的交付物是一個「開發者整合包 (Developer Kit)」,其中包含:
- 一份 Word/PDF/Markdown 文件,涵蓋環境、驗證、使用規範、支援方式。
- 一個 Swagger/OpenAPI 文件的線上連結,用於查閱詳細的 API 規格與線上測試。
- 一組專門給該甲方的測試環境憑證 (API Key 或 client_id/secret)。
用一個簡單的譬喻來開頭:
譬喻:主題樂園的 VIP 通行證
- 發行 (Login): 你在樂園門口(認證伺服器)用你的身份(帳號密碼)買了一張 VIP 通行證(JWT)。售票員(伺服器)在通行證上蓋了一個特殊的、無法偽造的防偽印章(Signature),並寫上了你的名字、VIP 等級和有效期限(Payload/Claims)。
- 驗證 (API Request): 當你想玩任何遊樂設施(存取受保護的 API)時,設施管理員(資源伺服器)會檢查你的通行證。他不會重新去門口問你的身份,而是直接檢查:
- 通行證是不是看起來是真的?(格式檢查)
- 上面的防偽印章是不是真的?(簽名驗證)
- 通行證過期了嗎?(Claims 驗證)
如果都沒問題,你就被允許進入設施。
當用戶端(例如瀏覽器或手機 App)帶著 JWT 來請求一個需要保護的 API 時,伺服器會執行以下步驟:
伺服器首先會從請求 (Request) 中取得 JWT。最常見的做法是從 HTTP 的 Authorization 標頭 (Header) 中取得。這個標頭的值通常會長這樣:
Authorization: Bearer <your_jwt_token>
伺服器會解析這個標頭,取出 <your_jwt_token> 的部分。
一個標準的 JWT 是一個由 . 分隔的長字串,包含三個部分:
Header.Payload.Signature
伺服器會先做最基本的檢查,確認這個 Token 是否符合 x.y.z 的格式。如果不符合,這顯然是一個無效的 Token,直接拒絕請求。
JWT 的前兩部分(Header 和 Payload)是使用 Base64Url 編碼的,並不是加密。任何人都可以解碼它們。
- Header (標頭): 通常包含演算法(如
HS256,RS256)和 Token 類型(JWT)。 - Payload (負載): 包含一系列的 "Claims" (聲明),這是關於用戶和 Token 本身的資訊。例如:
sub: Subject (主題),通常是使用者的唯一 ID。name: 使用者名稱。exp: Expiration Time (過期時間),Token 在此時間後失效。iss: Issuer (發行者),是誰發行的這個 Token。aud: Audience (受眾),這個 Token 是給誰用的。
伺服器會解碼這兩部分以讀取其中的內容,準備進行下一步的驗證。
這是整個流程中最核心的安全環節,用來確保 Token 在傳輸過程中沒有被篡改,並且它確實是由合法的伺服器所簽發的。
驗證過程取決於當初簽發時使用的演算法:
-
對稱加密 (如 HS256):
- 伺服器使用儲存在自己內部、絕不外洩的密鑰 (Secret Key)。
- 將收到的
Header和Payload(Base64Url 編碼後的字串) 用.連接起來。 - 使用與簽發時相同的演算法 (例如 HMAC-SHA256) 和同一個密鑰,對這個連接起來的字串進行加密運算,產生一個新的簽名。
- 比對這個新產生的簽名與收到的 JWT 中第三部分的簽名是否完全一致。
-
非對稱加密 (如 RS256):
- 伺服器使用公開的金鑰 (Public Key)。私鑰 (Private Key) 則被安全地存放在簽發 Token 的認證伺服器上。
- 驗證過程類似,但它會用 Public Key 來解開收到的簽名,確認它是否能與 Header 和 Payload 的雜湊值匹配。
如果簽名不匹配,意味著:
- Payload 的內容被惡意修改了(例如,駭客想把自己的使用者 ID 改成管理員的 ID)。
- 這個 Token 是由一個沒有正確密鑰的偽造者所簽發的。
在這種情況下,伺服器會立即拒絕該請求,通常回傳 401 Unauthorized 或 403 Forbidden 的 HTTP 狀態碼。
我們可以把這個過程想像成銀行驗證一張支票。
- 支票上的簽名 (Received Signature): 你拿著一張由 A 公司開給你的支票去銀行兌現。支票上有 A 公司財務長的親筆簽名。這個簽名就是從客戶端傳來的 JWT 中的第三部分——
Signature。 - 銀行存底的印鑑 (Server's Secret Key): 銀行(伺服器)的資料庫裡,儲存了 A 公司開戶時留下的標準印鑑樣本。這個樣本就是伺服器內部儲存、絕不外洩的
Secret Key。 - 驗證過程 (The Comparison): 銀行櫃員(伺服器)不會只看支票上的簽名像不像,他會拿出存底的標準印鑑,跟支票上的金額、收款人等資訊,重新蓋一次在白紙上,然後比對這個新蓋的印章跟支票上原有的簽名是否一模一樣。
伺服器識別簽名不匹配的過程,可以分為以下三個步驟:
步驟 1:拆解 Token
伺服器收到一個 JWT,它看起來像 Header.Payload.Signature。伺服器會把它拆成三個部分:
Header(已編碼)Payload(已編碼)Received_Signature(接收到的簽名)
Received_Signature 是當初發行 Token 時,用 Secret Key 加密前兩部分所產生的。
步驟 2:在本機端「重新計算」一個簽名
這是最關鍵的一步。伺服器會完全忽略接收到的 Received_Signature,並假裝自己是發行者,重新做一次簽名流程:
- 拿出剛才收到的
Header(已編碼)。 - 拿出剛才收到的
Payload(已編碼)。 - 用一個
.把這兩者連接起來,變成一個長字串:Header + "." + Payload。 - 從自己的安全設定中,取出那個絕不外洩的
Secret Key。 - 使用
Header中指定的演算法(例如 HS256),搭配這個Secret Key,對第 3 步的長字串進行加密運算。 - 運算後,伺服器得到了一個全新的、在本地端生成的簽名。我們稱之為
Calculated_Signature。
步驟 3:比對「接收的簽名」與「重新計算的簽名」
現在伺服器手上有兩個簽名:
Received_Signature: 從 Token 中拆解出來的,由客戶端帶來的簽名。Calculated_Signature: 伺服器剛剛用自己的密鑰重新計算出來的簽名。
伺服器會對這兩個簽名進行逐位元 (byte-by-byte) 的精確比對。
-
如果完全匹配 (Match): 這從密碼學上證明了兩件事:
- 真實性 (Authenticity): 這個 Token 確實是由持有正確
Secret Key的一方(也就是合法的伺服器)所簽發的。 - 完整性 (Integrity):
Header和Payload的內容從簽發那一刻到現在,連一個字元都沒有被修改過。因為只要內容有任何一絲變動,重新計算出來的Calculated_Signature就會與原始的完全不同。
- 真實性 (Authenticity): 這個 Token 確實是由持有正確
-
如果不匹配 (Mismatch): 這是一個絕對的危險信號,100% 代表發生了以下兩種情況之一:
- 內容被竄改 (Tampered): 有人在傳輸過程中攔截了 Token,並修改了
Payload的內容。例如,一個普通用戶想把自己的role從"user"改成"admin",或者想延長exp過期時間。他雖然改了 Payload,但他沒有Secret Key,所以無法產生一個能通過驗證的新簽名。 - 來源是偽造的 (Forged): 整個 Token 都是由一個惡意攻擊者產生的。他可以隨意編寫
Header和Payload,但他同樣沒有Secret Key,所以他偽造的簽名,永遠無法和伺服器用「真密鑰」計算出來的簽名相匹配。
- 內容被竄改 (Tampered): 有人在傳輸過程中攔截了 Token,並修改了
結論:
只要出現「簽名不匹配」的情況,伺服器就會立即、無條件地拒絕這次請求,通常會回傳 401 Unauthorized 錯誤,從而保護了後端的 API 資源不會被非法存取。這個比對過程是 JWT 安全機制的基石。
即使簽名驗證通過了,伺服器還需要檢查 Payload 中的標準 Claims,確保 Token 的使用是合規的。
exp(Expiration Time): 檢查 Token 是否已經過期。如果當前時間晚于exp時間,則 Token 失效。nbf(Not Before): 檢查 Token 是否已經生效。如果當前時間早于nbf時間,則 Token 尚未生效。iss(Issuer): 檢查 Token 的發行者是否是預期的發行者。aud(Audience): 檢查 Token 的接收者是否是當前的伺服器或服務。
如果任何一項檢查失敗,請求同樣會被拒絕。
sequenceDiagram
participant Client as 用戶端
participant AuthMiddleware as 伺服器 (驗證中介軟體)
participant ApiEndpoint as API 端點 (Controller Action)
Client->>AuthMiddleware: GET /api/products\n(Header: Authorization: Bearer ey...)
Note right of AuthMiddleware: 伺服器端內部驗證開始
AuthMiddleware->>AuthMiddleware: 1. 取得並解析 Token
AuthMiddleware->>AuthMiddleware: 2. 驗證簽名 (Signature)
AuthMiddleware->>AuthMiddleware: 3. 驗證聲明 (Claims: exp, iss, aud)
alt 驗證全部通過
AuthMiddleware->>ApiEndpoint: 轉發已驗證的請求
ApiEndpoint-->>AuthMiddleware: 處理完畢,回傳結果
AuthMiddleware-->>Client: 200 OK (回傳 API 結果)
else 任何驗證失敗
AuthMiddleware-->>Client: 401 Unauthorized (拒絕請求)
end
時序圖解說:
- 用戶端 (Client) 向伺服器發送一個請求,並在
Authorization標頭中附上 JWT。 - 請求首先被驗證中介軟體 (AuthMiddleware) 截獲。
- 中介軟體執行所有必要的內部驗證:解析 Token、驗證簽名、檢查是否過期等。
- 如果驗證成功 (
alt區塊的上半部):- 中介軟體會將請求轉發給它原本要去的 API 端點 (例如
ProductsController的某個 Action)。 - API 端點處理請求並回傳結果。
- 最終伺服器向用戶端回傳
200 OK及資料。
- 中介軟體會將請求轉發給它原本要去的 API 端點 (例如
- 如果驗證失敗 (
else區塊):- 中介軟體會立即中斷流程,不會將請求傳遞給 API 端點。
- 直接向用戶端回傳
401 Unauthorized錯誤。
這是一個非常重要的安全性問題,核心在於如何防止「權杖重放攻擊」(Token Replay Attack) 或稱「權杖盜用」。單純的 Bearer Token (持有者權杖) 機制,就像一把鑰匙,任何人撿到都能開門。您需要的是一把能認得主人的鑰匙。
要解決這個問題,核心思想是:將權杖 (Token) 與其合法的持有者 (Client) 進行唯一的綁定。當 Server A 收到請求時,它不僅要驗證權杖本身是否有效,還要驗證發起請求的 Client 是否就是該權杖的合法主人。
以下是幾種常見且有效的解決方案,由淺入深:
這是最基本也必須實作的一步。在產生權杖 (通常是 JWT) 時,必須明確指定這個權杖是發給「誰」用的。
-
作法:
- 在 Server A 的授權伺服器 (Authorization Server) 產生權杖時,於 JWT 的 Payload 中加入
aud(Audience) 聲明。這個aud的值應該是 Client A 的唯一識別碼 (例如client_id)。 - 當 Server A 收到來自任何客戶端的請求時,除了驗證權杖的簽章、是否過期等基本檢查外,還必須驗證權杖中的
aud聲明是否與當前發起請求的客戶端client_id相符。
- 在 Server A 的授權伺服器 (Authorization Server) 產生權杖時,於 JWT 的 Payload 中加入
-
流程:
- Client A 攜帶自己的
client_id和client_secret向 Server A 請求權杖。 - Server A 驗證通過後,產生一個 JWT,其中
aud聲明的值為 Client A 的client_id。 - Client B 盜取了這個權杖。
- Client B 拿著這個權杖去呼叫 Server A 的 API。
- Server A 收到請求,解析權杖後發現
aud是 Client A,但發起請求的卻是 Client B (如果 Client B 無法偽造 Client A 的client_id和secret),因此拒絕該請求。
- Client A 攜帶自己的
-
優點:實作相對簡單,是 OIDC (OpenID Connect) 的標準要求。
-
缺點:如果 Client B 不僅盜取了權杖,還盜取了 Client A 的
client_id和client_secret,那麼這個方法就會失效。它防範的是「不同客戶端」之間的盜用,但無法防範「身分被完全冒用」。
這是更強大的防護機制,它將權杖與客戶端持有的某個「秘密」進行密碼學上的綁定。即使權杖被盜,攻擊者沒有那個「秘密」,也無法使用。這類技術統稱為「持有證明」(Proof-of-Possession, PoP)。
-
概念:要求客戶端在建立 TLS 連線時,也必須提供自己的客戶端憑證。權杖會與這個客戶端憑證進行綁定。
-
作法:
- Client A 擁有一個自己的 X.509 憑證。
- 在向 Server A 請求權杖時,Client A 透過 mTLS 通道發送請求。
- Server A 產生權杖時,會將 Client A 憑證的雜湊值 (thumbprint) 寫入 JWT 的
cnf.x5t#S256聲明中。 - 當 Client A 帶著權杖呼叫 API 時,它必須透過同一個 mTLS 通道。
- Server A 驗證收到的權杖,並確認
cnf.x5t#S256的值與當前 TLS 通道中客戶端憑證的雜湊值完全相符。
-
優點:安全性極高,是業界公認的標準作法之一。
-
缺點:憑證的管理和部署對客戶端來說較為複雜,比較適合 Server-to-Server 的場景。
-
概念:一種較新的標準 (RFC 9449),專為無法輕易使用 mTLS 的場景設計 (例如:瀏覽器中的 SPA 應用、手機 App)。它透過客戶端簽署額外的資訊來證明自己擁有私鑰。
-
作法:
- Client A 在本機產生一對公私鑰。
- 當 Client A 請求權杖時,會將「公鑰」的資訊傳給 Server A。Server A 將此公鑰的雜湊值寫入 Access Token 的
cnf.jkt聲明中。 - 當 Client A 要呼叫 API 時,它會做兩件事:
- 將 Access Token 放在
Authorization標頭。 - 額外產生一個
DPoP標頭。這個標頭是一個新的、短時效的 JWT,用 Client A 的「私鑰」簽署,內容包含了 HTTP 方法 (GET/POST)、目標 URL 等資訊。
- 將 Access Token 放在
- Server A 收到請求後,會進行雙重驗證:
- 驗證 Access Token 本身。
- 用 Access Token 中儲存的公鑰雜湊值,去驗證
DPoP標頭這個 JWT 的簽章是否正確。
-
優點:安全性高,且比 mTLS 更靈活,適用於 Web 前端和行動應用。
-
缺點:客戶端和伺服器端的實作都比傳統 Bearer Token 複雜。
| 方法 | 適用場景 | 安全等級 | 實作複雜度 |
|---|---|---|---|
| Audience Validation | 所有場景 (基礎必備) | 中 | 低 |
| mTLS | Server-to-Server、高安全內部服務 | 高 | 高 |
| DPoP | Web 應用 (SPA)、行動應用 (Mobile App) | 高 | 中 |
最佳實踐路徑:
- 務必實作 Audience (
aud) 驗證。這是最基本的防線。 - 根據您的客戶端類型,選擇一種持有證明 (PoP) 機制:
- 如果您的 Client A 和 Client B 是後端服務 (Server-to-Server),請優先考慮 mTLS。
- 如果您的客戶端是瀏覽器或手機 App,請使用 DPoP。
透過結合「Audience 驗證」和「寄件人約束」,您就可以有效地確保從 Client A 取得的權杖,只有 Client A 能夠合法使用,從而徹底解決您的問題。