问题一:
core2.2升级到3.1之后 2.2中策略授权使用context.Resource 在3.1版本中不再是AuthorizationFilterContext类型,而是Endpoint类型。不能再通过context.Resource来获取http请求头相关的数据。下边的代码在core3.1中已经无效
AuthorizationFilterContext filterContext = context.Resource as AuthorizationFilterContext; var httpcontent = filterContext.HttpContext;
新的方式应该是通过IHttpContextAccessor来获取http请求相关的数据。 首先在Startup.cs类中注入依赖:
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
然后在自定义授权策略中使用
var httpContext = _httpContextAccessor.HttpContext;
问题二:
core3.1中自定义授权失败后的返回内容引发组件内部异常问题。 在自定义授权策略中获取到httpContext之后很容易想到在授权失败后直接通过httpContext写入返回值,然后返回。代码类似如下:
await httpContext.Response.WriteAsync(JsonConvert.SerializeObject(new { code=0,msg="授权失败"})); context.Fail();
这么做确实可以在接口中获取到想要的返回值。但是,这么做会触发一个.net core组件内部的异常:
An unhandled exception was thrown by the application.|Microsoft.AspNetCore.Server.Kestrel System.InvalidOperationException: Headers are read-only, response has already started. at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.ThrowHeadersReadOnlyException() at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpHeaders.Microsoft.AspNetCore.Http.IHeaderDictionary.set_Item(String key, StringValues value) at Microsoft.AspNetCore.Mvc.Formatters.OutputFormatter.WriteResponseHeaders(OutputFormatterWriteContext context) at Microsoft.AspNetCore.Mvc.Formatters.TextOutputFormatter.WriteAsync(OutputFormatterWriteContext context) at Microsoft.AspNetCore.Mvc.Infrastructure.ObjectResultExecutor.ExecuteAsync(ActionContext context, ObjectResult result) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeNextResultFilterAsync[TFilter,TFilterAsync]() --- End of stack trace from previous location where exception was thrown --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResultExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeResultFilters() --- End of stack trace from previous location where exception was thrown --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|24_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state, Boolean isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted) at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync() --- End of stack trace from previous location where exception was thrown --- at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope) at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger) at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context) at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)|url:
这是因为在自定义授权处理中向http请求的Response写入数据导致的,在授权处理之后.net core组件会默认的再次向Response中写入数据,但是组件写入的时候Response中已经开始返回内容了,所以组件报了异常。 有时候在自定义授权中不是默认返回值,而是授权失败后直接重定向到新的接口中,如下
httpContext.Response.Redirect("/user/login");
在自定义授权中直接这么操作的话是无效的!
正确的方式应该是在Startup.cs中配置自定义策略授权的时候统一处理授权失败的返回内容。如下:
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddCookie(JwtBearerDefaults.AuthenticationScheme, b => { b.LoginPath = "/user/login"; b.Cookie.Name = "SessionId"; b.Cookie.Domain = ".liemei.net"; b.Cookie.Path = "/"; b.Cookie.HttpOnly = true; //b.Cookie.Expiration = TimeSpan.FromSeconds(elvaSettings.SessionTimeOut); //b.Cookie.Expiration = new TimeSpan(0, 0, elvaSettings.SessionTimeOut); b.Events = new Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents { OnRedirectToLogin= content => { content.Response.WriteAsync(JsonConvert.SerializeObject(new { code = 0, msg = "授权失败" })); return Task.CompletedTask; } }; });
如果在授权失败后需要重定向到一个新的接口中,那么就在b.LoginPath 后边设置要重定向的路由。 如果在重定向之后要返回统一的json内容,那就定义跳转登录的事件,在事件中将要返回的内容写入Response.
完整的代码如下:
Startup.cs
public void ConfigureServices(IServiceCollection services) { services.AddControllers(); services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); services.AddAuthorization(option=> { option.AddPolicy("auth1",policy=> { policy.Requirements.Add(new AdultPolicyRequirement(12)); }); }); services.AddSingleton<IAuthorizationHandler, AdultAuthorizationHandler>(); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddCookie(JwtBearerDefaults.AuthenticationScheme, b => { b.LoginPath = "/user/login"; b.Cookie.Name = "SessionId"; b.Cookie.Domain = ".liemei.net"; b.Cookie.Path = "/"; b.Cookie.HttpOnly = true; //b.Cookie.Expiration = TimeSpan.FromSeconds(elvaSettings.SessionTimeOut); //b.Cookie.Expiration = new TimeSpan(0, 0, elvaSettings.SessionTimeOut); b.Events = new Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationEvents { OnRedirectToLogin= content => { content.Response.WriteAsync(JsonConvert.SerializeObject(new { code = 0, msg = "授权失败" })); return Task.CompletedTask; } }; }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseAuthentication(); app.UseRouting(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); }); }
自定义策略授权:
public class AdultPolicyRequirement: IAuthorizationRequirement { public int Age { get; set; } public AdultPolicyRequirement(int age) { this.Age = age; } } public class AdultAuthorizationHandler : AuthorizationHandler<AdultPolicyRequirement> { private readonly IHttpContextAccessor _httpContextAccessor; public AdultAuthorizationHandler(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, AdultPolicyRequirement requirement) { var httpContext = _httpContextAccessor.HttpContext; var request = httpContext.Request; int age = 0; if (request.Query.Keys.Contains("age")) { age = Convert.ToInt32(request.Query["age"]); } if (age > requirement.Age) { //通过验证,这句代码必须要有 context.Succeed(requirement); } else { if (context.Resource is Endpoint endpoint) { } httpContext.Response.Redirect("/user/login"); //await httpContext.Response.WriteAsync(JsonConvert.SerializeObject(new { code=1,msg="ok"})); //await httpContext.Response.Body.FlushAsync(); context.Fail(); } } }
控制器中:
[ApiController] [Route("[controller]")] [Authorize("auth1")] public class WeatherForecastController : ControllerBase { private static readonly string[] Summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; private readonly ILogger<WeatherForecastController> _logger; public WeatherForecastController(ILogger<WeatherForecastController> logger) { _logger = logger; } [HttpGet] public IEnumerable<WeatherForecast> Get() { var rng = new Random(); return Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = DateTime.Now.AddDays(index), TemperatureC = rng.Next(-20, 55), Summary = Summaries[rng.Next(Summaries.Length)] }) .ToArray(); } }