一段时间后,身份验证 cookie 在 React SPA 中消失
Authentication cookie disappears in react SPA after some time
我们有一个 SPA,用 React 编写,与 ASP.net 核心一起用于托管。
为了验证应用程序,我们使用 IdentityServer4 并使用 cookie。客户端根据示例配置,此处描述:https://github.com/IdentityServer/IdentityServer4/tree/main/samples/Quickstarts/4_JavaScriptClient/src
为了验证用户,一切正常。它将被重定向到登录页面。登录后,重定向到 SPA 就完成了。身份验证 cookie 已按预期设置:
- HttpOnly = true
- 安全 = 真
- SameSite = None
- Expires / Max-age = 登录后一周
出于身份验证原因,该 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 将被浏览器删除。
有没有人知道为什么这些值在一段时间后丢失了?
终于,我想出了解决这个问题的方法。
与IdentityServer的配置有关。缺少的部分是方法 AddAspNetIdentity<LoginInputModel>()
.
之前:
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。
您可以在 Startup.cs 的 Configure 方法中添加以下代码:
app.UseCookiePolicy(new CookiePolicyOptions
{
HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always,
Secure = CookieSecurePolicy.Always,
MinimumSameSitePolicy=SameSiteMode.None
});
在使用身份服务器服务之前添加:
app.UseIdentityServer();
您需要根据您的情况检查“HttpOnly”选项,因为它可能会给 oidc 客户端的反应带来麻烦。
我们有一个 SPA,用 React 编写,与 ASP.net 核心一起用于托管。
为了验证应用程序,我们使用 IdentityServer4 并使用 cookie。客户端根据示例配置,此处描述:https://github.com/IdentityServer/IdentityServer4/tree/main/samples/Quickstarts/4_JavaScriptClient/src
为了验证用户,一切正常。它将被重定向到登录页面。登录后,重定向到 SPA 就完成了。身份验证 cookie 已按预期设置:
- HttpOnly = true
- 安全 = 真
- SameSite = None
- Expires / Max-age = 登录后一周
出于身份验证原因,该 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 将被浏览器删除。
有没有人知道为什么这些值在一段时间后丢失了?
终于,我想出了解决这个问题的方法。
与IdentityServer的配置有关。缺少的部分是方法 AddAspNetIdentity<LoginInputModel>()
.
之前:
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。
您可以在 Startup.cs 的 Configure 方法中添加以下代码:
app.UseCookiePolicy(new CookiePolicyOptions
{
HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.Always,
Secure = CookieSecurePolicy.Always,
MinimumSameSitePolicy=SameSiteMode.None
});
在使用身份服务器服务之前添加:
app.UseIdentityServer();
您需要根据您的情况检查“HttpOnly”选项,因为它可能会给 oidc 客户端的反应带来麻烦。