【问题标题】:How to check custom attributes inside authorization process (policy or middleware)?如何检查授权过程中的自定义属性(策略或中间件)?
【发布时间】:2018-08-13 16:14:23
【问题描述】:

主要目标是在 OIDC 用户具有类型为“BlockedFrom”的自定义声明时阻止访问门户,该声明已添加到 ClaimsTransformation 中。

我已经通过Startup.Configure 方法中的中间件解决了它。一般原因是保留原始请求 URL 而不重定向到 /Account/AccessDenied 页面。

app.Use((context, next) =>
{
    var user = context.User;

    if (user.IsAuthenticated())
    {
        // Do not rewrite path when it marked with custom [AllowBlockedAttribute]!
        // /Home/Logout, for example. But how?
        //
        if (user.HasClaim(x => x.Type == UserClaimTypes.BlockedFrom))
        {
            // Rewrite to run specific method of HomeController for blocked users
            // with detailed message.
            //
            context.Request.Path = GenericPaths.Blocked;
        }
    }

    return next();
});

但是有一个意想不到的结果:HomeControllerLogout 方法也被阻塞了。用户在被阻止时无法注销,哈哈! 想到的第一件事-检查自定义属性,例如[AllowBlockedAttribute]。中间件中的硬编码路径常量看起来很疯狂。如何访问中间件中调用方法的属性?

另一种(更优雅)的方法是将此逻辑放入自定义BlockedHandler : AuthorizationHandler<BlockedRequirement> 并将其分配到Startup.ConfigureServices 方法的MVC 选项中作为一般策略:

services.AddSingleton<IAuthorizationHandler, BlockedHandler>();

services.AddMvc(options =>
{
    var policy = new AuthorizationPolicyBuilder()
        .RequireAuthenticatedUser()
        .AddRequirements(new BlockedRequirement())
        .Build();

    // Set the default authentication policy to require users to be authenticated.
    //
    options.Filters.Add(new AuthorizeFilter(policy));

}).SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

BlockedHandler的假设实现:

public class BlockedHandler : AuthorizationHandler<BlockedRequirement>
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, BlockedRequirement requirement)
    {
        if (!context.User.HasClaim(c => c.Type == UserClaimTypes.BlockedFrom))
        {
            context.Succeed(requirement);
            return Task.CompletedTask;
        }

        // User is blocked!

        if (context.Resource is AuthorizationFilterContext mvcContext)
        {
            if (mvcContext.ActionDescriptor is ControllerActionDescriptor descriptor)
            {
                var allowBlocked = descriptor.ControllerTypeInfo.CustomAttributes
                    .Concat<CustomAttributeData>(descriptor.MethodInfo.CustomAttributes)
                    .Any(x => x.AttributeType == typeof(AllowBlockedAttribute));

                // User can access called action.
                //
                if (allowBlocked)
                    context.Succeed(requirement);
            }

            // Ugly to call this as the next step?
            // mvcContext.HttpContext.Request.Path = GenericPaths.Blocked;
        }

        // Prevent redirection to AccessDenied
        // Stop authorization chain.

        return Task.CompletedTask;
    }
}

好的,现在我们可以处理自定义属性了。似乎 AuthorizationHandler 不是告诉 HttpContext 在没有重定向的情况下更改它的 RequestPath 的最佳位置。哪里可以做?

【问题讨论】:

    标签: c# asp.net-core-2.1


    【解决方案1】:

    我已经对框架源代码进行了一些挖掘,并找到了一种以授权处理程序方式进行这项工作的方法。

    授权过程的入口点是AuthorizeFilter。过滤器上下文有一个接受 IActionResultResult 属性。通过设置此属性,您可以缩短请求并显示您想要的任何操作结果(包括视图)。这是解决问题的关键。

    如果您遵循执行路径,您会意识到过滤器上下文已传递给授权组件,并且在 IAuthorizationHandler.HandleRequirementAsync 方法中可用。您可以通过向下转换从上下文对象的 Resource 属性中获取它(如 OP 所示)。

    还有一件更重要的事情:您必须从授权处理程序返回成功,否则您最终会不可避免地发生重定向。 (如果您查看default implementation of IPolicyEvaluator,这一点就会变得清晰。)

    所以把这一切放在一起:

    public class BlockedHandler : AuthorizationHandler<BlockedRequirement>
    {
        private Task HandleBlockedAsync(AuthorizationFilterContext filterContext)
        {
            // create a model for the view if needed...
            var model = new BlockedModel();
    
            // do some processing if needed...
    
            var modelMetadataProvider = filterContext.HttpContext.RequestServices.GetService<IModelMetadataProvider>();
            // short-circuit request by setting the action result
            filterContext.Result = new ViewResult
            {
                StatusCode = 403, // Client cannot access the requested resource
    
                ViewName = "~/Views/Shared/Blocked.cshtml",
                ViewData = new ViewDataDictionary(modelMetadataProvider, filterContext.ModelState) { Model = model }
            };
    
            return Task.CompletedTask;
        }
    
        protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, BlockedRequirement requirement)
        {
            if (context.User.HasClaim(c => c.Type == UserClaimTypes.BlockedFrom) && 
                context.Resource is AuthorizationFilterContext filterContext &&
                filterContext.ActionDescriptor is ControllerActionDescriptor descriptor)
            {
                var allowBlocked = descriptor.ControllerTypeInfo.CustomAttributes
                    .Concat(descriptor.MethodInfo.CustomAttributes)
                    .Any(x => x.AttributeType == typeof(AllowBlockedAttribute));
    
                if (!allowBlocked)
                    await HandleBlockedAsync(filterContext);
            }
    
            // We must return success in every case to avoid forbid/challenge.
            context.Succeed(requirement);
        }
    }
    

    【讨论】:

    • 我对在授权处理程序中构建视图有疑问,但它确实可以按预期工作。好在用户不能通过调用Controller Action直接访问这个页面存根,并且他的状态可以通过页面刷新来改变。
    • 在转换context.Resource 时,我得到了空值,因为它的类型为Microsoft.AspNetCore.Routing.RouteEndpoint。你如何获得AuthorizationFilterContext
    • 为了回答我自己的问题,这在 ASP.NET Core 3.1 中发生了变化。见:stackoverflow.com/questions/58565574/…
    【解决方案2】:

    我认为 AuthorizationHandler 绝对是放置这个逻辑的更好地方。但是 - 如果我理解正确的话 - 你的问题是在这个处理程序执行时已经选择了要调用的操作,所以你不能再更改路由了。

    当然,标准方式是启动重定向,但您希望避免这种情况以保留当前 URL。

    鉴于上述情况,我能想到一种方法:全局action filter

    动作过滤器可以在调用单个动作方法之前和之后立即运行代码。它们可用于操作传递给动作的参数和动作返回的结果。

    OnActionExecuting 方法似乎是放置逻辑的正确位置,因为此时您可以访问操作方法的属性并且您有机会使处理短路(通过设置ActionExecutingContext 参数的 Result 属性)如果用户被阻止。

    如果您不熟悉过滤器的概念,您可以在this MSDN article 中找到所有详细信息。

    【讨论】:

    • 作为授权过程的一部分的阻止似乎比动作过滤器更准确。它带有 AllowAnonymous 绕过和其他东西作为管道中的授权过滤器。但我听说重写 AuthorizeFilter 不是一个好习惯。所以它按要求工作......
    • @Arsync 是的,这段代码确实应该是授权过程的一部分。查看我的其他答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-10-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多