使用需要 session api 键的 .NET Minimal API 创建一个 API

create an API with .NET Minimal APIs that require session api key

这个视频非常棒,展示了如何使用 .net 6 创建 Minimal APIs:

https://www.youtube.com/watch?v=eRJFNGIsJEo

令人惊奇的是,它如何使用依赖注入来获取端点内所需的大部分内容。例如,如果我需要自定义 header 的值,我会这样:

app.MapGet("/get-custom-header", ([FromHeader(Name = "User-Agent")] string data) =>
{
    return $"User again is: {data}";
});

我可以有另一个端点,我可以像这样访问整个 httpContext:

app.MapGet("/foo", (Microsoft.AspNetCore.Http.HttpContext c) =>
{
    var path = c.Request.Path;
    return path;
});

我什至可以使用此代码注册我自己的 classes:builder.Services.AddTransient<TheClassIWantToRegister>()

如果我注册我的自定义 classes,我将能够在每次需要时创建一个 class 的实例和端点 (app.MapGet("...)


回到问题。 当用户登录时,我给他发这个:

{
  "ApiKey": "1234",
  "ExpirationDate": blabla bla
  .....
}

用户必须发送 1234 令牌才能使用 API。 如何避免像这样重复我的代码:

app.MapGet("/getCustomers", ([FromHeader(Name = "API-KEY")] string apiToken) =>
{
    // validate apiToken agains DB
    if(validationPasses)
       return Database.Customers.ToList();
    else
       // return unauthorized
});

我已经尝试创建自定义 class RequiresApiTokenKey 并将 class 注册为 builder.Services.AddTransient<RequiresApiTokenKey>() 以便我的 API 知道如何创建一个实例那 class 在需要时但是我怎样才能访问那个 class 中的当前 http 上下文? 我怎样才能避免不得不重复检查 header API-KEY header 在每个需要它的方法中是否有效?

根据我的评论对此进行了测试。

这会在每次请求时调用中间件中的 Invoke 方法,您可以在此处进行检查。

可能更好的方法是使用 AuthenticationHandler。使用这意味着您可以将单个端点归因于完成 API 密钥检查而不是所有传入请求

但是,我认为这仍然有用,中间件可用于您希望对每个请求执行的任何操作

使用中间件

Program.cs:

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

//our custom middleware extension to call UseMiddleware
app.UseAPIKeyCheckMiddleware();

if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.MapGet("/", () => "Hello World!");

app.Run();

APIKeyCheckMiddleware.cs

using Microsoft.Extensions.Primitives;

internal class APIKeyCheckMiddleware
{
    private readonly RequestDelegate _next;

    public APIKeyCheckMiddleware(RequestDelegate next)
    {
        _next = next;
    }

    public async Task Invoke(HttpContext httpContext)
    {
        
        //we could inject here our database context to do checks against the db
        if (httpContext.Request.Headers.TryGetValue("API-KEY", out StringValues value))
        {
            //do the checks on key
            var apikey = value;
        }
        else
        {
            //return 403
            httpContext.Response.StatusCode = 403;
        }
        
        await _next(httpContext);
    }
}

// Extension method used to add the middleware to the HTTP request pipeline.
public static class APIKeyCheckMiddlewareExtensions
{
    public static IApplicationBuilder UseAPIKeyCheckMiddleware(this IApplicationBuilder builder)
    {
        
        return builder.UseMiddleware<APIKeyCheckMiddleware>();
    }
}

我使用了 SmithMart 的答案,但不得不更改 Invoke 方法中的内容并在构造函数中使用 DI。 这是我的版本:

internal class ApiKeyCheckMiddleware
    {
        public static string ApiKeyHeaderName = "X-ApiKey";
        private readonly RequestDelegate _next;
        private readonly ILogger<ApiKeyCheckMiddleware> _logger;
        private readonly IApiKeyService _apiKeyService;

        public ApiKeyCheckMiddleware(RequestDelegate next, ILogger<ApiKeyCheckMiddleware> logger, IApiKeyService apiKeyService)
        {
            _next = next;
            _logger = logger;
            _apiKeyService = apiKeyService;
        }

        public async Task InvokeAsync(HttpContext httpContext)
        {
            var request = httpContext.Request;
            var hasApiKeyHeader = request.Headers.TryGetValue(ApiKeyHeaderName, out var apiKeyValue);

            if (hasApiKeyHeader)
            {
                _logger.LogDebug("Found the header {ApiKeyHeader}. Starting API Key validation", ApiKeyHeaderName);

                if (apiKeyValue.Count != 0 && !string.IsNullOrWhiteSpace(apiKeyValue))
                {
                    if (Guid.TryParse(apiKeyValue, out Guid apiKey))
                    {
                        var allowed = await _apiKeyService.Validate(apiKey);

                        if (allowed)
                        {
                            _logger.LogDebug("Client successfully logged in with key {ApiKey}", apiKeyValue);

                            var apiKeyClaim = new Claim("ApiKey", apiKeyValue);
                            var allowedSiteIdsClaim = new Claim("SiteIds", string.Join(",", allowedSiteIds));
                            var principal = new ClaimsPrincipal(new ClaimsIdentity(new List<Claim> { apiKeyClaim, allowedSiteIdsClaim }, "ApiKey"));
                            httpContext.User = principal;

                            await _next(httpContext);

                            return;
                        }
                    }

                    _logger.LogWarning("Client with ApiKey {ApiKey} is not authorized", apiKeyValue);
                }
                else
                {
                    _logger.LogWarning("{HeaderName} header found, but api key was null or empty", ApiKeyHeaderName);
                }
            }
            else
            {
                _logger.LogWarning("No ApiKey header found.");
            }

            httpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
        }
    }