自定义授权过滤器在 ASP.NET Core 3 中不起作用

Posted

技术标签:

【中文标题】自定义授权过滤器在 ASP.NET Core 3 中不起作用【英文标题】:Custom authorization filter not working in ASP.NET Core 3 【发布时间】:2020-09-22 21:06:48 【问题描述】:

我正在使用自定义授权属性筛选器向 ASP.NET Core 3.1 应用程序添加 AzureAD 身份验证(以及最终授权)。下面的代码实现了IAuthorizationFilterOnAuthorization 方法,当他们的身份验证过期时,我将用户重定向到SignIn 页面。

当带有[CustomAuthorizationFilter] 的控制器操作被点击时,我希望属性的OnAuthorization 方法会立即被点击,无论身份验证cookie 是否已过期。

这种期望不会发生,相反,如果用户未通过身份验证并且触发了控制器操作,则会自动通过 Microsoft 重新验证用户并创建有效的 cookie,然后才会触发 OnAuthorization 方法,从而击败我认为这是OnAuthorization 方法的目的。

我一直在做很多研究来了解这种行为,但我显然遗漏了一些东西。我发现的最有用的信息在Microsoft docs:

从 ASP.NET Core 3.0 开始,MVC 不会为 在控制器上发现的 [AllowAnonymous] 属性和 行动方法。此更改针对以下衍生产品在本地解决 AuthorizeAttribute,但对于 IAsyncAuthorizationFilter 和 IAuthorizationFilter 实现。

所以,IAuthorizationFilter 的实现似乎在 3.0+ 中被破坏了,我不知道如何修复它。

这种行为是正常的还是我的实现不正确?

如果正常,为什么我在OnAuthorization方法运行之前要重新认证?

如果不正确,如何正确实现?

CustomAuthorizationFilter.cs

public class CustomAuthorizationFilter : AuthorizeAttribute, IAuthorizationFilter

    public void OnAuthorization(AuthorizationFilterContext context)
    
        string signInPageUrl = "/UserAccess/SignIn";

        if (context.HttpContext.User.Identity.IsAuthenticated == false)
        
            if (context.HttpContext.Request.IsAjaxRequest())
            
                context.HttpContext.Response.StatusCode = 401;
                JsonResult jsonResult = new JsonResult(new  redirectUrl = signInPageUrl );
                context.Result = jsonResult;
            
            else
            
                context.Result = new RedirectResult(signInPageUrl);
            
        
    

使用的 IsAjaxRequest() 扩展:

//Needed code equivalent of Request.IsAjaxRequest().
//Found this solution for ASP.NET Core: https://***.com/questions/29282190/where-is-request-isajaxrequest-in-asp-net-core-mvc
//This is the one used in ASP.NET MVC 5: https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Web.Mvc/AjaxRequestExtensions.cs
public static class AjaxRequestExtensions

    public static bool IsAjaxRequest(this HttpRequest request)
    
        if (request == null)
        
            throw new ArgumentNullException("request");
        

        if (request.Headers != null)
        
            return (request.Headers["X-Requested-With"] == "XMLHttpRequest");
        

        return false;
    

Startup.cs 中的 AzureAD 身份验证实现

public void ConfigureServices(IServiceCollection services)

    IAppSettings appSettings = new AppSettings();
    Configuration.Bind("AppSettings", appSettings);

    services.AddAuthentication(AzureADDefaults.AuthenticationScheme)
        .AddAzureAD(options =>
        
            options.Instance = appSettings.Authentication.Instance;
            options.Domain = appSettings.Authentication.Domain;
            options.TenantId = appSettings.Authentication.TenantId;
            options.ClientId = appSettings.Authentication.ClientId;
            options.CallbackPath = appSettings.Authentication.CallbackPath;
        );

    services.Configure<OpenIdConnectOptions>(AzureADDefaults.OpenIdScheme, options =>
    
        options.UseTokenLifetime = false;
        options.Authority = options.Authority + "/v2.0/"; //Microsoft identity platform       
        options.TokenValidationParameters.ValidateIssuer = true;
        // https://***.com/questions/49469979/azure-ad-b2c-user-identity-name-is-null-but-user-identity-m-instance-claims9
        // https://***.com/questions/54444747/user-identity-name-is-null-after-federated-azure-ad-login-with-aspnetcore-2-2
        options.TokenValidationParameters.NameClaimType = "name";
        //https://***.com/a/53918948/12300287
        options.Events.OnSignedOutCallbackRedirect = context =>
        
            context.Response.Redirect("/UserAccess/LogoutSuccess");
            context.HandleResponse();

            return Task.CompletedTask;
        ;
    );

    services.Configure<CookieAuthenticationOptions>(AzureADDefaults.CookieScheme, options =>
    
        options.AccessDeniedPath = "/UserAccess/NotAuthorized";
        options.LogoutPath = "/UserAccess/Logout";
        options.ExpireTimeSpan = TimeSpan.FromMinutes(appSettings.Authentication.TimeoutInMinutes);
        options.SlidingExpiration = true;
    );


// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

    if (env.IsDevelopment())
    
        app.UseDeveloperExceptionPage();
    
    else
    
        app.UseExceptionHandler("/Home/Error");
        // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
        app.UseHsts();
    

    app.UseHttpsRedirection();

    app.UseStaticFiles();

    app.UseRouting();
        
    app.UseAuthentication(); // who are you?            
    app.UseAuthorization(); // are you allowed?

    app.UseEndpoints(endpoints =>
    
        endpoints.MapControllerRoute(
            name: "default",
            pattern: "controller=UserAccess/action=Login/id?");
    );

【问题讨论】:

我也遇到了同样的问题。 @GabrielCarvalho,不知道你是否已经想通了,但我发布了一个我确定的解决方案。 【参考方案1】:

我希望找到一种方法来创建一个AuthorizeAttribute 过滤器来解决这个问题,但由于时间限制,我选择了一个常规操作过滤器。它适用于 AJAX 调用,如果用户未经授权或未经身份验证,它会将用户重定向到相应的页面:

AjaxAuthorize 动作过滤器:

//custom AjaxAuthorize filter inherits from ActionFilterAttribute because there is an issue with 
//a inheriting from AuthorizeAttribute.
//post about issue: 
//https://***.com/questions/64017688/custom-authorization-filter-not-working-in-asp-net-core-3

//The statuses for AJAX calls are handled in InitializeGlobalAjaxEventHandlers JS function.

//While this filter was made to be used on actions that are called by AJAX, it can also handle
//authorization not called through AJAX.
//When using this filter always place it above any others as it is not guaranteed to run first.

//usage: [AjaxAuthorize(new[] "RoleName", "AnotherRoleName")]
public class AjaxAuthorize : ActionFilterAttribute

    public string[] Roles  get; set; 

    public AjaxAuthorize(params string[] roles)
    
        Roles = roles;
    

    public override void OnActionExecuting(ActionExecutingContext context)
    
        string signInPageUrl = "/UserAccess/SignIn";
        string notAuthorizedUrl = "/UserAccess/NotAuthorized";

        if (context.HttpContext.User.Identity.IsAuthenticated)
        
            if (Roles.Length > 0)
            
                bool userHasRole = false;
                foreach (var item in Roles)
                
                    if (context.HttpContext.User.IsInRole(item))
                    
                        userHasRole = true;
                    
                
                if (userHasRole == false)
                
                    if (context.HttpContext.Request.IsAjaxRequest())
                    
                        context.HttpContext.Response.StatusCode = 401;
                        JsonResult jsonResult = new JsonResult(new  redirectUrl = notAuthorizedUrl );
                        context.Result = jsonResult;
                    

                    else
                    
                        context.Result = new RedirectResult(notAuthorizedUrl);
                    
                
            

        
        else
        
            if (context.HttpContext.Request.IsAjaxRequest())
            
                context.HttpContext.Response.StatusCode = 403;
                JsonResult jsonResult = new JsonResult(new  redirectUrl = signInPageUrl );
                context.Result = jsonResult;
            
            else
            
                context.Result = new RedirectResult(signInPageUrl);
            
        
    

使用的 IsAjaxRequest() 扩展(重新发布以获得完整答案):

//Needed code equivalent of Request.IsAjaxRequest().
//Found this solution for ASP.NET Core: https://***.com/questions/29282190/where-is-request-isajaxrequest-in-asp-net-core-mvc
//This is the one used in ASP.NET MVC 5: https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Web.Mvc/AjaxRequestExtensions.cs
public static class AjaxRequestExtensions

    public static bool IsAjaxRequest(this HttpRequest request)
    
        if (request == null)
        
            throw new ArgumentNullException("request");
        

        if (request.Headers != null)
        
            return (request.Headers["X-Requested-With"] == "XMLHttpRequest");
        

        return false;
    

javascript ajax 全局错误处理程序:

//global settings for the AJAX error handler. All AJAX error events are routed to this function.
function InitializeGlobalAjaxEventHandlers() 
    $(document).ajaxError(function (event, xhr, ajaxSettings, thrownError) 
        //these statuses are set in the [AjaxAuthorize] action filter
        if (xhr.status == 401 || xhr.status == 403) 
            var response = $.parseJSON(xhr.responseText);
            window.location.replace(response.redirectUrl);
         else 
           RedirectUserToErrorPage();
             
    );

【讨论】:

这也有助于我的 IAuthorizationFilter 属性基于类的授权。谢谢你的回答 但是我不得不做一些改变来匹配我的场景。

以上是关于自定义授权过滤器在 ASP.NET Core 3 中不起作用的主要内容,如果未能解决你的问题,请参考以下文章

ASP.Net Core 的自定义承载令牌授权

拦截asp.net core Authorize action,授权成功后执行自定义动作

如何使用 AuthorizationHandlerContext 在 ASP.NET Core 2 自定义基于策略的授权中访问当前的 HttpContext

ASP.NET Core 使用 JWT 自定义角色/策略授权需要实现的接口

自定义页面过滤器中的 ASP .NET Core 注入服务

ASP.NET Core MVC 授权的扩展:自定义 Authorize Attribute 和 IApplicationModelProvide