如何在ASP.NET Core中自定义一个Authorize注解

原文

授权是一种安全机制,用于确定用户对资源的访问级别。我们经常需要按照组织或项目设定的规则来实现自定义授权逻辑。

在本文中,我们将学习如何在ASP.NET Core中实现自定义授权属性。

需要下载本文中的源码, 你可以访问我们的Github代码仓.

尽管在本文中我们深入讨论了自定义授权属性,但如果您对这些主题不熟悉,我们也有关于自定义属性通用属性的入门文章。

要了解更多关于基于角色的授权的信息,请参考Angular中使用ASP.NET Core Identity进行基于角色的授权使用Blazor WebAssembly进行基于角色的授权

自定义Authorize注解

ASP.NET Core提供了过滤器,用于在操作方法之前或之后执行用户定义的代码。其中一个在操作方法调用之前帮助授权请求的过滤器是IAuthorizationFilter

现在,让我们利用这个过滤器并实现一个简单的自定义授权属性。

实现一个简单的自定义的Authorize注解

IAuthorizationFilter公开了一个OnAuthorization()方法,该方法在每次操作方法调用之前执行:

void OnAuthorization(AuthorizationFilterContext context);

因此,要创建一个自定义授权属性,我们可以创建一个继承自IAuthorizationFilter接口并实现OnAuthorization()方法的属性:

public sealed class CustomAuthorizeAttribute : Attribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (context != null)
        {
            // Auth logic
        }
    }
}

我们可以在操作方法或控制器上使用这个属性。现在,只有在OnAuthorization()方法内的授权检查成功时,操作方法才会执行:

[HttpGet]
[CustomAuthorize]
public IEnumerable<WeatherForecast> Get()
{
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = Summaries[Random.Shared.Next(Summaries.Length)]
    })
    .ToArray();
}

这是一个不错的方法,但有时我们需要在过滤器内部注入外部依赖项以执行授权逻辑。例如,为了获取已登录用户的声明,我们将注入IHttpContextAccessor来访问声明:

public class CustomAuthorizeAttribute : Attribute
{
    private readonly IEnumerable<Claim> _claims;
    public CustomAuthorizeAttribute(IHttpContextAccessor httpContextAccessor)
    {
        _claims = httpContextAccessor.HttpContext.User.Claims;
    }
}

我们讨论的简单方法的缺点是我们无法将外部依赖项注入到过滤器中。这是因为在使用属性时,必须提供它们的构造函数参数。

这就是我们可以使用TypeFilterAttribute的地方。它具有System.Type数据类型的ImplementationType属性,可以通过构造函数进行初始化:

public class TypeFilterAttribute : Attribute, IFilterFactory, IOrderedFilter
{
    public TypeFilterAttribute(Type type)
    {
        ImplementationType = type ?? throw new ArgumentNullException(nameof(type));
    }
    public Type ImplementationType { get; }
    // ...more code omitted for brevity
}

这个TypeFilterAttribute使用Microsoft.Extensions.DependencyInjection.ObjectFactory来实例化ImplementationType,而不是从IoC容器中解析。有了这个,我们现在可以在属性中定义依赖关系,而运行时会负责依赖注入。

现在,通过结合IAuthorizationFilterTypeFilterAttribute,我们可以创建一个支持注入外部依赖项的自定义AuthorizeAttribute

现在让我们看看这个实际的例子。

实现一个具有依赖项的自定义授权属性

现在,让我们实现一个简单的自定义授权属性,验证传递的自定义会话标头的HTTP请求。

首先,我们启动一个ASP.NET Core Web API项目,并在Program.cs文件中配置HttpContextAccessor以进行依赖注入:

builder.Services.AddHttpContextAccessor();

之后我们定义SessionRequirementFilter:

public class SessionRequirementFilter : IAuthorizationFilter
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    public SessionRequirementFilter(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (!_httpContextAccessor.HttpContext!.Request.Headers["X-Session-Id"].Any())
        {
            context.Result = new UnauthorizedObjectResult(string.Empty);
            return;
        }
    }
}

我们继承自IAuthorizationFilter并实现了OnAuthorization方法,在其中检查请求中是否存在自定义会话标头X-Session-Id。正如您注意到的,我们注入了IHttpContextAccessor以访问HttpContext

接下来,我们创建一个继承自TypeFilterAttribute的属性:

public class SessionRequirementAttribute : TypeFilterAttribute
{
    public SessionRequirementAttribute() : base(typeof(SessionRequirementFilter))
    {
    }
}

继承自TypeFilterAttribute允许我们传递SessionRequirementFilter类,当我们使用这个属性时它会执行。

最后,我们可以在自动生成的WeatherForecast控制器的Get()方法上装饰该属性:

[HttpGet("WithCustomAuthorizeAttribute")]
[SessionRequirement]
public IEnumerable<WeatherForecast> Get()
{
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = Summaries[Random.Shared.Next(Summaries.Length)]
    })
    .ToArray();
}

现在让我们执行这段代码,但不传递X-Session-Id标头。 Get()方法不会被调用,而是返回未授权的响应(401):

另一方面,如果我们添加会话标头,我们将得到带有OK(200) HTTP状态代码的响应:

基于策略的授权

虽然我们可以使用自定义授权属性来构建授权逻辑,但Microsoft建议使用基于策略的授权来构建自定义授权。基于策略的授权将授权与应用程序逻辑解耦,提供了一种灵活、可重用和可扩展的ASP.NET Core安全模型。

要实现基于策略的授权,我们需要了解三个概念:

  • 策略(Policies)
  • 需求(Requirements)
  • 处理程序(Handlers)
    一个策略包括多个需求。需求是一个类,接受参数以验证授权逻辑。最后,一个授权处理程序包含根据添加到它的需求验证策略的逻辑。

现在,让我们将这个实践起来。我们将实现与前一节相同的授权检查,以验证是否传递了自定义会话标头的HTTP请求。

Implement Policy-Based Authorization

首先,让我们创建一个用于会话标头验证的需求:

public class SessionRequirement : IAuthorizationRequirement
{
    public SessionRequirement(string sessionHeaderName)
    {
        SessionHeaderName = sessionHeaderName;
    }
    public string SessionHeaderName { get; }
}

需求必须实现空的标记接口IAuthorizationRequirement。我们还接受一个构造函数参数,用于传递我们要检查其存在的会话标头的名称。

接下来,我们创建一个包含授权逻辑的处理程序:

public class SessionHandler : AuthorizationHandler<SessionRequirement>
{
    private readonly IHttpContextAccessor _httpContextAccessor;
    public SessionHandler(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }
    protected override Task HandleRequirementAsync
        (AuthorizationHandlerContext context, SessionRequirement requirement)
    {
        var httpRequest = _httpContextAccessor.HttpContext!.Request;
        if (!httpRequest.Headers[requirement.SessionHeaderName].Any())
        {
            context.Fail();
            return Task.CompletedTask;
        }
        context.Succeed(requirement);
        return Task.CompletedTask;
    }
}

要创建一个授权处理程序,我们继承自AuthorizationHandler<TRequirement>。这确保了对需求类型TRequirement的授权处理程序的调用。

然后,我们实现AuthorizationHandler类的HandleRequirementAsync()方法。该方法接受授权上下文和需求实例本身。然后,我们从SessionRequirement实例中获取标头名称,并使用通过构造函数注入的外部IHttpContextAccessor服务进行验证。

在评估需求之后,我们在AuthorizationHandlerContext实例上调用Succeed()方法,并将需求实例作为参数传递给该方法。这表示需求成功。

另一方面,我们在AuthorizationHandlerContext实例上使用Fail()方法来标记需求失败。这会阻止进一步访问所请求的资源。

接下来,让我们将处理程序注册到服务集合中:

builder.Services.AddSingleton<IAuthorizationHandler, SessionHandler>();

现在,我们可以将策略注册到Authorization服务中:

builder.Services.AddAuthorization(options =>
{
    options.AddPolicy("SessionPolicy", policy =>
    {
        policy.Requirements.Add(new SessionRequirement("X-Session-Id"));
    });
});

在这里,我们创建了一个名为SessionPolicy的策略,并配置了相关的需求。虽然我们可以向策略添加多个需求,但在我们的示例中,我们添加了一个单独的SessionRequirement,并通过构造函数传递了会话标头的名称。

最后,让我们在我们的控制器操作方法中使用策略:

[HttpGet("WithCustomAuthorizationPolicy")]
[Authorize(Policy = "SessionPolicy")]
public IEnumerable<WeatherForecast> Get()
{
    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
    {
        Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
        TemperatureC = Random.Shared.Next(-20, 55),
        Summary = Summaries[Random.Shared.Next(Summaries.Length)]
    })
    .ToArray();
}

使用起来非常简单,因为我们将创建的策略名称传递给Authorize属性的构造函数。这要求在执行操作方法时必须满足策略。

现在让我们测试一下代码。

我们在不传递X-Session-Id标头的情况下执行代码,然后我们会收到一个未授权的响应:

相反,添加会话标头会产生成功的响应:

总结

我们学习了在ASP.NET Core中创建自定义授权的两种方法。尽管使用IAuthorizationFilter实现自定义授权属性很简单,但基于策略的授权更灵活,通过将授权与应用程序逻辑解耦来帮助我们构建一个松散耦合的安全模型。这就是为什么基于策略的授权更适合实现可扩展的授权解决方案的原因。

发表回复

您的电子邮箱地址不会被公开。