从ASP.NET Core 1.1 MVC迁移到2.0后,自定义cookie身份validation无法正常工作

我已经将ASP.NET Core 1.1 MVC项目迁移到ASP.NET Core 2.0,现在我注意到对应用程序的未授权部分的请求不再导致“401 Unauthorized”响应,而是导致响应的代码exception“ 500内部服务器错误”。

日志文件的示例摘录(John Smith无权访问他尝试访问的控制器操作):

2018-01-02 19:58:23 [DBG] Request successfully matched the route with name '"modules"' and template '"m/{ModuleName}"'. 2018-01-02 19:58:23 [DBG] Executing action "Team.Controllers.ModulesController.Index (Team)" 2018-01-02 19:58:23 [INF] Authorization failed for user: "John Smith". 2018-01-02 19:58:23 [INF] Authorization failed for the request at filter '"Microsoft.AspNetCore.Mvc.Authorization.AuthorizeFilter"'. 2018-01-02 19:58:23 [INF] Executing ForbidResult with authentication schemes ([]). 2018-01-02 19:58:23 [INF] Executed action "Team.Controllers.ModulesController.Index (Team)" in 146.1146ms 2018-01-02 19:58:23 [DBG] System.InvalidOperationException occurred, checking if Entity Framework recorded this exception as resulting from a failed database operation. 2018-01-02 19:58:23 [DBG] Entity Framework did not record any exceptions due to failed database operations. This means the current exception is not a failed Entity Framework database operation, or the current exception occurred from a DbContext that was not obtained from request services. 2018-01-02 19:58:23 [ERR] An unhandled exception has occurred while executing the request System.InvalidOperationException: No authenticationScheme was specified, and there was no DefaultForbidScheme found. at Microsoft.AspNetCore.Authentication.AuthenticationService.d__12.MoveNext() ... 

我使用自定义cookie身份validation,作为中间件实现。 这是我的Startup.cs(app.UseTeamAuthentication()是对中间件的调用):

 public class Startup { public Startup(IHostingEnvironment env) { var builder = new ConfigurationBuilder() .SetBasePath(env.ContentRootPath) .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true); builder.AddEnvironmentVariables(); Configuration = builder.Build(); } public IConfigurationRoot Configuration { get; } public void ConfigureServices(IServiceCollection services) { services.Configure(Configuration); services.AddSingleton(); services.AddDbContext(options => options .ConfigureWarnings(warnings => warnings.Throw(CoreEventId.IncludeIgnoredWarning)) .ConfigureWarnings(warnings => warnings.Throw(RelationalEventId.QueryClientEvaluationWarning))); services.AddAuthorization(options => { options.AddPolicy(Security.TeamAdmin, policyBuilder => policyBuilder.RequireClaim(ClaimTypes.Role, Security.TeamAdmin)); options.AddPolicy(Security.SuperAdmin, policyBuilder => policyBuilder.RequireClaim(ClaimTypes.Role, Security.SuperAdmin)); }); services.AddDistributedMemoryCache(); services.AddSession(options => { options.IdleTimeout = System.TimeSpan.FromMinutes(5); options.Cookie.HttpOnly = true; }); services.AddMvc() .AddJsonOptions(options => options.SerializerSettings.ContractResolver = new DefaultContractResolver()) .AddViewLocalization( LanguageViewLocationExpanderFormat.SubFolder, options => { options.ResourcesPath = "Resources"; }) .AddDataAnnotationsLocalization(); services.Configure(options => { options.DefaultRequestCulture = new RequestCulture("en-US"); options.SupportedCultures = TeamConfig.SupportedCultures; options.SupportedUICultures = TeamConfig.SupportedCultures; options.RequestCultureProviders.Insert(0, new MyCultureProvider(options.DefaultRequestCulture)); }); services.AddScoped(); } public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) { loggerFactory.AddConsole(Configuration.GetSection("Logging")); loggerFactory.AddDebug(); Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.File("log.txt", outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss} [{Level:u3}] {Message}{NewLine}{Exception}") .CreateLogger(); loggerFactory.AddSerilog(); if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); app.UseDatabaseErrorPage(); } bool UseHttps = Configuration.GetValue("Https", false); if (UseHttps) { app.UseRewriter(new RewriteOptions().AddRedirectToHttps()); } app.UseStaticFiles(); app.UseTeamDatabaseSelector(); app.UseTeamAuthentication(); var localizationOptions = app.ApplicationServices.GetService<IOptions>(); app.UseRequestLocalization(localizationOptions.Value); app.UseSession(); app.UseMvc(routes => { routes.MapRoute( name: "modules", template: "m/{ModuleName}", defaults: new { controller = "Modules", action = "Index" } ); routes.MapRoute( name: "actions", template: "a/{action}", defaults: new { controller = "Actions" } ); routes.MapRoute( name: "modules_ex", template: "mex/{action}", defaults: new { controller = "ModulesEx" } ); routes.MapRoute( name: "default", template: "{controller=Home}/{action=Index}/{id?}"); }); } } 

这是中间件:

 public class TeamAuthentication { private readonly RequestDelegate next; private readonly ILogger logger; public TeamAuthentication(RequestDelegate _next, ILogger _logger) { next = _next; logger = _logger; } public async Task Invoke(HttpContext context, ApplicationDbContext db) { if (TeamConfig.AuthDebug) { logger.LogDebug("Auth-Invoke: " + context.Request.Path); } const string LoginPath = "/Login"; const string LoginPathTimeout = "/Login?timeout"; const string LogoutPath = "/Logout"; bool Login = (context.Request.Path == LoginPath || context.Request.Path == LoginPathTimeout); bool Logout = (context.Request.Path == LogoutPath); string TokenContent = context.Request.Cookies["t"]; bool DatabaseSelected = context.Items["ConnectionString"] != null; bool Authenticated = false; bool SessionTimeout = false; // provjera tokena if (!Login && !Logout && DatabaseSelected && TokenContent != null) { try { var token = await Security.CheckToken(db, logger, TokenContent, context.Response); if (token.Status == Models.TokenStatus.OK) { Authenticated = true; context.Items["UserID"] = token.UserID; List userClaims = new List(); var person = await db.Person.AsNoTracking() .Where(x => x.UserID == token.UserID) .FirstOrDefaultAsync(); if (person != null) { var emp = await db.Employee.AsNoTracking() .Where(x => x.PersonID == person.ID) .FirstOrDefaultAsync(); if (emp != null) { context.Items["EmployeeID"] = emp.ID; } } string UserName = ""; if (person != null && person.FullName != null) { UserName = person.FullName; } else { var user = await db.User.AsNoTracking() .Where(x => x.ID == token.UserID) .Select(x => new { x.Login }).FirstOrDefaultAsync(); UserName = user.Login; } context.Items["UserName"] = UserName; userClaims.Add(new Claim(ClaimTypes.Name, UserName)); if ((token.Roles & (int)Security.TeamRoles.TeamAdmin) == (int)Security.TeamRoles.TeamAdmin) { userClaims.Add(new Claim(ClaimTypes.Role, Security.TeamAdmin)); } if ((token.Roles & (int)Security.TeamRoles.SuperAdmin) == (int)Security.TeamRoles.SuperAdmin) { userClaims.Add(new Claim(ClaimTypes.Role, Security.TeamAdmin)); userClaims.Add(new Claim(ClaimTypes.Role, Security.SuperAdmin)); } ClaimsPrincipal principal = new ClaimsPrincipal(new ClaimsIdentity(userClaims, "local")); context.User = principal; } else if (token.Status == Models.TokenStatus.Expired) { SessionTimeout = true; } } catch (System.Exception ex) { logger.LogCritical(ex.Message); } } if (Login || (Logout && DatabaseSelected) || Authenticated) { await next.Invoke(context); } else { if (Utility.IsAjaxRequest(context.Request)) { if (TeamConfig.AuthDebug) { logger.LogDebug("Auth-Invoke => AJAX 401"); } context.Response.StatusCode = 401; context.Response.Headers.Add(SessionTimeout ? "X-Team-Timeout" : "X-Team-Login", "1"); } else { string RedirectPath = SessionTimeout ? LoginPathTimeout : LoginPath; if (TeamConfig.AuthDebug) { logger.LogDebug("Auth-Invoke => " + RedirectPath); } context.Response.Redirect(RedirectPath); } } } } } 

这是相同的中间件,我认为代码对于被剥离的问题并不重要:

 public class TeamAuthentication { private readonly RequestDelegate next; private readonly ILogger logger; public async Task Invoke(HttpContext context, ApplicationDbContext db) { // preparatory actions... var token = await Security.CheckToken(db, logger, TokenContent, context.Response); if (token.Status == Models.TokenStatus.OK) { List userClaims = new List(); string UserName = ""; // find out the UserName... userClaims.Add(new Claim(ClaimTypes.Name, UserName)); if ((token.Roles & (int)Security.TeamRoles.TeamAdmin) == (int)Security.TeamRoles.TeamAdmin) { userClaims.Add(new Claim(ClaimTypes.Role, Security.TeamAdmin)); } if ((token.Roles & (int)Security.TeamRoles.SuperAdmin) == (int)Security.TeamRoles.SuperAdmin) { userClaims.Add(new Claim(ClaimTypes.Role, Security.TeamAdmin)); userClaims.Add(new Claim(ClaimTypes.Role, Security.SuperAdmin)); } ClaimsPrincipal principal = new ClaimsPrincipal(new ClaimsIdentity(userClaims, "local")); } // ... 

这是我授权访问控制器的方式:

 namespace Team.Controllers { [Authorize(Policy = Security.TeamAdmin)] public class ModulesController : Controller { // ... 

我试图通过Google-ing研究这个问题并找到像https://docs.microsoft.com/en-us/aspnet/core/migration/1x-to-2x/identity-2x这样的文章和一些相似的文章,但他们没有帮我解决这个问题。

恕我直言,您可能希望切换到内置的角色基本授权,而不是滚动您自己的自定义策略授权 ,必然会有您没有想到的情况由它处理(避免重新发明轮子:)。

对于身份validation,您应该使用设置cookie身份validation方案

 services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie(); 

阅读有关此处提供的设置,了解没有ASP.Net Identity的自定义方案。

至于授权,你在这里混合了身份validation和授权,中间件做了两个但是命名为UseTeamAuthentication , 这里解释了差异,因此这两个东西在ASP.Net Core基础结构中是分开的。

您已完成授权(自定义)需要通过IAuthorizationRequirement接口实现需求来完成,您可以在上面的自定义策略链接中阅读如何执行此操作。 但我强烈建议您使用内置的Roles机制。

干杯:)