自定义授权过滤器在 ASP.NET Core 3 中不起作用 [英] Custom authorization filter not working in ASP.NET Core 3

查看:34
本文介绍了自定义授权过滤器在 ASP.NET Core 3 中不起作用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm working on adding authentication (and eventually authorization) with AzureAD to an ASP.NET Core 3.1 app using a custom authorization attribute filter. The code below implements the IAuthorizationFilter's OnAuthorization method within which I redirect the user to the SignIn page when their authentication expires.

当使用 [CustomAuthorizationFilter] 的控制器操作被命中时,我希望属性的 OnAuthorization 方法会立即被命中,无论身份验证 cookie 是否已过期.

When a controller action with [CustomAuthorizationFilter] is hit I expect the attribute's OnAuthorization method to be hit right away whether or not the authentication cookie has expired.

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

That expectation doesn't happen and instead if a user is not authenticated and a controller action is hit, user is automatically reauthenticated with Microsoft and a valid cookie is created, and only then the OnAuthorization method is hit, defeating what I thought was the purpose of the OnAuthorization method.

我一直在进行大量研究以了解这种行为,但显然我遗漏了一些东西.我发现的最有用的信息是 微软文档:

I've been doing a lot of research to understand this behavior, but I'm clearly missing something. The most useful piece of information I found was in Microsoft docs:

从 ASP.NET Core 3.0 开始,MVC 不会为[AllowAnonymous] 在控制器上发现的属性和动作方法.这一变化在本地解决了衍生品AuthorizeAttribute,但这是一个突破性的变化IAsyncAuthorizationFilter 和 IAuthorizationFilter 实现.

As of ASP.NET Core 3.0, MVC doesn't add AllowAnonymousFilters for [AllowAnonymous] attributes that were discovered on controllers and action methods. This change is addressed locally for derivatives of AuthorizeAttribute, but it's a breaking change for IAsyncAuthorizationFilter and IAuthorizationFilter implementations.

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

So, it appears that implementations with IAuthorizationFilter may be broken in 3.0+ and I don't know how to fix it.

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

Is this behavior normal or is my implementation incorrect?

如果正常,为什么我在 OnAuthorization 方法运行之前重新进行身份验证?

If normal, why am I reauthenticated before the OnAuthorization method runs?

如果不正确,我该如何正确实施?

If incorrect, how can I implement it correctly?

CustomAuthorizationFilter.cs

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() 扩展:

The IsAjaxRequest() extension used:

//Needed code equivalent of Request.IsAjaxRequest().
//Found this solution for ASP.NET Core: https://stackoverflow.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 身份验证实现

AzureAD authentication implementation in Startup.cs

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://stackoverflow.com/questions/49469979/azure-ad-b2c-user-identity-name-is-null-but-user-identity-m-instance-claims9
        // https://stackoverflow.com/questions/54444747/user-identity-name-is-null-after-federated-azure-ad-login-with-aspnetcore-2-2
        options.TokenValidationParameters.NameClaimType = "name";
        //https://stackoverflow.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?}");
    });
}

推荐答案

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

I hoped to find a way to create an AuthorizeAttribute filter to solve this issue, but due to time constraints I settled on a regular action filter. It works with AJAX calls and it redirects the user to the appropriate pages if they are unauthorized or unauthenticated:

AjaxAuthorize 动作过滤器:

AjaxAuthorize action filter:

//custom AjaxAuthorize filter inherits from ActionFilterAttribute because there is an issue with 
//a inheriting from AuthorizeAttribute.
//post about issue: 
//https://stackoverflow.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() 扩展(重新发布以获得完整答案):

The IsAjaxRequest() extension used (reposted for a complete answer):

//Needed code equivalent of Request.IsAjaxRequest().
//Found this solution for ASP.NET Core: https://stackoverflow.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 全局错误处理程序:

JavaScript ajax global error handler:

//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();
        }     
    });
}

这篇关于自定义授权过滤器在 ASP.NET Core 3 中不起作用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆