今天我們來談談怎麼在 API 開發中加上幾種保護機制,確保你的 API 不會輕易被外部攻擊或未授權的用戶存取。這篇文章主要會聚焦在程式實作部分,透過實例來展示如何使用 JWT Token、API Key、IP 白名單、速率限制等技術來保護 API。明天,我們會補充如何使用 Postman 進行權限測試,並且展示自動化測試的部分。
在這次的專案中,我們把設定從原本的 Program.cs
分離到 Startup.cs
,這樣可以讓應用程式的結構更清晰,也更方便後續的擴展和維護。
這是應用程式的入口,負責啟動應用並參考 Startup.cs
來進行所有的配置。
namespace FirstWebAPI
{
public class Program
{
public static void Main(string[] args)
{
CreateHostBuilder(args).Build().Run(); // 啟動應用
}
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>(); // 使用 Startup 類來配置應用程式
});
}
}
在 Startup.cs
裡,我們負責所有的設定,包括 JWT Token 驗證、API Key 機制、IP 白名單,還有速率限制等等。
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.IdentityModel.Tokens;
using Microsoft.OpenApi.Models;
using System.Text;
namespace FirstWebAPI
{
public class Startup
{
// 配置服務
public void ConfigureServices(IServiceCollection services)
{
services.AddMemoryCache(); // 添加記憶體快取服務
// 配置 JWT 身份驗證
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
// JWT 驗證參數
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true, // 驗證發行者
ValidateAudience = true, // 驗證受眾
ValidateLifetime = true, // 驗證 Token 是否過期
ValidateIssuerSigningKey = true, // 驗證簽名密鑰
ValidIssuer = "TestIssuer", // 發行者
ValidAudience = "TestAudience", // 受眾
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("ThisIsASuperSecretKey12345ThisIsASuperSecretKey12345")) // 簽名密鑰
};
});
// 配置授權策略
services.AddAuthorization(options =>
{
options.AddPolicy("GetPolicy", policy =>
policy.RequireRole("Admin", "Employee"));
options.AddPolicy("AdminOnly", policy =>
policy.RequireRole("Admin"));
});
services.AddControllers(); // 添加控制器服務
// 添加 Swagger 服務
services.AddEndpointsApiExplorer();
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "FirstWebAPI", Version = "v1" });
// 定義安全方案
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = @"JWT 授權標頭使用 Bearer 格式。
請在下方輸入 'Bearer ' 加上您的 Token。
例如:'Bearer 12345abcdef'",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
// 全局使用安全方案
c.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type=ReferenceType.SecurityScheme,
Id="Bearer"
}
},
new string[]{}
}
});
});
}
// 配置 HTTP 請求管道
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage(); // 使用開發人員例外頁面
// 啟用 Swagger
app.UseSwagger();
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "FirstWebAPI V1");
//c.RoutePrefix = string.Empty; // 將 Swagger UI 設定在應用程式根目錄
});
}
app.UseHttpsRedirection(); // 強制使用 HTTPS
app.UseRouting(); // 啟用路由
app.UseAuthentication(); // 啟用身份驗證中介軟體
app.UseAuthorization(); // 啟用授權中介軟體
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers(); // 映射控制器路由
});
}
}
}
這裡是 BaseController
,負責處理速率限制,確保每個 IP 在一段時間內的請求數量受到控制。
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
namespace FirstWebAPI.Controllers
{
[ApiController]
public abstract class BaseController : ControllerBase
{
private const int MAX_REQUESTS_PER_MINUTE = 10; // 每分鐘最多請求數
private readonly IMemoryCache _cache;
public BaseController(IMemoryCache cache)
{
_cache = cache;
}
// 檢查是否超過速率限制
protected bool IsRateLimitExceeded(HttpRequest request)
{
var clientIp = request.HttpContext.Connection.RemoteIpAddress.MapToIPv4().ToString(); // 取得請求 IP
var cacheKey = $"RequestCount_{clientIp}";
var requestCount = _cache.GetOrCreate(cacheKey, entry =>
{
entry.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(1); // 設定有效期 1 分鐘
return 0;
});
requestCount++;
_cache.Set(cacheKey, requestCount); // 更新 Cache 中的請求數
return requestCount > MAX_REQUESTS_PER_MINUTE; // 返回是否超過速率限制
}
}
}
這是用來驗證 API Key 和 IP 白名單的部分,我們透過 ValidateRequestAttribute
把它們整合起來,應用到每個 Controller 上。
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
namespace FirstWebAPI.Filters
{
public class ValidateRequestAttribute : ActionFilterAttribute
{
private const string ApiKey = "my-secure-api-key"; // 模擬的 API Key
private static readonly List<string> WhiteListIPs = new List<string> { "127.0.0.1", "192.168.1.1", "72.14.201.13"}; // 白名單 IP
public override void OnActionExecuting(ActionExecutingContext context)
{
var request = context.HttpContext.Request;
// 驗證 API Key
if (!IsApiKeyValid(request))
{
context.Result = new UnauthorizedObjectResult("無效的 API Key");
return;
}
// 驗證 IP 白名單
if (!IsIpAllowed(request))
{
context.Result = new UnauthorizedObjectResult("你的 IP 地址不在白名單中");
return;
}
base.OnActionExecuting(context); // 如果驗證通過,繼續執行 Action
}
private bool IsApiKeyValid(HttpRequest request)
{
return request.Headers.TryGetValue("X-API-Key", out var extractedApiKey) && extractedApiKey == ApiKey;
}
private bool IsIpAllowed(HttpRequest request)
{
var remoteIp = request.HttpContext.Connection.RemoteIpAddress;
var ipString = remoteIp.MapToIPv4().ToString();
return WhiteListIPs.Contains(ipString);
}
}
}
ActionFilterAttribute
ActionFilterAttribute
是 ASP.NET Core 提供的一個功能,讓你可以在控制器的動作方法執行前後執行一些共用的邏輯。簡單來說,它可以幫助你攔截請求,做一些驗證、日誌紀錄或是修改回應等處理。
ActionFilterAttribute
的類別,實作你想要的邏輯:public class CustomActionFilter : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext context)
{
// 在動作執行前的邏輯
base.OnActionExecuting(context);
}
public override void OnActionExecuted(ActionExecutedContext context)
{
// 在動作執行後的邏輯
base.OnActionExecuted(context);
}
}
ActionFilterAttribute
套用到你的控制器或動作方法上:[CustomActionFilter] // 套用在整個控制器
public class HomeController : Controller
{
[CustomActionFilter] // 或套用在單一動作方法
public IActionResult Index()
{
return View();
}
}
ActionFilterAttribute
幫助你將重複的邏輯集中處理,讓程式碼更整潔,維護更容易。
TokenController
負責生成 JWT Token,之後你可以使用這個 Token 來進行 API 請求的身份驗證。
using FirstWebAPI.Filters;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Text;
namespace FirstWebAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
[ValidateRequest] // 這會對整個 Controller 進行 API Key 和 IP 白名單的驗證
public class TokenController : BaseController
{
private const string SecretKey = "ThisIsASuperSecretKey12345ThisIsASuperSecretKey12345"; // 簽名密鑰
private const string Issuer = "TestIssuer"; // 發行者
private const string Audience = "TestAudience"; // 受眾
public TokenController(IMemoryCache cache) : base(cache) {}
[HttpPost("GenerateToken")]
public IActionResult GenerateToken(string username, string role)
{
if (string.IsNullOrEmpty(username) || string.IsNullOrEmpty(role) || (role != "Employee" && role != "Admin"))
{
return BadRequest("無效的使用者憑證。");
}
var tokenString = GenerateJwtToken(username, role);
return Ok(new { Token = tokenString });
}
private static string GenerateJwtToken(string username, string role)
{
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey)); // 建立加密密鑰
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
var claims = new[]
{
new Claim(ClaimTypes.Name, username),
new Claim(ClaimTypes.Role, role)
};
var token = new JwtSecurityToken(
issuer: Issuer,
audience: Audience,
claims: claims,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: credentials
);
return new JwtSecurityTokenHandler().WriteToken(token); // 返回 Token
}
}
}
這個 ProductController
結合了所有的保護機制,包括 JWT Token、API Key、IP 白名單和速率限制。
using FirstWebAPI.Filters;
using FirstWebAPI.Model;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Memory;
using System.Collections.Generic;
using System.Linq;
namespace FirstWebAPI.Controllers
{
[Route("api/[controller]")]
[ApiController]
[ValidateRequest] // 這會對整個 Controller 進行 API Key 和 IP 白名單的驗證
public class ProductController : BaseController
{
public ProductController(IMemoryCache cache) : base(cache) {}
// 模擬的產品資料庫
private static List<Product> products = new List<Product>
{
new Product { Id=1, Name = "Laptop", Price = 1000m, Description = "High-performance laptop", Category = "Electronics" },
new Product { Id=2, Name = "Smartphone", Price = 600m, Description = "Latest model smartphone", Category = "Electronics" },
new Product { Id=3, Name = "Coffee Maker", Price = 150m, Description = "Automatic coffee maker", Category = "Home Appliances" }
};
[HttpGet("GetProduct")]
[Authorize(Policy = "GetPolicy")] // 允許 Admin 和 Employee 存取
public IActionResult GetProduct()
{
if (IsRateLimitExceeded(Request))
{
return StatusCode(429, "請求次數過多,請稍後再試");
}
return Ok(products); // 返回產品列表
}
[HttpGet("GetProduct{id}")]
[Authorize(Policy = "GetPolicy")] // 允許 Admin 和 Employee 存取
public IActionResult GetProduct(int id)
{
if (IsRateLimitExceeded(Request))
{
return StatusCode(429, "請求次數過多,請稍後再試");
}
var product = products.FirstOrDefault(p => p.Id == id);
if (product == null)
{
return NotFound("產品不存在");
}
return Ok(product);
}
[HttpPost("CreateProduct")]
[Authorize(Policy = "AdminOnly")] // 只有 Admin 可以新增
public IActionResult CreateProduct([FromBody] Product product)
{
if (IsRateLimitExceeded(Request))
{
return StatusCode(429, "請求次數過多,請稍後再試");
}
if (product == null)
{
return BadRequest("無效的產品數據");
}
products.Add(product);
return CreatedAtAction(nameof(GetProduct), new { id = product.Id }, product);
}
[HttpPut("UpdateProduct{id}")]
[Authorize(Policy = "AdminOnly")] // 只有 Admin 可以更新
public IActionResult UpdateProduct(int id, [FromBody] Product updatedProduct)
{
var product = products.FirstOrDefault(p => p.Id == id);
if (product == null)
{
return NotFound("產品不存在");
}
if (updatedProduct == null)
{
return BadRequest("無效的產品數據");
}
product.Name = updatedProduct.Name;
product.Price = updatedProduct.Price;
product.Description = updatedProduct.Description;
product.Category = updatedProduct.Category;
return NoContent();
}
[HttpDelete("DeleteProduct{id}")]
[Authorize(Policy = "AdminOnly")] // 只有 Admin 可以刪除
public IActionResult DeleteProduct(int id)
{
var product = products.FirstOrDefault(p => p.Id == id);
if (product == null)
{
return NotFound("產品不存在");
}
products.Remove(product);
return NoContent();
}
}
}
今天,我們完成了 API 保護機制的實作,結合了 JWT Token、API Key、IP 白名單和速率限制等安全機制。這些措施可以有效地保護 API,防止未授權的存取與過多請求。明天我們會介紹如何使用 Postman 測試這些 API 並設定自動化測試,確保 API 在實際運作中的安全性與穩定性。順便會介紹一點點簡單的Postman自動化與寫測試案例等等功能。
這邊的範例程式碼,我把APIKey、白名單IP、等等「機敏資訊」暫時先寫在程式裡面,是因為這是一個簡單測試的範例程式碼,在實際開發上不要把這些資訊寫死在程式上,你應該要用其他方式去儲存,不論是AppSetting、secret、Azure Key Vault、Config等等等.…看你專案與使用的工具,千萬不要像這樣寫在程式馬上,很重要!所以特別提醒!