在 ASP.NET Core 的自定义中间件中获取对请求的 Controller 和 Action 的引用

Ste*_*ler 4 c# asp.net-core-mvc asp.net-core

我正在开发一个自定义中间件,用于对调用 API 的客户端进行身份验证。

我使用属性来定义操作是否需要身份验证,但我不知道如何在 Invoke 方法内获取对所请求的控制器操作的引用。

下面是我到目前为止的代码

AuthenticateClient.cs

public class AuthenticateClient
{
    private readonly RequestDelegate _next;
    private readonly ILogger _logger;
    private readonly GenericUnitOfWork _worker;

    public AuthenticateClient(RequestDelegate next, ApiDbContext db, IHttpContextAccessor httpContext, IHostingEnvironment env, ILoggerFactory loggerFactory, IOptions<Utility.LCLog.Settings> settings)
    {
        _next = next;
        _logger = loggerFactory.CreateLogger(settings.Value.ApplicationName);
        _worker = new GenericUnitOfWork(new AppHelper(httpContext, db, env));
    }

    public async Task Invoke(HttpContext context)
    {
        if (!context.Request.Headers.Keys.Contains("ClientAuth"))
        {
            _logger.LogWarning("ClientAuth missing in request", new string[] { "Host: " + context.Request.Host, "IP: " + context.Request.HttpContext.Connection.RemoteIpAddress });

            context.Response.StatusCode = 400;
            await context.Response.WriteAsync("ClientAuth missing from request header values");

            return;
        }
        else
        {
            string[] tmp = context.Request.Headers["ClientAuth"].ToString().Split("/");

            if (tmp.Length != 2)
            {
                context.Response.StatusCode = 400;
                await context.Response.WriteAsync("The format of the ClientAuth value is wrong");

                return;
            }
            else
            {
                Client client;
                string key, pass;

                key = tmp[0];
                pass = tmp[1];

                client = await _worker.GetRepo<Client>().SingleOrDefault(clnt => clnt.Active && clnt.Key.Equals(key) && clnt.Password.Equals(pass));

                if (client == null)
                {
                    _logger.LogWarning("Client authentication failed", new string[] { "Key: " + key, "Password: " + pass, "Host: " + context.Request.Host, "IP: " + context.Request.HttpContext.Connection.RemoteIpAddress });

                    context.Response.StatusCode = 401;
                    await context.Response.WriteAsync("Authentication failed");

                    return;
                }
            }                
        }

        await _next.Invoke(context);
    }
}
Run Code Online (Sandbox Code Playgroud)

ClientAuthenticationAttribute.cs

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class ClientAuthenticationAttribute : Attribute
{
    private readonly bool _authRequired;

    public ClientAuthenticationAttribute(bool authRequired = true)
    {
        _authRequired = authRequired;
    }

    public bool AuthRequired { get { return _authRequired; } }
}
Run Code Online (Sandbox Code Playgroud)

Tom*_*han 5

我建议您将身份验证授权的逻辑分开,并将它们保存在不同的位置。

从这里回顾一下:

  • 身份验证是验证您是谁的过程。

  • 授权是在我们知道您是谁的情况下验证您是否有权访问特定资源的过程。

您当前正在尝试做的是在中间件组件中对用户进行身份验证和授权。尽管您可能可以通过将所有此类逻辑移至您在 api 框架(无论是 ASP.NET Core MVC、Web API 2 还是其他框架)中注册的过滤器中来使其工作,但这意味着您的其他中间件组件都没有访问用户数据(我猜,这是您首先选择在中间件中实现它的原因之一)。

鉴于您对身份验证和授权分离的新了解,可能的解决方案是执行以下操作:

仅用于身份验证的中间件

在您的中间件中,您只需关心身份验证,并将授权留给管道中的后续组件。实际上,这意味着您的中间件应该执行以下操作:

  1. 查找用户令牌、cookie 或用于用户验证其请求的任何内容
  2. 如果不存在,则将请求视为匿名,并调用下一个管道组件,而不将用户附加到请求上下文。
  3. 如果存在有效令牌,则从中解析用户数据(例如,从 JWT 解析用户的声明、在数据库中查找角色等)并将其存储在请求上下文中。我发现创建IPrincipal并设置context.Request.User它以及直接向上下文字典添加信息都很有用。
  4. 用户在请求上下文中注册后,调用下一个管道组件。

假设经过身份验证的用户进行授权

您现在可以重写您的授权逻辑,以假设已经在请求上下文中注册了经过身份验证的用户。

在 ASP.NET Web API 2 应用程序中,您将实现一个继承自 的自定义筛选器属性AuthorizationFilterAttribute,以确保它首先运行筛选器。例如,在我当前的应用程序中,我们有以下属性来授权用户拥有特定的声明。请注意,它不会做任何工作来确定用户是谁;如果用户未附加到上下文,则响应只是Unauthorized。您可以在这里更复杂,对缺乏访问权限的用户处理匿名请求与经过身份验证的请求不同,例如,将匿名请求重定向到登录表单,同时将缺乏访问权限的用户重定向到错误页面。

[AttributeUsage(validOn: AttributeTargets.Method)]
public class AuthorizeClaimsFilterAttribute : AuthorizationFilterAttribute
{
    public AuthorizeClaimsFilterAttribute(string claimType, string claimValue)
    {
        ClaimType = claimType;
        ClaimValue = claimValue;
    }

    public string ClaimType { get; }
    public string ClaimValue { get; }

    public override void OnAuthorization(HttpActionContext actionContext)
    {
        if (!(actionContext.RequestContext.Principal is ClaimsPrincipal principal)
            || !principal.HasClaim(x => x.Type == ClaimType && x.Value == ClaimValue))
        {
            actionContext.Response = actionContext.Request.CreateResponse(HttpStatusCode.Unauthorized);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

要使用它,我们只需用它来装饰操作方法:

[AuthorizeClaimsFilter("urn:ourapp:claims:admin", true)]
public IHttpActionResults OnlyAdminsCanAccess() { /* ... */ }
Run Code Online (Sandbox Code Playgroud)