問題描述
異常日志
[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 中,依此類推。
代碼上的原因是:通過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;
};
});
...
}