这个问题提示我写这篇文章。通常,问题是有关使用ASP.NET Core内置授权来限制对中间件的访问。在ASP.NET Core中,针对MVC的授权机制已很好地公开(通过AuthorizeAttribute
),但是对于中间件,这是一项手动工作(至少目前如此)。这样做的原因可能是因为没有太多的终端中间件。
这不是我第一次收到这个问题,所以我迅速用典型的代码来完成任务。但是,经过一番思考,我决定在这里给出详细的答案。
基于策略的授权
从本质上讲,ASP.NET Core中的授权基于策略。最后,对其他指定需求(角色,索赔)的可用方法进行了评估。这意味着足以为当前用户验证策略。可以借助轻松地完成此操作IAuthorizationService
。唯一需要的是策略名称和HttpContext
。遵循授权中间件即可完成工作。
public class AuthorizationMiddleware
{
private readonly RequestDelegate _next; private readonly string _policyName; public AuthorizationMiddleware(RequestDelegate next, string policyName) { _next = next; _policyName = policyName; } public async Task Invoke(HttpContext httpContext, IAuthorizationService authorizationService) { AuthorizationResult authorizationResult = await authorizationService.AuthorizeAsync(httpContext.User, null, _policyName); if (!authorizationResult.Succeeded) { await httpContext.ChallengeAsync(); return; } await _next(httpContext); } }
当然,中间件注册可以封装在扩展方法中,以便于使用。
public static class AuthorizationApplicationBuilderExtensions
{
public static IApplicationBuilder UseAuthorization(this IApplicationBuilder app, string policyName) { // Null checks removed for brevity ... return app.UseMiddleware(policyName); } }
剩下的唯一事情就是将该中间件放在应该具有受限访问权限的中间件前面(如果需要验证多个策略,则可以多次放置该中间件)。
public class Startup
{
public void ConfigureServices(IServiceCollection services) { ... services.AddAuthorization(options => { options.AddPolicy("PolicyName", ...); }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { ... app.UseAuthentication(); app.Map("/policy-based-authorization", branchedApp => { branchedApp.UseAuthorization("PolicyName"); ... }); ... } }
简单有效。实现目标,对不对?
简单的授权,角色和方案
尽管这是我的首选解决方案,但上述方法远非完美。它没有提供全部功能,也不是用户友好的。类似的东西AuthorizeAttribute
会更好。这意味着充分利用策略,角色和方案。起初,这听起来像是一项艰巨的工作,但事实是,所有的辛苦工作都为我们完成了,我们只需要超越Microsoft.AspNetCore.Authorization
并使用Microsoft.AspNetCore.Authorization.Policy
软件包中的某些服务即可。但是在此之前,需要一种用户友好的方式来定义限制。这不是挑战,因为ASP.NET Core对此具有接口。
internal class AuthorizationOptions : IAuthorizeData
{
public string Policy { get; set; } public string Roles { get; set; } public string AuthenticationSchemes { get; set; } }
此选项类与相似AuthorizeAttribute
。这和AuthorizeAttribute
工具一样不足为奇IAuthorizeData
。
实现IAuthorizeData
允许AuthorizationPolicy
在的帮助下将类转换为IAuthorizationPolicyProvider
。
public class AuthorizationMiddleware
{
private readonly RequestDelegate _next; private readonly IAuthorizeData[] _authorizeData; private readonly IAuthorizationPolicyProvider _policyProvider; private AuthorizationPolicy _authorizationPolicy; public AuthorizationMiddleware(RequestDelegate next, IAuthorizationPolicyProvider policyProvider, IOptions authorizationOptions) { // Null checks removed for brevity _next = next; _authorizeData = new[] { authorizationOptions.Value }; _policyProvider = policyProvider; } public async Task Invoke(HttpContext httpContext, IPolicyEvaluator policyEvaluator) { if (_authorizationPolicy is null) { _authorizationPolicy = await AuthorizationPolicy.CombineAsync(_policyProvider, _authorizeData); } ... await _next(httpContext); } ... }
该政策需要评估。这需要两次调用IPolicyEvaluator
,一次用于身份验证,一次用于授权。
public class AuthorizationMiddleware
{
...
public async Task Invoke(HttpContext httpContext, IPolicyEvaluator policyEvaluator) { ... AuthenticateResult authenticateResult = await policyEvaluator.AuthenticateAsync(_authorizationPolicy, httpContext); PolicyAuthorizationResult authorizeResult = await policyEvaluator.AuthorizeAsync(_authorizationPolicy, authenticateResult, httpContext, null); if (authorizeResult.Challenged) { await ChallengeAsync(httpContext); return; } else if (authorizeResult.Forbidden) { await ForbidAsync(httpContext); return; } await _next(httpContext); } ... }
最后是处理Challenged
和Forbidden
方案。现在可以使用HttpContext
扩展方法来执行此操作,但是要记住要使用已提供的方案,这一点很重要。
public class AuthorizationMiddleware
{
...
private async Task ChallengeAsync(HttpContext httpContext) { if (_authorizationPolicy.AuthenticationSchemes.Count > 0) { foreach (string authenticationScheme in _authorizationPolicy.AuthenticationSchemes) { await httpContext.ChallengeAsync(authenticationScheme); } } else { await httpContext.ChallengeAsync(); } } private async Task ForbidAsync(HttpContext httpContext) { if (_authorizationPolicy.AuthenticationSchemes.Count > 0) { foreach (string authenticationScheme in _authorizationPolicy.AuthenticationSchemes) { await httpContext.ForbidAsync(authenticationScheme); } } else { await httpContext.ForbidAsync(); } } }
现在可以修改注册方法。这里要注意的重要一点是,不设置任何AuthorizationOptions
属性将导致使用默认策略(与修饰动作或控制器相同[Authorize]
)。这种情况可能值得重载。
public static class AuthorizationApplicationBuilderExtensions
{
public static IApplicationBuilder UseAuthorization(this IApplicationBuilder app) { return app.UseAuthorization(new AuthorizationOptions()); } public static IApplicationBuilder UseAuthorization(this IApplicationBuilder app, AuthorizationOptions authorizationOptions) { if (app == null) { throw new ArgumentNullException(nameof(app)); } if (authorizationOptions == null) { throw new ArgumentNullException(nameof(authorizationOptions)); } return app.UseMiddleware<AuthorizationMiddleware>(Options.Create(authorizationOptions)); } }
这使得AuthorizeAttribute
中间件管道可以使用提供的所有功能。如果应用程序未使用MVC,请务必记住有关添加策略服务的信息。
public class Startup
{
public void ConfigureServices(IServiceCollection services) { ... services.AddAuthorization(options => { options.AddPolicy("PolicyName", ...); }) .AddAuthorizationPolicyEvaluator(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { ... app.UseAuthentication(); app.Map("/simple-authorization", branchedApp => { branchedApp.UseAuthorization(); ... }); app.Map("/role-based-authorization", branchedApp => { branchedApp.UseAuthorization(new AuthorizationOptions { Roles = "Employee" }); ... }); app.Map("/policy-based-authorization", branchedApp => { branchedApp.UseAuthorization(new AuthorizationOptions { Policy = "EmployeeOnly" }); ... }); ... } }
当人们想要从外部限制中间件时,以上所有代码都是一种复制粘贴解决方案,但也可以轻松地将其放入中间件中(最终,我决定在服务器发送事件中间件的情况下这样做) 。
关于未来的小笔记
中间件管道中的授权状态应该发生变化。ASP.NET Core 3.0应该可以使端点路由在MVC之外可用,并且它具有对授权的支持。在ASP.NET Core 2.2中,已经存在一种授权中间件(与上面的中间件非常相似),该中间件基于IAuthorizeData
元数据来限制终结点。这意味着在3.0中可以定义指向中间件的受限端点。