ASP.NET Core5应用AuthenticationTicket过期后无法接收请求如何解决
你的站点无响应的核心原因是自定义ITicketStore实现中使用了同步Redis调用,且异步方法未正确实现异步逻辑,大量请求触发时导致ASP.NET Core线程池饥饿,无法处理新请求。你遇到的"所有请求超时、控制台无日志"表现刚好符合线程池耗尽的特征:请求还没进入ASP.NET Core处理管道就因为没有可用线程被阻塞,因此不会产生任何日志输出。
具体问题点
- Redis操作全部为同步执行:
cache.Set、cache.Get<byte[]>、cache.Remove都是同步方法,会直接阻塞调用线程。在Ticket过期后,所有携带过期Cookie的请求都会触发Redis查询,大量同步阻塞瞬间耗尽线程池可用工作线程。 - 异步方法未正确实现:
RenewAsync、RetrieveAsync、RemoveAsync都没有实际的异步操作,直接返回Task.CompletedTask,相当于把异步方法当成同步方法用,进一步加剧线程阻塞。 - 过期时间不匹配:Cookie的过期时间设为2分钟,但Redis中存储Ticket的TTL硬编码为5分钟,会出现Cookie未过期但Redis中Ticket已被清理的异常情况。
解决方案
第一步:给RedisCacheService补充异步方法
首先扩展你的RedisCacheService,提供原生异步操作接口:
// RedisCacheService新增以下异步方法 public async Task SetAsync(string key, byte[] value, TimeSpan ttl) { await _database.StringSetAsync(key, value, ttl); } public async Task<byte[]> GetAsync(string key) { return await _database.StringGetAsync(key); } public async Task RemoveAsync(string key) { await _database.KeyDeleteAsync(key); }
第二步:重写RedisCacheTicketStore为全异步实现
public class RedisCacheTicketStore : ITicketStore { private readonly RedisCacheService cache; private readonly IConfiguration configuration; private readonly ILogger logger; public RedisCacheTicketStore( RedisCacheService redisCacheService, IConfiguration configuration, ILogger logger) { this.cache = redisCacheService; this.configuration = configuration; this.logger = logger; } public async Task<string> StoreAsync(AuthenticationTicket ticket) { var key = $"AuthSessionStore-{Guid.NewGuid()}"; await RenewAsync(key, ticket); logger.Debug("The ticket {Key} was stored.", key); return key; } public async Task RenewAsync(string key, AuthenticationTicket ticket) { // 直接从Ticket属性取过期时间,和Cookie逻辑保持一致 var timeToLive = ticket.Properties.ExpiresUtc.HasValue ? ticket.Properties.ExpiresUtc.Value - DateTimeOffset.UtcNow : TimeSpan.FromMinutes(configuration.GetValue("SessionCookieLifetimeMinutes", 60)); var bytes = SerializeToBytes(ticket); await cache.SetAsync(key, bytes, timeToLive); logger.Debug("The ticket was renew and will be expire at {ExpiresAtUtc}", ticket.Properties.ExpiresUtc); } public async Task<AuthenticationTicket> RetrieveAsync(string key) { var bytes = await cache.GetAsync(key); var ticket = DeserializeFromBytes(bytes); logger.Debug("The ticket {Key} was retrieved.", key); return ticket; } public async Task RemoveAsync(string key) { await cache.RemoveAsync(key); logger.Debug("The ticket {Key} was removed.", key); } private static byte[] SerializeToBytes(AuthenticationTicket source) { return TicketSerializer.Default.Serialize(source); } private static AuthenticationTicket DeserializeFromBytes(byte[] source) { return source == null ? null : TicketSerializer.Default.Deserialize(source); } }
修改完成后,当Ticket过期时RetrieveAsync会返回null,ASP.NET Core Cookie认证中间件会自动将请求标记为未认证,触发挑战流程跳转到登录页,符合你的预期。
分布式缓存场景Cookie会话最佳实践
- 所有缓存操作必须使用异步实现,绝对不能在异步管道中使用同步IO阻塞线程
- Ticket的过期时间直接从
AuthenticationTicket.Properties.ExpiresUtc获取,和Cookie本身的过期逻辑保持一致,避免两边过期时间不匹配 - 增加Redis故障容错机制:可以给Redis操作加超时和异常捕获,出现异常时直接返回null,让中间件走重新登录逻辑,避免Redis故障拖垮整个站点
- 如果开启滑动过期,需要每次调用
RetrieveAsync时判断剩余TTL,不足一定比例时自动调用RenewAsync刷新过期时间
内容的提问来源于stack exchange,提问作者Kevin Hoang
相关产品推荐
相关产品推荐

