ASP.NET 核心 3.0 政策重定向

ASP.NET Core 3.0 Policy Redirect

我们有这部分代码控制某些服务高级页面。在方法拒绝重定向到升级页面,对于没有高级会员资格的人。

代码在 asp.NET 核心 2 上运行完美,但在 asp.NET 核心 3 上运行失败。 context.Resource 不再是 AuthorizationFilterContext 类型,但端点未提供结果成员。

¿如何使用 asp.Net 核心 3 上提供的新 Enpoint 使页面重定向?

public Task Deny(AuthorizationHandlerContext context, SubscriptionRequirement requirement)
    {
        var mvcContext = context.Resource as AuthorizationFilterContext;
        if (mvcContext == null)
            return Task.CompletedTask;

        mvcContext.Result = new RedirectToActionResult("Upgrade", "Subscription", new { ReturnUrl = _contextAccessor.HttpContext.Request.Path });
        context.Succeed(requirement);
        return Task.CompletedTask;
    }

根据 SOC 负责人的说法,将授权与响应重定向混在一起似乎不是一个好的做法。

相反,您可以将授权逻辑包装到策略中,然后调用 IAuthorizationService 并根据需要重定向 anywhere/anytime。

假设您定义了一个 "premium membership" 策略。然后您可以毫不费力地使用 Middleware/Resource Filter/Action 过滤器甚至操作方法重定向请求。例如,我创建 MembershipResourceFilter 如下:

public class MembershipResourceFilter : IAsyncResourceFilter
{
    public async Task OnResourceExecutionAsync(ResourceExecutingContext context, ResourceExecutionDelegate next)
    {
        var HttpContext = context.HttpContext;
        var authZ = HttpContext.RequestServices.GetRequiredService<IAuthorizationService>();
        var routeData= context.RouteData;
        var result = await authZ.AuthorizeAsync(HttpContext.User, routeData,"premium membership");
        if(!result.Succeeded)
        {
            context.Result = new RedirectToActionResult("Upgrade", "Subscription", new { ReturnUrl = HttpContext.Request.Path });
        }
        await next();
    }
}

我用以下策略测试了上面的代码,它对我来说工作正常。

services.AddAuthorization(o =>{
    o.AddPolicy("premium membership", pb => pb
        .RequireAuthenticatedUser()
        .RequireAssertion((context)=>{
            // check current context.User has premium membership
            var user = context.User;
            var routeData = context.Resource as RouteData;
            if(routeData != null){
                try{
                    var controller = routeData.Values["controller"]?.ToString();
                    var action = routeData.Values["action"]?.ToString();
                    // now you get the route value
                    if(controller == "Home" && action == "Action"){
                        // ...
                        return true;
                    }
                }catch{
                    return false;
                }
            }
            return false;
        })
    );
});

[编辑]

如果您不想更改 [Authorize("Premium")],您可以创建一个简单的 中间件 而不是资源过滤器:

    ...
    app.UseAuthentication();
    app.UseRouting();


    app.Use(async(ctx,next)=>{
        var ep= ctx.Features.Get<IEndpointFeature>()?.Endpoint;
        var authAttr = ep?.Metadata?.GetMetadata<AuthorizeAttribute>()
        if(authAttr!=null && authAttr.Policy == "premium membership"){
            var authService = ctx.RequestServices.GetRequiredService<IAuthorizationService>();
            var result = await authService.AuthorizeAsync(ctx.User, ctx.GetRouteData(),authAttr.Policy);
            if(!result.Succeeded)
            {
                var path = $"/Subscription/Upgrade?ReturnUrl={ctx.Request.Path}";
                ctx.Response.Redirect(path) ;
                return;
            }
        }
        await next();
    });

    app.UseAuthorization();
    app.UseEndpoints(endpoints =>{ ... });

中间件和资源过滤器基本上做同样的事情:调用授权服务并在需要时重定向。

我通过将 IHttpContextAccessor 注入 AuthorizationHandler 构造函数然后执行以下操作解决了这个问题:

public Task Deny(AuthorizationHandlerContext context, SubscriptionRequirement requirement)
{
    //your logic here
    context.Succeed(requirement);
    return _contextAccessor.HttpContext.ExecuteResultAsync(
      new RedirectToActionResult("Upgrade", "Subscription", new { 
        ReturnUrl = _contextAccessor.HttpContext.Request.Path 
    }));
}

HttpContext.ExecuteResultAsync是扩展方法:

 public static class HttpContextExtensions  
{
    private static readonly RouteData EmptyRouteData = new RouteData();

    private static readonly ActionDescriptor EmptyActionDescriptor = new ActionDescriptor();

    public static Task ExecuteResultAsync<TResult>(this HttpContext context, TResult result)
        where TResult : IActionResult
    {
        if (context == null) throw new ArgumentNullException(nameof(context));
        if (result == null) throw new ArgumentNullException(nameof(result));

        var executor = context.RequestServices.GetRequiredService<IActionResultExecutor<TResult>>();

        var routeData = context.GetRouteData() ?? EmptyRouteData;
        var actionContext = new ActionContext(context, routeData, EmptyActionDescriptor);

        return executor.ExecuteAsync(actionContext, result);
    }
}

我正在使用核心 3.1,以防它有所作为。