問題一:
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(); } }