You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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在重定向流程中未被浏览器正确携带,具体可能的触发点如下:

  1. SameSite Cookie配置过严
    你设置了SameSiteMode.Strict,该模式下Cookie仅会在同一站点的请求中被携带。外部登录回调是跨站点跳转(从第三方登录服务商回到你的应用),浏览器可能不会在后续的重定向请求中携带Cookie。建议改为SameSiteMode.Lax,这是.NET Core的默认配置,允许跨站点GET请求携带Cookie,适配外部登录的场景。

  2. SecurePolicy与环境不匹配
    CookieSecurePolicy.Always要求Cookie只能通过HTTPS传输。如果你的开发环境使用HTTP而非HTTPS,浏览器会拒绝保存Cookie,导致跳转后的请求无法识别认证状态。开发环境可临时改为CookieSecurePolicy.SameAsRequest,生产环境再恢复为Always。

  3. SecurityStamp更新导致Cookie失效
    在登录成功后你调用了UpdateSecurityStampAsync,这会更新用户的安全戳,而认证Cookie是绑定安全戳的。如果更新后没有重新生成认证Cookie,跳转后的请求会因安全戳不匹配被判定为未认证。建议在更新安全戳后重新调用signInManager.SignInAsync生成新的Cookie。

  4. 重定向响应的Cookie提交时机
    LocalRedirect返回302重定向响应时,部分浏览器可能未及时保存认证Cookie,导致跳转后的请求未携带Cookie。可以尝试替换LocalRedirect为RedirectToAction,或者在重定向前确保Cookie已被写入响应。

内容的提问来源于stack exchange,提问作者jstuardo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 05:29:53