首页 > 技术文章 > Abp授权失败重定向至登录页,修改为返回401

wwwk 2022-03-21 23:40 原文

问题描述

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;
        };
    });
    
    ...
}

推荐阅读