範例使用的是後端 .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="

以上提供一個最小、靈活且有基本安全性的方案,隨時可再依需求追加其他功能。