Abp授權失敗重定向至登錄頁,修改為返回401


問題描述

Abp 5.X版本,未認證直接訪問API重定向至登錄頁。

異常日志

[01:02:56 INF] Authorization failed. These requirements were not met:
PermissionRequirement: AbpIdentity.Users
[01:02:56 WRN] ---------- RemoteServiceErrorInfo ----------
{
  "code": "Volo.Authorization:010001",
  "message": "授權失敗! 提供的策略尚未授予.",
  "details": null,
  "data": {},
  "validationErrors": null
}

[01:02:56 WRN] Exception of type 'Volo.Abp.Authorization.AbpAuthorizationException' was thrown.
Volo.Abp.Authorization.AbpAuthorizationException: Exception of type 'Volo.Abp.Authorization.AbpAuthorizationException' was thrown.
   ...
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextExceptionFilterAsync>g__Awaited|26_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
[01:02:56 WRN] Code:Volo.Authorization:010001
[01:02:56 INF] AuthenticationScheme: Identity.Application was challenged.
[01:02:56 INF] Executed action Volo.Abp.Identity.IdentityUserController.GetListAsync (Volo.Abp.Identity.HttpApi) in 167.7765ms
[01:02:56 INF] Executed endpoint 'Volo.Abp.Identity.IdentityUserController.GetListAsync (Volo.Abp.Identity.HttpApi)'
[01:02:56 INF] Request finished HTTP/2 GET https://localhost:44324/api/identity/users - - - 302 0 - 268.2960ms
[01:02:56 INF] Request starting HTTP/2 GET https://localhost:44324/Account/Login?ReturnUrl=%2Fapi%2Fidentity%2Fusers - -
[01:02:56 INF] Executing endpoint '/Account/Login'

期望目標

訪問API時,與Abp4.X行為一致。返回如下

{
  "error": {
    "code": "Volo.Authorization:010001",
    "message": "Authorization failed! Given policy has not granted.",
    "details": null,
    "data": {},
    "validationErrors": null
  }
}

如何解決

該問題在**Abp5.X**版本之前就存在(如果Abp5.X生成模板的時候,選擇分離IdentityServer則只會返回401且無返回值。不分離的話會走IdentityServerCookie認證,就會導致重定向至登錄頁,比如在Abp4.4.4新建一個Controller,並添加[Authorize]

[Route("api/test")]
[Authorize]
public class TestController : Test4Controller
{
    [HttpGet]
    public Task TestAsync()
    {
        return Task.CompletedTask;
    }
}

直接訪問就會重定向至登錄頁。
但是,如果你是按照標准寫法,通過Application.Contracts層創建接口,然后Controller層調用。

[Authorize]
public class NewTestAppService : Test4AppService, INewTestAppService
{
    public Task GetTestAsync()
    {
        return Task.CompletedTask;
    }
}
[Route("api/new-test")]
public class NewTestController : Test4Controller, INewTestAppService
{
    private readonly INewTestAppService _newTestAppService;

    public NewTestController(INewTestAppService newTestAppService)
    {
        _newTestAppService = newTestAppService;
    }

    [HttpGet]
    public Task GetTestAsync()
    {
        return _newTestAppService.GetTestAsync();
    }
}

則會返回標准異常Json。

根據Issues/2643所說:當您調用需要身份驗證的控制器時,身份驗證中間件會發現當前用戶未通過身份驗證,並調用 ChallengeAsync(DefaultChallengeScheme 是標識 Cookie)。此時,請求已被短路。
如果匿名控制器調用應用程序服務方法,它將執行 ABP 篩選器和偵聽器。框架拋出 AbpAuthorizationException,過濾器將異常包裝到 401 中,依此類推。

代碼上的原因是:通過abp new AbpDemo -u none創建的項目,會將Identity Server相關模塊和接口項目集成在一起, Volo.Abp.Account.Web.IdentityServer模塊配置了Identity Cookies認證方案,啟動項里面也配置了JWT認證方案。其中AbpAccountWebIdentityServerModule配置IdentityCookies認證方案

// TODO: Try to reuse from AbpIdentityAspNetCoreModule
context.Services
    .AddAuthentication(o =>
    {
        // IdentityConstants.ApplicationScheme 為 Identity.Application
        o.DefaultScheme = IdentityConstants.ApplicationScheme;
        o.DefaultSignInScheme = IdentityConstants.ExternalScheme;
    })
    .AddIdentityCookies();

在Abp5.X版本中,Abp官方將認證行為保持一致(Issues/9926),從而導致了之后版本,匿名訪問需認證API將會重定向至登錄頁。這是一次非常好的改動。

第一種.Net Core傳統解決方案。

private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
    context.Services.ConfigureApplicationCookie(options =>
    {
        options.ForwardDefaultSelector = ctx =>
        {
            return ctx.Request.Path.StartsWithSegments("/api") ? JwtBearerDefaults.AuthenticationScheme : null;
        };
    });

    context.Services.AddAuthentication().AddJwtBearer(options =>
    {
        options.Authority = configuration["AuthServer:Authority"];
        options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);
        options.Audience = "Test";
        options.BackchannelHttpHandler = new HttpClientHandler
        {
            ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
        };
        options.Events = new JwtBearerEvents
        {
            OnChallenge = async context =>
            {
                context.HandleResponse();
                context.Response.ContentType = "application/json;charset=utf-8";
                context.Response.StatusCode = StatusCodes.Status401Unauthorized;

                var response = new RemoteServiceErrorResponse(new RemoteServiceErrorInfo("未認證"));
                await context.Response.WriteAsJsonAsync(response);
            },
            OnForbidden = async context =>
            {
                context.Response.ContentType = "application/json;charset=utf-8";
                context.Response.StatusCode = StatusCodes.Status403Forbidden;

                var response = new RemoteServiceErrorResponse(new RemoteServiceErrorInfo("未授權"));
                await context.Response.WriteAsJsonAsync(response);
            }
        };
    });
}

其中返回信息根據實際情況填寫。

第二種方法是將行為盡量和Abp趨於一致。僅限**.NET 5.0/6.0**.
新建AuthorizationExceptionHandler類,繼承IAbpAuthorizationExceptionHandler接口。

public class AuthorizationExceptionHandler : IAbpAuthorizationExceptionHandler
{
    private readonly Func<object, Task> _clearCacheHeadersDelegate;

    public AuthorizationExceptionHandler()
    {
        _clearCacheHeadersDelegate = ClearCacheHeaders;
    }

    public Task HandleAsync(AbpAuthorizationException exception, HttpContext httpContext)
    {
        return HandleAndWrapExceptionAsync(exception, httpContext);
    }

    protected virtual async Task HandleAndWrapExceptionAsync(AbpAuthorizationException exception, HttpContext httpContext)
    {
        var errorInfoConverter = httpContext.RequestServices.GetRequiredService<IExceptionToErrorInfoConverter>();
        var statusCodeFinder = httpContext.RequestServices.GetRequiredService<IHttpExceptionStatusCodeFinder>();

        httpContext.Response.Clear();
        httpContext.Response.StatusCode = (int)statusCodeFinder.GetStatusCode(httpContext, exception);
        httpContext.Response.OnStarting(_clearCacheHeadersDelegate, httpContext.Response);
        httpContext.Response.Headers.Add(AbpHttpConsts.AbpErrorFormat, "true");

        await httpContext.Response.WriteAsJsonAsync(
                new RemoteServiceErrorResponse(
                    errorInfoConverter.Convert(exception)
            )
        );
    }

    private Task ClearCacheHeaders(object state)
    {
        var response = (HttpResponse)state;

        response.Headers[HeaderNames.CacheControl] = "no-cache";
        response.Headers[HeaderNames.Pragma] = "no-cache";
        response.Headers[HeaderNames.Expires] = "-1";
        response.Headers.Remove(HeaderNames.ETag);

        return Task.CompletedTask;
    }
}

新建AuthorizationMiddlewareResultHandler類,繼承IAuthorizationMiddlewareResultHandler接口。

public class AuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
    private readonly IAbpAuthorizationExceptionHandler _authorizationExceptionHandler;

    public AuthorizationMiddlewareResultHandler(IAbpAuthorizationExceptionHandler authorizationExceptionHandler)
    {
        _authorizationExceptionHandler = authorizationExceptionHandler;
    }

    public async Task HandleAsync(
        RequestDelegate next,
        HttpContext context,
        AuthorizationPolicy policy,
        PolicyAuthorizationResult authorizeResult)
    {
        if (authorizeResult.Challenged)
        {
            await context.ChallengeAsync();
            await _authorizationExceptionHandler.HandleAsync(
                new AbpAuthorizationException(code: AbpAuthorizationErrorCodes.GivenPolicyHasNotGranted), context);
            return;
        }

        if (authorizeResult.Forbidden)
        {
            await context.ForbidAsync();
            await _authorizationExceptionHandler.HandleAsync(
                new AbpAuthorizationException(code: AbpAuthorizationErrorCodes.GivenPolicyHasNotGranted), context);
            return;
        }

        await next(context);
    }
}

將其注入到容器內。

public override void ConfigureServices(ServiceConfigurationContext context)
{
    context.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, AuthorizationMiddlewareResultHandler>();
    context.Services.Replace(ServiceDescriptor.Singleton<IAbpAuthorizationExceptionHandler, AuthorizationExceptionHandler>());
}

別忘記將任何以**/api**開頭的請求轉發到 JWT 方案。

private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
{
    context.Services.ConfigureApplicationCookie(options =>
    {
        options.ForwardDefaultSelector = ctx =>
        {
            return ctx.Request.Path.StartsWithSegments("/api") ? JwtBearerDefaults.AuthenticationScheme : null;
        };
    });
    
    ...
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM