问题描述
异常日志
[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且无返回值。不分离的话会走IdentityServer的Cookie认证,就会导致重定向至登录页),比如在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 中,依此类推。
在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;};});...}
