CookieAuthentication持续重建Cookie与身份验证票据问题排查
环境与配置
- Blazor Server应用,默认认证方案为Cookie,挑战方案为OpenIdConnect
- 基于SQL Server分布式缓存实现
ITicketStore - 访问令牌有效期48小时,刷新令牌有效期30天
Cookie配置代码
services.AddOptions<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme) .Configure<ITicketStore>((options, store) => { options.ExpireTimeSpan = TimeSpan.FromDays(14); options.SlidingExpiration = true; options.SessionStore = store; });
认证服务配置代码
services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme) .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => { options.Authority = authority; options.ClientId = clientId; options.ClientSecret = clientSecret; options.ResponseType = OpenIdConnectResponseType.Code; options.ResponseMode = OpenIdConnectResponseMode.FormPost; options.GetClaimsFromUserInfoEndpoint = true; options.MapInboundClaims = false; options.SaveTokens = true; options.UseTokenLifetime = false; options.UseSecurityTokenValidator = true; options.Scope.Add(OpenIdConnectScope.OpenIdProfile); options.Scope.Add(OpenIdConnectScope.Email); options.Scope.Add(OpenIdConnectScope.OfflineAccess); options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name", RoleClaimType = "role" }; });
登录逻辑代码
public async Task OnGet(string redirectUri) { await HttpContext.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme, new AuthenticationProperties { RedirectUri = redirectUri, IsPersistent = true, }); }
问题现象
用户每次访问应用时,认证Cookie都会被重建,分布式缓存中持续新增身份验证票据条目。已配置Cookie为持久化且有效期14天,该现象不符合预期。
更新:ITicketStore实现代码
public class AuthenticationTicketStore( IDistributedCache cache, ILogger<AuthenticationTicketStore> logger) : ITicketStore { private const string KeyPrefix = "AuthSessionStore-"; private readonly TicketSerializer ticketSerializer = TicketSerializer.Default; public async Task<string> StoreAsync(AuthenticationTicket ticket) { var key = $"{KeyPrefix}{Guid.NewGuid():N}"; await RenewAsync(key, ticket); return key; } public Task RenewAsync(string key, AuthenticationTicket ticket) { if (ticket == null) { throw new ArgumentNullException(nameof(ticket)); } var options = new DistributedCacheEntryOptions(); var expiresUtc = ticket.Properties.ExpiresUtc; if (expiresUtc.HasValue) { options.SetAbsoluteExpiration(expiresUtc.Value); } if (ticket.Properties.AllowRefresh ?? false) { options.SetSlidingExpiration(TimeSpan.FromMinutes(60)); } return cache.SetAsync(key, ticketSerializer.Serialize(ticket), options); } public async Task<AuthenticationTicket> RetrieveAsync(string key) { var value = await cache.GetAsync(key); return value != null ? ticketSerializer.Deserialize(value) : null; } public Task RemoveAsync(string key) => cache.RemoveAsync(key); }
核心问题分析
- 配置覆盖问题:使用
AddOptions配置CookieAuthenticationOptions后,后续调用AddCookie会重置默认配置,导致SessionStore、ExpireTimeSpan等设置未生效,中间件无法复用原有缓存票据,只能每次创建新票据。 - 票据过期时间未同步:虽然设置了
UseTokenLifetime = false,但OpenIdConnect中间件未将Cookie的14天有效期同步到AuthenticationTicket的ExpiresUtc属性,SlidingExpiration逻辑失效,导致每次请求都触发票据重建。 - 缓存调用逻辑异常:正常滑动过期时,中间件应调用
RenewAsync更新现有缓存条目,而非StoreAsync生成新Key。若StoreAsync被频繁调用,说明中间件判定当前票据无效,需要创建新票据。
解决方案
1. 合并Cookie配置,避免覆盖
将CookieAuthenticationOptions的配置直接写入AddCookie方法,确保配置生效:
services.AddAuthentication(options => { options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => { options.ExpireTimeSpan = TimeSpan.FromDays(14); options.SlidingExpiration = true; // 直接注入SessionStore,避免配置被覆盖 options.SessionStore = sp => sp.GetRequiredService<ITicketStore>(); }) .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options => { // 原有OpenIdConnect配置... });
2. 强制设置票据过期时间
在OpenIdConnect的OnTokenValidated事件中,手动指定票据的过期时间,确保与Cookie有效期一致:
options.Events = new OpenIdConnectEvents { OnTokenValidated = context => { // 覆盖ID令牌的有效期,使用Cookie配置的14天 context.Properties.ExpiresUtc = DateTimeOffset.UtcNow.AddDays(14); context.Properties.IsPersistent = true; context.Properties.AllowRefresh = true; return Task.CompletedTask; } };
3. 验证ITicketStore注册
确保ITicketStore已正确注册到DI容器:
services.AddScoped<ITicketStore, AuthenticationTicketStore>();
4. 排查缓存读取逻辑
在RetrieveAsync方法中添加日志,检查缓存中是否能正确读取到原有票据,确认缓存Key是否匹配、缓存是否提前失效。
内容的提问来源于stack exchange,提问作者Ivan Debono
相关产品推荐
相关产品推荐

