问题描述

Abp 5.X版本,未认证直接访问API重定向至登录页。

异常日志

  1. [01:02:56 INF] Authorization failed. These requirements were not met:
  2. PermissionRequirement: AbpIdentity.Users
  3. [01:02:56 WRN] ---------- RemoteServiceErrorInfo ----------
  4. {
  5. "code": "Volo.Authorization:010001",
  6. "message": "授权失败! 提供的策略尚未授予.",
  7. "details": null,
  8. "data": {},
  9. "validationErrors": null
  10. }
  11. [01:02:56 WRN] Exception of type 'Volo.Abp.Authorization.AbpAuthorizationException' was thrown.
  12. Volo.Abp.Authorization.AbpAuthorizationException: Exception of type 'Volo.Abp.Authorization.AbpAuthorizationException' was thrown.
  13. ...
  14. at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeInnerFilterAsync()
  15. --- End of stack trace from previous location ---
  16. at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextExceptionFilterAsync>g__Awaited|26_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted)
  17. [01:02:56 WRN] Code:Volo.Authorization:010001
  18. [01:02:56 INF] AuthenticationScheme: Identity.Application was challenged.
  19. [01:02:56 INF] Executed action Volo.Abp.Identity.IdentityUserController.GetListAsync (Volo.Abp.Identity.HttpApi) in 167.7765ms
  20. [01:02:56 INF] Executed endpoint 'Volo.Abp.Identity.IdentityUserController.GetListAsync (Volo.Abp.Identity.HttpApi)'
  21. [01:02:56 INF] Request finished HTTP/2 GET https://localhost:44324/api/identity/users - - - 302 0 - 268.2960ms
  22. [01:02:56 INF] Request starting HTTP/2 GET https://localhost:44324/Account/Login?ReturnUrl=%2Fapi%2Fidentity%2Fusers - -
  23. [01:02:56 INF] Executing endpoint '/Account/Login'

期望目标

访问API时,与Abp4.X行为一致。返回如下

  1. {
  2. "error": {
  3. "code": "Volo.Authorization:010001",
  4. "message": "Authorization failed! Given policy has not granted.",
  5. "details": null,
  6. "data": {},
  7. "validationErrors": null
  8. }
  9. }

如何解决

该问题在**Abp5.X**版本之前就存在(如果Abp5.X生成模板的时候,选择分离IdentityServer则只会返回401且无返回值。不分离的话会走IdentityServerCookie认证,就会导致重定向至登录页,比如在Abp4.4.4新建一个Controller,并添加[Authorize]

  1. [Route("api/test")]
  2. [Authorize]
  3. public class TestController : Test4Controller
  4. {
  5. [HttpGet]
  6. public Task TestAsync()
  7. {
  8. return Task.CompletedTask;
  9. }
  10. }

直接访问就会重定向至登录页。
但是,如果你是按照标准写法,通过Application.Contracts层创建接口,然后Controller层调用。

  1. [Authorize]
  2. public class NewTestAppService : Test4AppService, INewTestAppService
  3. {
  4. public Task GetTestAsync()
  5. {
  6. return Task.CompletedTask;
  7. }
  8. }
  1. [Route("api/new-test")]
  2. public class NewTestController : Test4Controller, INewTestAppService
  3. {
  4. private readonly INewTestAppService _newTestAppService;
  5. public NewTestController(INewTestAppService newTestAppService)
  6. {
  7. _newTestAppService = newTestAppService;
  8. }
  9. [HttpGet]
  10. public Task GetTestAsync()
  11. {
  12. return _newTestAppService.GetTestAsync();
  13. }
  14. }

则会返回标准异常Json。

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

在Abp5.X版本中,Abp官方将认证行为保持一致(Issues/9926),从而导致了之后版本,匿名访问需认证API将会重定向至登录页。这是一次非常好的改动。

第一种.Net Core传统解决方案。

  1. private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
  2. {
  3. context.Services.ConfigureApplicationCookie(options =>
  4. {
  5. options.ForwardDefaultSelector = ctx =>
  6. {
  7. return ctx.Request.Path.StartsWithSegments("/api") ? JwtBearerDefaults.AuthenticationScheme : null;
  8. };
  9. });
  10. context.Services.AddAuthentication().AddJwtBearer(options =>
  11. {
  12. options.Authority = configuration["AuthServer:Authority"];
  13. options.RequireHttpsMetadata = Convert.ToBoolean(configuration["AuthServer:RequireHttpsMetadata"]);
  14. options.Audience = "Test";
  15. options.BackchannelHttpHandler = new HttpClientHandler
  16. {
  17. ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
  18. };
  19. options.Events = new JwtBearerEvents
  20. {
  21. OnChallenge = async context =>
  22. {
  23. context.HandleResponse();
  24. context.Response.ContentType = "application/json;charset=utf-8";
  25. context.Response.StatusCode = StatusCodes.Status401Unauthorized;
  26. var response = new RemoteServiceErrorResponse(new RemoteServiceErrorInfo("未认证"));
  27. await context.Response.WriteAsJsonAsync(response);
  28. },
  29. OnForbidden = async context =>
  30. {
  31. context.Response.ContentType = "application/json;charset=utf-8";
  32. context.Response.StatusCode = StatusCodes.Status403Forbidden;
  33. var response = new RemoteServiceErrorResponse(new RemoteServiceErrorInfo("未授权"));
  34. await context.Response.WriteAsJsonAsync(response);
  35. }
  36. };
  37. });
  38. }

其中返回信息根据实际情况填写。

第二种方法是将行为尽量和Abp趋于一致。仅限**.NET 5.0/6.0**.
新建AuthorizationExceptionHandler类,继承IAbpAuthorizationExceptionHandler接口。

  1. public class AuthorizationExceptionHandler : IAbpAuthorizationExceptionHandler
  2. {
  3. private readonly Func<object, Task> _clearCacheHeadersDelegate;
  4. public AuthorizationExceptionHandler()
  5. {
  6. _clearCacheHeadersDelegate = ClearCacheHeaders;
  7. }
  8. public Task HandleAsync(AbpAuthorizationException exception, HttpContext httpContext)
  9. {
  10. return HandleAndWrapExceptionAsync(exception, httpContext);
  11. }
  12. protected virtual async Task HandleAndWrapExceptionAsync(AbpAuthorizationException exception, HttpContext httpContext)
  13. {
  14. var errorInfoConverter = httpContext.RequestServices.GetRequiredService<IExceptionToErrorInfoConverter>();
  15. var statusCodeFinder = httpContext.RequestServices.GetRequiredService<IHttpExceptionStatusCodeFinder>();
  16. httpContext.Response.Clear();
  17. httpContext.Response.StatusCode = (int)statusCodeFinder.GetStatusCode(httpContext, exception);
  18. httpContext.Response.OnStarting(_clearCacheHeadersDelegate, httpContext.Response);
  19. httpContext.Response.Headers.Add(AbpHttpConsts.AbpErrorFormat, "true");
  20. await httpContext.Response.WriteAsJsonAsync(
  21. new RemoteServiceErrorResponse(
  22. errorInfoConverter.Convert(exception)
  23. )
  24. );
  25. }
  26. private Task ClearCacheHeaders(object state)
  27. {
  28. var response = (HttpResponse)state;
  29. response.Headers[HeaderNames.CacheControl] = "no-cache";
  30. response.Headers[HeaderNames.Pragma] = "no-cache";
  31. response.Headers[HeaderNames.Expires] = "-1";
  32. response.Headers.Remove(HeaderNames.ETag);
  33. return Task.CompletedTask;
  34. }
  35. }

新建AuthorizationMiddlewareResultHandler类,继承IAuthorizationMiddlewareResultHandler接口。

  1. public class AuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
  2. {
  3. private readonly IAbpAuthorizationExceptionHandler _authorizationExceptionHandler;
  4. public AuthorizationMiddlewareResultHandler(IAbpAuthorizationExceptionHandler authorizationExceptionHandler)
  5. {
  6. _authorizationExceptionHandler = authorizationExceptionHandler;
  7. }
  8. public async Task HandleAsync(
  9. RequestDelegate next,
  10. HttpContext context,
  11. AuthorizationPolicy policy,
  12. PolicyAuthorizationResult authorizeResult)
  13. {
  14. if (authorizeResult.Challenged)
  15. {
  16. await context.ChallengeAsync();
  17. await _authorizationExceptionHandler.HandleAsync(
  18. new AbpAuthorizationException(code: AbpAuthorizationErrorCodes.GivenPolicyHasNotGranted), context);
  19. return;
  20. }
  21. if (authorizeResult.Forbidden)
  22. {
  23. await context.ForbidAsync();
  24. await _authorizationExceptionHandler.HandleAsync(
  25. new AbpAuthorizationException(code: AbpAuthorizationErrorCodes.GivenPolicyHasNotGranted), context);
  26. return;
  27. }
  28. await next(context);
  29. }
  30. }

将其注入到容器内。

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

别忘记将任何以**/api**开头的请求转发到 JWT 方案。

  1. private void ConfigureAuthentication(ServiceConfigurationContext context, IConfiguration configuration)
  2. {
  3. context.Services.ConfigureApplicationCookie(options =>
  4. {
  5. options.ForwardDefaultSelector = ctx =>
  6. {
  7. return ctx.Request.Path.StartsWithSegments("/api") ? JwtBearerDefaults.AuthenticationScheme : null;
  8. };
  9. });
  10. ...
  11. }