Tengo una situación extraña que no puedo duplicar consistentemente. Tengo un sitio web MVC desarrollado en .NET Core 3.0 y autoriza a los usuarios con .NET Core Identity. Cuando ejecuto el sitio en un entorno de desarrollo localmente, todo funciona bien (el clásico "¡funciona en mi máquina!") . Cuando lo implemento en mi servidor web provisional es cuando empiezo a ver el problema. Los usuarios pueden iniciar sesión correctamente, ser autenticados y redirigidos a la página de inicio. Nota: todos los controladores, excepto el que maneja la autenticación, están decorados con el atributo [Authorize] y el atributo [AutoValidateAntiforgeryToken] . La página de inicio carga bien. Sin embargo, hay un par de llamadas ajax que se ejecutan cuando se carga la página que devuelven la llamada al controlador de inicio para cargar algunos datos condicionales y verificar si algunas variables de nivel de sesión ya se han configurado. Estas llamadas ajax devuelven un 401 no autorizado . El problema es que no puedo lograr que este comportamiento se repita consistentemente. De hecho, otro usuario inició sesión al mismo tiempo (misma aplicación, mismo servidor) y funcionó bien para ellos. Abrí la consola del desarrollador en Chrome y rastreé lo que creo que es el problema hasta un factor común (o poco común). Las llamadas (como cargar la página de inicio o las llamadas ajax que fueron exitosas para el otro usuario) que funcionan tienen ".AspNetCore.Antiforgery", ".AspNetCore.Identity.Application" y ".AspNetCore.Session" cookies establecidas en los encabezados de solicitud. Las llamadas que no funcionan (mis llamadas ajax) solo tienen el conjunto de cookies ".AspNetCore.Session". Otra cosa a tener en cuenta es que este comportamiento ocurre para cada llamada ajax en el sitio. Todas las llamadas realizadas a las acciones del controlador mediante la navegación o la publicación de formularios funcionan bien.
Lo que es extraño para mí es que otro usuario puede iniciar sesión, e incluso yo puedo iniciar sesión ocasionalmente después de una nueva publicación, y hacer que esas llamadas ajax funcionen bien con las cookies configuradas correctamente.
Aquí hay parte del código para ser un poco más específico. No estoy seguro si es algo que configuré mal con la configuración de Identidad o Sesión.
Inicio.cs
public class Startup { public Startup(IConfiguration configuration) { Configuration = configuration; } public IConfiguration Configuration { get; } public IWebHostEnvironment Env { get; set; } // This method gets called by the runtime. Use this method to add services to the container. public void ConfigureServices(IServiceCollection services) { services.AddIdentity<User, UserRole>(options => { options.User.RequireUniqueEmail = true; }).AddEntityFrameworkStores<QCAuthorizationContext>() .AddDefaultTokenProviders(); ; services.AddDbContext<QCAuthorizationContext>(cfg => { cfg.UseSqlServer(Configuration.GetConnectionString("Authorization")); }); services.AddSingleton<IConfiguration>(Configuration); services.AddControllersWithViews(); services.AddDistributedMemoryCache(); services.AddSession(options => { // Set a short timeout for easy testing. options.IdleTimeout = TimeSpan.FromHours(4); options.Cookie.HttpOnly = true; // Make the session cookie essential options.Cookie.IsEssential = true; }); services.Configure<IdentityOptions>(options => { options.Password.RequireDigit = true; options.Password.RequireLowercase = true; options.Password.RequireNonAlphanumeric = true; options.Password.RequireUppercase = true; options.Password.RequiredLength = 6; options.Password.RequiredUniqueChars = 1; // Lockout settings options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(30); options.Lockout.MaxFailedAccessAttempts = 10; options.Lockout.AllowedForNewUsers = true; }); services.ConfigureApplicationCookie(options => { //cookie settings options.ExpireTimeSpan = TimeSpan.FromHours(4); options.SlidingExpiration = true; options.LoginPath = new Microsoft.AspNetCore.Http.PathString("/Account/Login"); }); services.AddHttpContextAccessor(); //services.TryAddSingleton<IActionContextAccessor, ActionContextAccessor>(); IMvcBuilder builder = services.AddRazorPages(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider serviceProvider) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); app.UseHsts(); } app.UseStaticFiles(); app.UseCookiePolicy(); app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.UseSession(); app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); endpoints.MapControllerRoute( name: "auth4", pattern: "{controller=Account}/{action=Authenticate}/{id?}"); }); } }Acción del controlador de inicio de sesión
[HttpPost] public async Task<IActionResult> Login(LoginViewModel iViewModel) { ViewBag.Message = ""; try { var result = await signInManager.PasswordSignInAsync(iViewModel.Email, iViewModel.Password, false, false); if (result.Succeeded) { var user = await userManager.FindByNameAsync(iViewModel.Email); if (!user.FirstTimeSetupComplete) { return RedirectToAction("FirstLogin"); } return RedirectToAction("Index", "Home"); } else { ViewBag.Message = "Login Failed."; } } catch (Exception ex) { ViewBag.Message = "Login Failed."; } return View(new LoginViewModel() { Email = iViewModel.Email }); }Controlador de casa
public class HomeController : BaseController { private readonly ILogger<HomeController> _logger; public HomeController(IConfiguration configuration, ILogger<HomeController> logger, UserManager<User> iUserManager) : base(configuration, iUserManager) { _logger = logger; } public async Task<IActionResult> Index() { HomeViewModel vm = HomeService.GetHomeViewModel(); vm.CurrentProject = HttpContext.Session.GetString("CurrentProject"); vm.CurrentInstallation = HttpContext.Session.GetString("CurrentInstallation"); if (!string.IsNullOrEmpty(vm.CurrentProject) && !string.IsNullOrEmpty(vm.CurrentInstallation)) { vm.ProjectAndInstallationSet = true; } return View(vm); } public IActionResult CheckSessionVariablesSet() { var currentProject = HttpContext.Session.GetString("CurrentProject"); var currentInstallation = HttpContext.Session.GetString("CurrentInstallation"); return Json(!string.IsNullOrEmpty(currentProject) && !string.IsNullOrEmpty(currentInstallation)); } public IActionResult CheckSidebar() { try { var sidebarHidden = bool.Parse(HttpContext.Session.GetString("SidebarHidden")); return Json(new { Success = sidebarHidden }); } catch (Exception ex) { return Json(new { Success = false }); } } }Controlador básico
[AutoValidateAntiforgeryToken] [Authorize] public class BaseController : Controller { protected IConfiguration configurationManager; protected SQLDBContext context; protected UserManager<User> userManager; public BaseController(IConfiguration configuration, UserManager<User> iUserManager) { userManager = iUserManager; configurationManager = configuration; } public BaseController(IConfiguration configuration) { configurationManager = configuration; } protected void EnsureDBConnection(string iProject) { switch (iProject) { case "A": DbContextOptionsBuilder<SQLDBContext> AOptionsBuilder = new DbContextOptionsBuilder<SQLDBContext>(); AOptionsBuilder.UseLazyLoadingProxies().UseSqlServer(configurationManager.GetConnectionString("A")); context = new SQLDBContext(AOptionsBuilder.Options); break; case "B": DbContextOptionsBuilder<SQLDBContext> BOptionsBuilder = new DbContextOptionsBuilder<SQLDBContext>(); BOptionsBuilder.UseLazyLoadingProxies().UseSqlServer(configurationManager.GetConnectionString("B")); context = new SQLDBContext(BOptionsBuilder.Options); break; case "C": DbContextOptionsBuilder<SQLDBContext> COptionsBuilder = new DbContextOptionsBuilder<SQLDBContext>(); COptionsBuilder.UseLazyLoadingProxies().UseSqlServer(configurationManager.GetConnectionString("C")); context = new SQLDBContext(COptionsBuilder.Options); break; } } }_Layout.cshtml Javascript (ejecuta las llamadas ajax antes mencionadas cuando se cargan las páginas)
<script type="text/javascript"> var afvToken; $(function () { afvToken = $("input[name='__RequestVerificationToken']").val(); $.ajax({ url: VirtualDirectory + '/Home/CheckSidebar', headers: { "RequestVerificationToken": afvToken }, complete: function (data) { console.log(data); if (data.responseJSON.success) { toggleSidebar(); } } }); $.ajax({ url: VirtualDirectory + '/Home/CheckSessionVariablesSet', headers: { "RequestVerificationToken": afvToken }, complete: function (data) { console.log(data); if (data.responseJSON) { $('#sideBarContent').attr('style', ''); } else { $('#sideBarContent').attr('style', 'display:none;'); } } }); $.ajax({ url: VirtualDirectory + '/Account/UserRoles', headers: { "RequestVerificationToken": afvToken }, complete: function (data) { if (data.responseJSON) { var levels = data.responseJSON; if (levels.includes('Admin')) { $('.adminSection').attr('style', ''); } else { $('.adminSection').attr('style', 'display:none;'); } } } }); }); </script>EDITAR:
Lo que encontré es el encabezado "Cookie" con ".AspNetCore.Antiforgery", ".AspNetCore.Identity.Application" y los atributos ".AspNetCore.Session" siempre están configurados correctamente en las solicitudes ajax cuando se ejecutan localmente. Cuando se implementa, solo establece la cookie con el atributo de sesión. Encontré una configuración que tengo en mi Startup.cs que establece la cookie en HttpOnly: options.Cookie.HttpOnly = true; ¿Podría esto estar causando mi problema? ¿Configurarlo en falso funcionaría? Si eso no es seguro, ¿cuáles son algunas soluciones/métodos alternativos para mi enfoque? Todavía necesito implementar el principio básico de autenticación de usuario Y poder activar solicitudes ajax.
OTRA EDICIÓN:
Hoy, después de implementar el sitio nuevamente, lo ejecuté simultáneamente en Firefox y Chrome. Firefox envió la cookie correcta después de la autenticación y funciona bien. Sin embargo, Chrome sigue mostrando el comportamiento 401.
¡Me parece que su problema podría deberse a un comportamiento diferente de las cookies en escenarios http vs https!
Las cookies seguras que se configuran en modo https no se pueden recuperar cuando se vuelven a publicar en http .
Vea esto para más información.
También vi esta parte en su Inicio, lo que aumenta la posibilidad de adivinar:
if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } else { app.UseExceptionHandler("/Error"); app.UseHsts(); } En su entorno de desarrollo, todo funciona bien en http . Pero en el entorno de implementación, https entra y si algunas solicitudes van a http y otras van a https , algunas cookies no regresan y puede enfrentar este problema.
Como descubrió, esta es la diferencia de llamada ajax en varios navegadores . La programación del lado del servidor funciona bien y no puede tener una respuesta casual a menos que enfrente diferentes solicitudes que provienen de un navegador (aquí google chome). Creo que usar una afirmación en una llamada ajax debería resolver el problema como emplear withcredentials : true . Déjame saber si el problema persiste o no.
Esto parece un problema de administración de sesión, uso de services.AddDistributedMemoryCache() a veces genera problemas de sesión, especialmente en un entorno de alojamiento compartido. ¿Podría intentar almacenar en caché en un db.
p.ej
services.AddDistributedSqlServerCache(options => { options.ConnectionString = connectionString; options.SchemaName = "dbo"; options.TableName = "DistributedCache"; }); Asegúrese de manejar los problemas de GDPR , que afectan la cookie de sesión de .Net core> 2.0. Estos surgieron para ayudar a los desarrolladores a cumplir con las regulaciones de GDPR.
Por ejemplo, en su aplicación, como una de las opciones disponibles, puede hacer que la cookie de sesión sea esencial, para permitir que se escriba incluso antes de que el usuario acepte los términos de la cookie, es decir
services.AddSession(options => { options.Cookie.IsEssential = true; // make the session cookie Essential });