Abp授权失败重定向至登录页,修改为返回401


Issues/2643所说:当您调用需要身份验证的控制器时,身份验证中间件会发现当前用户未通过身份验证,并调用 ChallengeAsync(DefaultChallengeScheme 是标识 Cookie)。此时,请求已被短路。
如果匿名控制器调用应用程序服务方法,它将执行 ABP 筛选器和侦听器。框架抛出 AbpAuthorizationException,过滤器将异常包装到 401 中,依此类推。

在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 _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();
        var statusCodeFinder = httpContext.RequestServices.GetRequiredService();

        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();
    context.Services.Replace(ServiceDescriptor.Singleton());
}

别忘记将任何以**/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;
        };
    });
    
    ...
}