範例使用的是後端 .NET 8 框架和 C# 語言。
比較好的設計方式是將 API 白名單驗證放在資料庫進行管理,增添「有效期限」、「即時封鎖」、「金鑰更新機制」…等功能,此範例僅提供一個想法,實際應用請依照專案需求進行擴充。
因為開放給第三方使用 API 的情境很多,過於複雜的設計對於小專案來說不太適合,因此想了一個最小可行的方式,以「API Attribute」的方式,設計一個可以簡單擴充和應用的安全驗證機制。
我們可以先將要驗證的 API 服務白名單寫在 appsettings.json 內:
1 "ExternalServices": {
2 "Sample": { // 服務名稱
3 "ApiKey": "Ecbu7wrgwB.hmMp3Dhkmxc?e#K@$=ct@YppY97y.KNf%+St4.z*wV6QdEazRQUx=", // 自訂驗證金鑰
4 "AllowedIPs": [
5 "::1", // 本地開發端
6 "127.0.0.1", // 支援 IP address
7 "google.com" // 支援 Domain name 驗證
8 ]
9 }
10 }
這邊重申一下,比較好的設計方式是將 API 白名單驗證放在資料庫進行管理;寫在 appsettings.json 做法的好處是,不用每次對方請求 API 時都得跟資料庫連線。
在 Web API 專案內新增一個目錄/Filters
,底下建立 Attribute 繼承:
1public class ApiSecurityAttribute : ActionFilterAttribute
2{
3
4 /// <summary>
5 /// 服務名稱
6 /// </summary>
7 public required string ServiceName { get; set; }
8
9 public override async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
10 {
11 var configuration = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>();
12 var serviceConfig = configuration.GetSection($"AppSettings:ExternalServices:{ServiceName}");
13
14 var apiKey = context.HttpContext.Request.Headers["X-API-KEY"].FirstOrDefault();
15 if (string.IsNullOrWhiteSpace(apiKey) || apiKey != serviceConfig["ApiKey"])
16 {
17 context.Result = new NotFoundResult();
18 return;
19 }
20
21 var remoteIp = context.HttpContext.Connection.RemoteIpAddress?.ToString();
22 var originalHost = context.HttpContext.Request.Headers["X-Forwarded-Host"].FirstOrDefault() ?? context.HttpContext.Request.Host.Value;
23 var allowedIPsAndDomains = serviceConfig.GetSection("AllowedIPs").Get<string[]>();
24
25 if (allowedIPsAndDomains != null && !string.IsNullOrWhiteSpace(remoteIp))
26 {
27 var matchTasks = allowedIPsAndDomains.Select(allowed => MatchAsync(allowed, remoteIp, originalHost));
28 var results = await Task.WhenAll(matchTasks);
29 if (!results.Any(match => match))
30 {
31 context.Result = new NotFoundResult();
32 return;
33 }
34 }
35 else
36 {
37 context.Result = new NotFoundResult();
38 return;
39 }
40
41 await next();
42 }
43
44 private async Task<bool> MatchAsync(string allowed, string remoteIp, string hostName)
45 {
46 // IP 檢查
47 if (IPAddress.TryParse(allowed, out _))
48 {
49 return allowed == remoteIp;
50 }
51
52 // Domain name 檢查
53 if (hostName.Contains(allowed)) return true;
54 }
55
56}
以上程式碼中,當驗證失敗會回傳「404 NotFound」。
接下來只要在要驗證的 API Controller 內,加進上述寫好的 Attribute:
1 /// <summary>
2 /// API驗證測試
3 /// </summary>
4 /// <returns></returns>
5 [HttpPost("test")]
6 [ApiSecurity(ServiceName = "Sample")] // ServiceName 取自 appsettings 的服務名稱
7 public IActionResult Test()
8 {
9 return Ok();
10 }
最後,請要使用的第三方服務,提供 IP 或 Domain 白名單,寫進 appsettings.json 內的 “AllowedIPs” 清單,並請對方在 Request 時在 Headers 增加 “X-API-KEY” 填入指定 API 金鑰:
1Request Headers
2"X-API-KEY": "Ecbu7wrgwB.hmMp3Dhkmxc?e#K@$=ct@YppY97y.KNf%+St4.z*wV6QdEazRQUx="
以上提供一個最小、靈活且有基本安全性的方案,隨時可再依需求追加其他功能。