【问题标题】:How to change status code & add message from failed AuthorizationHandler policy如何更改状态代码并从失败的 AuthorizationHandler 策略添加消息
【发布时间】:2018-02-20 16:19:52
【问题描述】:

在一个实现自定义策略的 .net 核心应用程序上工作。

假设我们有一个非常简单的自定义策略:

internal class RequireNamePolicy : AuthorizationHandler<RequireNameRequirement>, IAuthorizationRequirement
{
    protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequireNameRequirement requirement)
    {
        var nameClaim = context.User.Claims.FirstOrDefault(c => c.Type == Claims.Name);
        if (nameClaim != null && nameClaim.Value == "Chimney Spork")
        {
            context.Succeed(requirement);
        }
        else
        {
            context.Fail();
        }

        return Task.CompletedTask;
    }
}

internal class RequireNameRequirement : IAuthorizationRequirement
{

}

现在假设声明不存在,所以我们点击 context.Fail()。 默认响应是没有消息正文的 403。

我的问题是,我们将在哪里更改状态代码(更改为 401)并返回一条说明问题的消息(即声明不存在)?

【问题讨论】:

    标签: .net asp.net-core


    【解决方案1】:

    context.Resource as AuthorizationFilterContext 在 net core 3.1 中为 null

    最后我把方法改写成这样:

      public class SysUserAuthHandler : AuthorizationHandler<SysUserAuthRequirement> {
    
        private readonly IFetchLoginUser fetchUser;
    
        private readonly IHttpContextAccessor httpContextAccessor;
    
        public SysUserAuthHandler( IFetchLoginUser fetchLoginUser, IHttpContextAccessor httpContextAccessor ) {
          fetchUser = fetchLoginUser;
          this.httpContextAccessor = httpContextAccessor;
        }
    
        protected override Task HandleRequirementAsync( AuthorizationHandlerContext context, SysUserAuthRequirement requirement ) {
          var httpContext = httpContextAccessor.HttpContext;
          byte[] bytes;
          string msg;
    
          if (!string.IsNullOrWhiteSpace( context.User.Identity.Name )) {
            var myUser = fetchUser.LoadUser( context.User.Identity.Name, SystemEnum.FooSytem);
    
            if ((myUser.Auth & requirement.Auth) == requirement.Auth) {
              context.Succeed( requirement );
              return Task.CompletedTask;
            }
    
            msg = requirement.Auth switch {
              1 => "You don't have Auth of Maker",
              2 => "You don't have Auth of Checker",
              4 => "You don't have Auth of Admin",
              8 => "You don't have Auth of Operator",
              _ => "You don't have Auth"
            };
          }
          else {
            msg = "User Invalid, Please check your login status or login again";
          }
    
          bytes = Encoding.UTF8.GetBytes( msg );
          httpContext.Response.StatusCode = 405;
          httpContext.Response.ContentType = "application/json";
          httpContext.Response.Body.WriteAsync( bytes, 0, bytes.Length );
          //context.Succeed( requirement );
          return Task.CompletedTask;
        }
    
      }
    
      public class SysUserAuthRequirement : IAuthorizationRequirement {
    
        public long Auth { get; private set; }
    
        public SysUserAuthRequirement( long auth ) {
          Auth = auth;
        }
    
      }
    

    不要忘记在启动中添加这一行

        services.AddHttpContextAccessor();
    

    【讨论】:

      【解决方案2】:

      自定义 AuthorizationMiddleware 的行为的文档可以在下面找到:

      https://docs.microsoft.com/en-us/aspnet/core/security/authorization/customizingauthorizationmiddlewareresponse?view=aspnetcore-5.0

      我的代码最终看起来像这样:

      public class GuidKeyAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
      {
          private readonly AuthorizationMiddlewareResultHandler
               DefaultHandler = new AuthorizationMiddlewareResultHandler();
      
          public async Task HandleAsync(
              RequestDelegate requestDelegate,
              HttpContext httpContext,
              AuthorizationPolicy authorizationPolicy,
              PolicyAuthorizationResult policyAuthorizationResult)
          {
      
              if (policyAuthorizationResult.Challenged && !policyAuthorizationResult.Succeeded && authorizationPolicy.Requirements.Any(requirement => requirement is GuidKeyRequirement))
              {
                  httpContext.Response.StatusCode = (int)HttpStatusCode.Forbidden;
                  return;
              }
      
              // Fallback to the default implementation.
              await DefaultHandler.HandleAsync(requestDelegate, httpContext, authorizationPolicy,
                                     policyAuthorizationResult);
          }
      }
      

      Startup.cs:

      services.AddSingleton<IAuthorizationMiddlewareResultHandler,
          GuidKeyAuthorizationMiddlewareResultHandler>();
      

      【讨论】:

      • IAuthorizationMiddlewareResultHandler 是 asp.net 5 中的新功能!
      【解决方案3】:

      我已经使用 ExceptionHandler 实现了它。我想保留 403 状态码,但要自定义错误消息。所以我创建了自定义异常:

      public class ForbiddenException : BaseException
      {
          public ForbiddenException(string message) : base(HttpStatusCode.Forbidden, message)
          {
          }
      
          public ForbiddenException(string message, object info) : base(HttpStatusCode.Forbidden, message, info)
          {
          }
      }
      
      public abstract class BaseException : Exception
      {
          protected BaseException(HttpStatusCode httpErrorCode, string message, Exception innerException = null) : base(message,
              innerException)
          {
              HttpErrorCode = httpErrorCode;
          }
      
          protected BaseException(HttpStatusCode httpErrorCode, string message, object info, Exception innerException = null) :
              this(httpErrorCode,
                  message, innerException)
          {
              Info = info;
          }
      
          public HttpStatusCode HttpErrorCode { get; set; }
      
          public object Info { get; set; }
      }
      

      然后在 AuthorizationHandler 里面我抛出了这个异常:

      public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
      {
          protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
          {
              List<Claim> userPermissionsClaims =
                  context.User.Claims.Where(c => c.Type == PermissionConstants.PermissionClaimType).ToList();
      
              if (userPermissionsClaims.Any(pc => requirement.Permissions.Any(p => (Permissions)Convert.ToInt16(pc.Value) == p)))
              {
                  context.Succeed(requirement);
              }
              else
              {
                  throw new ForbiddenException(string.Format(ErrorMessages.PermissionsRequired, string.Join(", ", requirement.Permissions)));
              }
      
              return Task.CompletedTask;
          }
      }
      

      这是一个全局异常处理程序:

      app.UseExceptionHandler(exceptionHandler =>
              {
                  exceptionHandler.Run(async context =>
                  {
                      await HandleException(context.Features.Get<IExceptionHandlerFeature>().Error, context, env);
                  });
              });
      
      public async Task HandleException(Exception exception, HttpContext context)
          {
              var message = exception?.Message;
      
              if (exception is BaseException)
              {
                  context.Response.StatusCode = (int)(exception as BaseException)?.HttpErrorCode;
              }
              else
              {
                  context.Response.StatusCode = (int)HttpStatusCode.InternalServerError;
              }
      
              await context.Response.WriteAsync(message);
              await context.Response.CompleteAsync();
          }
      

      【讨论】:

        【解决方案4】:

        使用依赖注入设置 ITempDataProviderIHttpContextAccessor

        在 HandleRequirementAsync 方法中保存消息:

        Dictionary<string, object> errs = new Dictionary<string, object>();
        errs.Add("AccessDeniedReason", "The current domain is not enabled!");
                        
        TempDataProvider.SaveTempData(HttpContextAccessor.HttpContext, errs);
        context.Fail();
        

        在视图中加载消息:

        @page
        @model AccessDeniedModel
        @inject ITempDataProvider TempDataProvider
        
        @{
        var t =  TempDataProvider.LoadTempData(HttpContext);
        string AccessDeniedReason = (string)t?["AccessDeniedReason"];
        
        // ...
        }
        

        【讨论】:

          【解决方案5】:

          上面为 asp.netcore 3.1 工作的那些在我的集成测试中失败了,但 Postman 工作。我最终做了类似以下的事情:

          public class MyAuthorizationHandler: AuthorizationHandler<PermissionRequirement>
          {
              private readonly IHttpContextAccessor _httpContextAccessor;
          
              public MyAuthorizationHandler(IHttpContextAccessor httpContextAccessor)
              {
                  _httpContextAccessor = httpContextAccessor;
              }
          
              protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
              {
                  //code to check for authorization
                  if (isAuthorized)
                  {
                      context.Succeed(requirement);
                  }
                  else
                  {
                      var message = "Custom message";
                      _httpContextAccessor.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
                      _httpContextAccessor.HttpContext.Response.ContentType = "application/json";
                      await _httpContextAccessor.HttpContext.Response.WriteAsync(message);
                      await _httpContextAccessor.HttpContext.Response.CompleteAsync(); //Needed this or my integration tests failed with a System.IO exception.
                      context.Fail();
                  }
              }
          }
          
          internal class PermissionRequirement : IAuthorizationRequirement
          {
          
          }
          

          更新 事实证明,即使上述代码在 Postman 测试和集成测试中正确执行,也会记录 Splunk 错误:“System.InvalidOperationException:无法设置 StatusCode,因为响应已经开始。” 我最终升级到 .net 5.0 并改用中间件结果处理程序。 https://docs.microsoft.com/en-us/aspnet/core/security/authorization/customizingauthorizationmiddlewareresponse?view=aspnetcore-5.0。用于创建消息的代码类似,只是不需要 CompleteAsync()。

          【讨论】:

            【解决方案6】:

            这还没有实现。您可以关注此here。一种可能的解决方法是:

            internal class RequireNamePolicy : AuthorizationHandler<RequireNameRequirement>, IAuthorizationRequirement
            {
                protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, RequireNameRequirement requirement)
                {
                    var authorizationFilterContext = context.Resource as AuthorizationFilterContext;
            
                    var nameClaim = context.User.Claims.FirstOrDefault(c => c.Type == Claims.Name);
                    if (nameClaim != null && nameClaim.Value == "Chimney Spork")
                    {
                        context.Succeed(requirement);
                    }
                    else
                    {
                        authorizationFilterContext.Result = new JsonResult("Custom message") { StatusCode = 401 };
                        context.Succeed(requirement);
                    }
            
                    return Task.CompletedTask;
                }
            }
            

            【讨论】:

            • 这似乎有效,但看起来丑得要命。谢谢
            • 返回 200 失败
            猜你喜欢
            • 2015-01-01
            • 1970-01-01
            • 2014-01-22
            • 1970-01-01
            • 2021-03-31
            • 1970-01-01
            • 1970-01-01
            • 2017-02-12
            • 1970-01-01
            相关资源
            最近更新 更多