iT邦幫忙

2024 iThome 鐵人賽

DAY 20
0

今天我們來談談怎麼在 API 開發中加上幾種保護機制,確保你的 API 不會輕易被外部攻擊或未授權的用戶存取。這篇文章主要會聚焦在程式實作部分,透過實例來展示如何使用 JWT Token、API Key、IP 白名單、速率限制等技術來保護 API。明天,我們會補充如何使用 Postman 進行權限測試,並且展示自動化測試的部分。


在這次的專案中,我們把設定從原本的 Program.cs 分離到 Startup.cs,這樣可以讓應用程式的結構更清晰,也更方便後續的擴展和維護。


1. 程式入口:Program.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 類來配置應用程式
                });
    }
}


2. 應用程式設定:Startup.cs

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(); // 映射控制器路由
            });
        }
    }
}

3. 基礎控制器:BaseController 與速率限制

這裡是 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; // 返回是否超過速率限制
        }
    }
}


4. API Key 與 IP 白名單驗證

這是用來驗證 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 提供的一個功能,讓你可以在控制器的動作方法執行前後執行一些共用的邏輯。簡單來說,它可以幫助你攔截請求,做一些驗證、日誌紀錄或是修改回應等處理。

主要用途

  • 在動作執行前:你可以檢查請求是否有效,例如檢查 API Key、驗證使用者身份等。
  • 在動作執行後:你可以對回應結果進行修改,或是執行一些後續處理。

基本使用方式

  1. 建立一個繼承自 ActionFilterAttribute 的類別,實作你想要的邏輯:
public class CustomActionFilter : ActionFilterAttribute
{
    public override void OnActionExecuting(ActionExecutingContext context)
    {
        // 在動作執行前的邏輯
        base.OnActionExecuting(context);
    }

    public override void OnActionExecuted(ActionExecutedContext context)
    {
        // 在動作執行後的邏輯
        base.OnActionExecuted(context);
    }
}

  1. 將這個 ActionFilterAttribute 套用到你的控制器或動作方法上:
[CustomActionFilter] // 套用在整個控制器
public class HomeController : Controller
{
    [CustomActionFilter] // 或套用在單一動作方法
    public IActionResult Index()
    {
        return View();
    }
}

常見應用場景

  • 驗證 API Key:確保每個請求都有正確的 API Key。
  • 記錄日誌:自動記錄每次進入和離開動作方法的資訊。

ActionFilterAttribute 幫助你將重複的邏輯集中處理,讓程式碼更整潔,維護更容易。


5. TokenController - 生成 JWT Token

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
        }
    }
}


6. ProductController - 設定 API 保護機制

這個 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等等等.…看你專案與使用的工具,千萬不要像這樣寫在程式馬上,很重要!所以特別提醒!


上一篇
Day 19: 保護你的 API(下) .NET Core 中的進階 API 安全設置
下一篇
Day 21: 實際開發 API 權限保護(下) Postman 測試
系列文
使用 C# 從零開始玩轉 Web API,從基礎到微服務與雲端部署的全面探索22
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言