一段时间后,身份验证 cookie 在反应 SPA 中消失

Posted

技术标签:

【中文标题】一段时间后,身份验证 cookie 在反应 SPA 中消失【英文标题】:Authentication cookie disappears in react SPA after some time 【发布时间】:2020-10-12 04:05:06 【问题描述】:

我们有一个 SPA,用 React 和 ASP.net 核心一起编写,用于托管。

为了验证应用程序,我们使用 IdentityServer4 并使用 cookie。客户端根据示例配置,这里描述:https://github.com/IdentityServer/IdentityServer4/tree/main/samples/Quickstarts/4_javascriptClient/src

对于验证用户,一切正常。它将被重定向到登录页面。登录后,重定向到 SPA 完成。身份验证 cookie 按预期设置:

HttpOnly = 真 安全 = 真 SameSite = 无 过期/最长使用期限 = 登录后一周

出于身份验证的原因,该 cookie 也用于其他 MVC(.net 核心和 MVC 5)应用程序。 在 SPA 中,我们也在使用 SignalR,它需要 cookie 进行身份验证。

我们的问题:

在浏览器中闲置约 30 分钟并进行刷新或导航后,身份验证 cookie(以及仅此而已,其他剩余部分)会自动从浏览器中消失。然后用户必须再次登录。为什么这会与 SPA 一起发生?

代码

完整代码见github

客户

UserService.ts 的片段

const openIdConnectConfig: UserManagerSettings = 
  authority: baseUrls.person,
  client_id: "js",
  redirect_uri: joinUrl(baseUrls.spa, "signincallback"),
  response_type: "code",
  scope: "openid offline_access profile Person.Api Translation.Api",
  post_logout_redirect_uri: baseUrls.spa,
  automaticSilentRenew: true
;

export const getUserService = asFactory(() => 
  const userManager = new UserManager(openIdConnectConfig);
  return createInstance(createStateHandler(defaultUserState), userManager, createSignInProcess(userManager));
, sameInstancePerSameArguments());

服务器

截取Startup.cs

public void ConfigureServices(IServiceCollection services)

    Log.Information($"Start configuring services. Environment: _environment.EnvironmentName");
    services.AddControllersWithViews();

    services.AddIdentity<LoginInputModel, RoleDto>()
            .AddDefaultTokenProviders();

    var certificate = LoadSigningCertificate();
    var identityServerBuilder = services.AddIdentityServer(options =>
                                                           
                                                               options.Events.RaiseErrorEvents = true;
                                                               options.Events.RaiseInformationEvents = true;
                                                               options.Events.RaiseFailureEvents = true;
                                                               options.Events.RaiseSuccessEvents = true;
                                                           )
                                        .AddSigningCredential(certificate)
                                        .AddProfileService<ProfileService>()
                                        .AddInMemoryIdentityResources(Config.Ids)
                                        .AddInMemoryApiResources(Config.Apis)
                                        .AddInMemoryClients(new ClientConfigLoader().LoadClients(Configuration));

    if (_environment.IsDevelopment())
    
        identityServerBuilder.AddDeveloperSigningCredential();
    

    services.Configure<CookiePolicyOptions>(options =>
                                            
                                                options.CheckConsentNeeded = context => false;
                                                options.MinimumSameSitePolicy = SameSiteMode.None;
                                            );

    services.AddDataProtection()
            .PersistKeysToFileSystem(new DirectoryInfo(_sharedAuthTicketKeys))
            .SetApplicationName("SharedCookieApp");

    services.AddAsposeMailLicense(Configuration);
    var optionalStartupSettings = SetupStartupSettings();
    if (optionalStartupSettings.IsSome)
    
        var settings = optionalStartupSettings.Value;

        services.ConfigureApplicationCookie(options =>
                                            
                                                options.AccessDeniedPath = new PathString("/Account/AccessDenied");
                                                options.Cookie.Name = ".AspNetCore.Auth.Cookie";
                                                options.Cookie.Path = "/";
                                                options.Cookie.HttpOnly = true;
                                                options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
                                                options.LoginPath = new PathString("/account/login");
                                                options.Cookie.SameSite = SameSiteMode.None;
                                            );

        var authBuilder = services.AddAuthentication(options =>  options.DefaultAuthenticateScheme = "Identity.Application"; );
        authBuilder = ConfigureSaml2(authBuilder, settings);
        authBuilder = ConfigureGoogle(authBuilder);
        authBuilder.AddCookie();
    
    else
    
        throw new InvalidOperationException($"Startup settings are not configured in appsettings.json.");
    

    SetupEntityFramework(services);


来自appsettings.json的身份服务器客户端配置片段


    "Enabled": true,
    "ClientId": "js",
    "ClientName": "JavaScript Client",
    "AllowedGrantTypes": [ "authorization_code" ],
    "RequirePkce": true,
    "RequireClientSecret": false,
    "RedirectUris": [ "https://dev.myCompany.ch/i/signincallback", "https://dev.myCompany.com/i/signincallback", "https://dev.myCompany.de/i/signincallback" ],
    "PostLogoutRedirectUris": [ "https://dev.myCompany.ch/i/", "https://dev.myCompany.com/i/", "https://dev.myCompany.de/i/" ],
    "AllowedCorsOrigins": [],
    "AllowedScopes": [ "openid", "offline_access", "profile", "Translation.Api", "Person.Api" ],
    "RequireConsent": false,
    "AllowOfflineAccess": true


更新

与此同时,我发现 cookie 在 30 分钟空闲时间后请求 https://ourdomain/.well-known/openid-configuration 时,丢失了 Domain、Path、Expires/Max-Age、HttpOnly、Secure 和 SameSite.None 的值时间>。这些值肯定是在登录后设置的。 响应 cookie 的值 Expires/Max-Age 设置为过去的时间,因此 cookie 将被浏览器丢弃。

有谁知道,为什么这些价值观在一段时间后会丢失?

【问题讨论】:

您是否勾选“记住我”来创建持久登录?您是否有任何 SPA 身份验证令牌的自动续订?客户端令牌的生命周期是多少? - 记住我:yes 已设置。 - 自动续订:是的,我们使用 oidc-client(npmjs.com/package/oidc-client) 和 automaticSilentRenew - 客户端令牌生命周期:根据identityserver4.readthedocs.io/en/latest/reference/client.html(默认为 3600 秒/1 小时) 有很多事情可能是错误的,您还没有发布与身份验证相关的代码,DataProtectionKeys 不相关。您可以从客户端/服务器发布一些代码吗?当您说“30 分钟后,cookie 自动消失。用户必须再次登录”时,您似乎也对身份验证流程感到困惑更新 IdSrv 检查 cookie 的位置。您专注于 cookie,但在 1 小时过去之前它不应该成为问题。 【参考方案1】:

最后,我想出了如何解决这个问题。

这与 IdentityServer 的配置有关。缺少的部分是方法AddAspNetIdentity&lt;LoginInputModel&gt;()

之前:

var certificate = LoadSigningCertificate();
var identityServerBuilder = services.AddIdentityServer(options =>
                                                       
                                                           options.Events.RaiseErrorEvents = true;
                                                           options.Events.RaiseInformationEvents = true;
                                                           options.Events.RaiseFailureEvents = true;
                                                           options.Events.RaiseSuccessEvents = true;
                                                       )
                                    .AddSigningCredential(certificate)
                                    .AddProfileService<ProfileService>()
                                    .AddInMemoryIdentityResources(Config.Ids)
                                    .AddInMemoryApiResources(Config.Apis)
                                    .AddInMemoryClients(new ClientConfigLoader().LoadClients(Configuration));

现在:

var certificate = LoadSigningCertificate();
var identityServerBuilder = services.AddIdentityServer(options =>
                                                       
                                                           options.Events.RaiseErrorEvents = true;
                                                           options.Events.RaiseInformationEvents = true;
                                                           options.Events.RaiseFailureEvents = true;
                                                           options.Events.RaiseSuccessEvents = true;
                                                       )
                                    .AddSigningCredential(certificate)
                                    .AddAspNetIdentity<LoginInputModel>()
                                    .AddProfileService<ProfileService>()
                                    .AddInMemoryIdentityResources(Config.Ids)
                                    .AddInMemoryApiResources(Config.Apis)
                                    .AddInMemoryClients(new ClientConfigLoader().LoadClients(Configuration));

通过附加配置行,Identity Server 可以正确处理 cookie。

【讨论】:

【参考方案2】:

您可以在 Startup.cs 的 Configure 方法中添加以下代码:

        app.UseCookiePolicy(new CookiePolicyOptions
        
            HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always,
            Secure = CookieSecurePolicy.Always,
            MinimumSameSitePolicy=SameSiteMode.None
        );

在使用身份服务器服务之前添加:

       app.UseIdentityServer();

您需要根据您的情况检查“HttpOnly”选项,因为它可能会给 oidc 客户端的反应带来麻烦。

【讨论】:

以上是关于一段时间后,身份验证 cookie 在反应 SPA 中消失的主要内容,如果未能解决你的问题,请参考以下文章

Laravel 5.6 - Passport JWT httponly cookie SPA 身份验证用于自用 API?

带有 Sanctum 身份验证的 Laravel 8 (Reactjs SPA)

在 SPA 中对访客/匿名用户进行身份验证

Laravel 5.6 - 用于自助API的Passport JWT httponly cookie SPA身份验证?

在反应中使用 cookie 的客户端身份验证和受保护的路由

一起使用不记名令牌和 cookie 身份验证