.NET Core 8外部登录后IsAuthenticated需刷新才为true问题排查
我在.NET Core 8 Web应用中实现了外部登录功能,遇到以下异常情况:
- 回调方法执行完成后,通过
LocalRedirect(returnUrl)跳转到受保护页面时,目标页面检测到User.Identity.IsAuthenticated为false,因此被重定向回登录页; - 但直接在浏览器地址栏输入受保护页面的URL时,该属性会变为
true,页面可正常显示。
注:外部认证完成后,info对象的IsAuthenticated属性为true,回调方法内的User.Identity.IsAuthenticated也为true,仅跳转后的目标页面该属性为false,自定义AuthorizeAttribute中也检测到该属性为false。
回调方法代码
[AllowAnonymous] [HttpGet("ExternalLoginCallback")] public async Task<IActionResult> ExternalLoginCallback(string? returnUrl = null, string? remoteError = null) { returnUrl ??= Url.Content("~/"); LoginViewModel loginViewModel = new () { ReturnUrl = returnUrl, ExternalLogins = (await signInManager.GetExternalAuthenticationSchemesAsync()).ToList() }; if (remoteError != null) { ModelState .AddModelError(string.Empty, $"Error desde el proveedor externo: {remoteError}"); loginViewModel.SetError(remoteError); return View("Login", loginViewModel); } // Get the login information about the user from the external login provider var info = await signInManager.GetExternalLoginInfoAsync(); if (info == null) { ModelState .AddModelError(string.Empty, "Error al cargar la información del login externo."); loginViewModel.SetError("Error al cargar la información del login externo."); return View("Login", loginViewModel); } // If the user already has a login (i.e if there is a record in AspNetUserLogins // table) then sign-in the user with this external login provider var signInResult = await signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false, bypassTwoFactor: true); var userName = info.Principal.FindFirstValue(ClaimTypes.Email); if (signInResult.Succeeded) { if (userName != null) { await SetLastLoggedOnTime(userName); var user = await userManager.FindByNameAsync(userName); if (user != null) await userManager.UpdateSecurityStampAsync(user); } return LocalRedirect(returnUrl); } else { // Verifica que el usuario exista. Si existe, agrega el login y autentica var creado = userName == null ? null : await usuarioService.CreateUserFromExternal(userName, info); if (creado != null) { await userManager.AddLoginAsync(creado, info); await signInManager.SignInAsync(creado, isPersistent: false); if (userName != null) await SetLastLoggedOnTime(userName); return LocalRedirect(returnUrl); } else { loginViewModel.Notification = new Integration.ViewModels.MessageViewModel { Type = Integration.ViewModels.AlertTypes.Danger }; if (signInResult.IsLockedOut) loginViewModel.SetError("El usuario está bloqueado."); else if (signInResult.IsNotAllowed) loginViewModel.SetError("El usuario no está autorizado para ingresar."); else loginViewModel.SetError("El usuario no está registrado en la plataforma."); } } return View("Login", loginViewModel); }
认证配置代码
var ab = services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddJwtBearer(x => { x.RequireHttpsMetadata = true; x.SaveToken = true; x.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidIssuer = configuration["Modules:Authenticate:AuthJwt:Issuer"], ValidateAudience = true, ValidAudience = configuration["Modules:Authenticate:AuthJwt:Audience"], ValidateIssuerSigningKey = true, RequireExpirationTime = false, ValidateLifetime = true, ClockSkew = TimeSpan.Zero, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(configuration["Modules:Authenticate:AuthJwt:Key"] ?? string.Empty)) }; }); string googleClientId = configuration["Modules:Authenticate:Google:ClientId"] ?? string.Empty; if (googleClientId != string.Empty) ab.AddGoogle(googleOptions => { googleOptions.ClientId = googleClientId; googleOptions.ClientSecret = configuration["Modules:Authenticate:Google:ClientSecret"] ?? string.Empty; googleOptions.CallbackPath = "/Security/GoogleSignIn"; }); string facebookAppId = configuration["Modules:Authenticate:Facebook:AppId"] ?? string.Empty; if (facebookAppId != string.Empty) ab.AddFacebook(facebookOptions => { facebookOptions.AppId = configuration["Modules:Authenticate:Facebook:AppId"] ?? string.Empty; facebookOptions.AppSecret = configuration["Modules:Authenticate:Facebook:AppSecret"] ?? string.Empty; facebookOptions.CallbackPath = "/Security/FacebookSignIn"; }); services.ConfigureApplicationCookie(options => { options.Cookie.Name = "MyCore"; options.Cookie.Path = "/"; options.LoginPath = "/Authenticate/Security/Login"; options.LogoutPath = "/Authenticate/Security/Logout"; options.AccessDeniedPath = "/Authenticate/Security/AccessDenied"; options.ReturnUrlParameter = "ReturnUrl"; options.Cookie.HttpOnly = true; options.ExpireTimeSpan = TimeSpan.FromMinutes(30); options.SlidingExpiration = true; options.Cookie.SameSite = SameSiteMode.Strict; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Events = new CookieAuthenticationEvents { OnRedirectToLogin = (context) => { // Hice esto para que no considere el módulo en el que se encuentra el usuario Uri oldUri = new(context.RedirectUri, UriKind.RelativeOrAbsolute); Uri newUri = new($"{options.LoginPath}{oldUri.Query}", UriKind.Relative); context.HttpContext.Response.Redirect(newUri.ToString()); return Task.CompletedTask; } }; });
这个问题的核心是认证Cookie在重定向流程中未被浏览器正确携带,具体可能的触发点如下:
SameSite Cookie配置过严
你设置了SameSiteMode.Strict,该模式下Cookie仅会在同一站点的请求中被携带。外部登录回调是跨站点跳转(从第三方登录服务商回到你的应用),浏览器可能不会在后续的重定向请求中携带Cookie。建议改为SameSiteMode.Lax,这是.NET Core的默认配置,允许跨站点GET请求携带Cookie,适配外部登录的场景。SecurePolicy与环境不匹配
CookieSecurePolicy.Always要求Cookie只能通过HTTPS传输。如果你的开发环境使用HTTP而非HTTPS,浏览器会拒绝保存Cookie,导致跳转后的请求无法识别认证状态。开发环境可临时改为CookieSecurePolicy.SameAsRequest,生产环境再恢复为Always。SecurityStamp更新导致Cookie失效
在登录成功后你调用了UpdateSecurityStampAsync,这会更新用户的安全戳,而认证Cookie是绑定安全戳的。如果更新后没有重新生成认证Cookie,跳转后的请求会因安全戳不匹配被判定为未认证。建议在更新安全戳后重新调用signInManager.SignInAsync生成新的Cookie。重定向响应的Cookie提交时机
LocalRedirect返回302重定向响应时,部分浏览器可能未及时保存认证Cookie,导致跳转后的请求未携带Cookie。可以尝试替换LocalRedirect为RedirectToAction,或者在重定向前确保Cookie已被写入响应。
内容的提问来源于stack exchange,提问作者jstuardo

