Abp授权失败重定向至登录页,修改为返回401
问题描述
异常日志
[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;
};
});
...
}
本文作者:hiwwwk
本文链接:https://www.cnblogs.com/wwwk/p/16037364.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步