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

标签 c# asp.net-mvc asp.net-core authentication asp.net-core-3.1

我正在使用自定义授权属性过滤器向 ASP.NET Core 3.1 应用程序添加 AzureAD 身份验证(并最终授权)。下面的代码实现了 IAuthorizationFilterOnAuthorization我将用户重定向到 SignIn 的方法当他们的身份验证到期时页面。
当 Controller Action 带有 [CustomAuthorizationFilter]命中我希望属性的 OnAuthorization无论身份验证 cookie 是否已过期,都会立即命中的方法。
这种期望不会发生,相反,如果用户未通过身份验证并且触发了 Controller 操作,则用户会自动通过 Microsoft 重新进行身份验证并创建一个有效的 cookie,然后才会出现 OnAuthorization方法被击中,击败我认为是OnAuthorization的目的方法。
我一直在做很多研究来理解这种行为,但我显然错过了一些东西。我发现的最有用的信息是在 Microsoft docs :

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+ 中坏了,我不知道如何修复它。
这种行为是正常的还是我的实现不正确?
如果正常,为什么我在OnAuthorization之前重新认证方法运行?
如果不正确,我该如何正确实现?
自定义授权过滤器.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://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 身份验证实现
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 filter 来解决这个问题,但由于时间限制,我决定使用常规操作过滤器。它适用于 AJAX 调用,如果用户未经授权或未经身份验证,它会将用户重定向到适当的页面:
AjaxAuthorize Action 过滤器:

//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() 扩展(重新发布以获得完整答案):
//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 全局错误处理程序:
//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();
        }     
    });
}

关于c# - 自定义授权过滤器在 ASP.NET Core 3 中不起作用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64017688/

相关文章:

c# - Parallel.ForEach - 嵌套实例对象的线程安全

c# - String NULL 是否总是等于 C# 中的另一个 String NULL?

c# - Linq 避免两次调用函数

asp.net-mvc - ASP.NET Razor C# Html.ActionLink 创建空链接

c# - JavaScript float 错误

c# - 输入验证错误类未添加到自定义 HtmlHelper 扩展方法中

asp.net-mvc - ASP.NET MVC 中不明确的 Controller 名称

azure - 如何使用与 Azure.Storage 之间的 HTTP 分块文件传输?

c# - 用于控制请求对象解构的选项

mysql - 如何从 .Net Core 连接到 MySQL