.NET 6调用SignOutAsync后,同Cookie请求仍保持认证状态的问题
问题分析与解决方案
核心问题
你的问题核心是客户端Cookie已被清除,但服务器端身份验证会话未被注销——即使UI登出后,Postman保留旧Cookie发送请求时,服务器仍会认为用户处于已认证状态,导致User.Identity.IsAuthenticated为true,[Authorize]接口也返回200。
关键原因
默认ASP.NET Core Cookie认证是基于Cookie本身的有效性(过期时间、签名合法性),不会主动在服务器端存储并验证会话状态。只要Cookie未过期且签名有效,服务器就会通过认证,完全不受客户端登出操作的影响。
解决方案
1. 启用服务器端会话存储(推荐)
通过配置服务器端会话跟踪,让服务器主动管理会话状态,登出时直接销毁服务器端的会话记录:
步骤1:添加会话服务与认证配置
在Program.cs(或Startup.cs)中配置会话和Cookie认证:
// 添加会话服务 builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(30); // 与Cookie过期时间保持一致 options.Cookie.HttpOnly = true; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; options.Cookie.SameSite = SameSiteMode.Strict; }); // 配置Cookie认证,启用服务器端会话存储 builder.Services.AddAuthentication("Cookies") .AddCookie(options => { // 使用内存存储会话(生产环境建议替换为Redis等分布式缓存) options.SessionStore = new MemoryCacheTicketStore(builder.Services.BuildServiceProvider().GetRequiredService<IMemoryCache>()); options.Events = new CookieAuthenticationEvents { // 每次请求验证服务器端会话是否存在 OnValidatePrincipal = async context => { var sessionId = context.Principal.FindFirstValue("SessionId"); if (string.IsNullOrEmpty(sessionId)) { context.RejectPrincipal(); await context.HttpContext.SignOutAsync("Cookies"); return; } var ticketStore = context.HttpContext.RequestServices.GetRequiredService<ITicketStore>(); var ticket = await ticketStore.RetrieveAsync(sessionId); if (ticket == null) { context.RejectPrincipal(); await context.HttpContext.SignOutAsync("Cookies"); } } }; });
步骤2:实现会话存储类
添加MemoryCacheTicketStore类用于存储会话票据:
public class MemoryCacheTicketStore : ITicketStore { private readonly IMemoryCache _cache; private readonly TimeSpan _expiration = TimeSpan.FromMinutes(30); public MemoryCacheTicketStore(IMemoryCache cache) { _cache = cache; } public async Task<string> StoreAsync(AuthenticationTicket ticket) { var sessionId = Guid.NewGuid().ToString(); // 将SessionId加入Claims,用于后续验证 var claimsIdentity = (ClaimsIdentity)ticket.Principal.Identity; claimsIdentity.AddClaim(new Claim("SessionId", sessionId)); _cache.Set(sessionId, ticket, _expiration); return await Task.FromResult(sessionId); } public async Task RenewAsync(string key, AuthenticationTicket ticket) { _cache.Set(key, ticket, _expiration); await Task.CompletedTask; } public async Task<AuthenticationTicket?> RetrieveAsync(string key) { return await Task.FromResult(_cache.Get<AuthenticationTicket>(key)); } public async Task RemoveAsync(string key) { _cache.Remove(key); await Task.CompletedTask; } }
步骤3:修改登录与登出逻辑
登录时初始化会话,登出时销毁服务器端会话:
// 登录接口 public async Task<IActionResult> Login(LoginModel model) { // 用户验证逻辑... var claims = new List<Claim> { new Claim(ClaimTypes.Name, model.Username) }; var claimsPrincipal = new ClaimsPrincipal(new ClaimsIdentity(claims, "Cookies")); var authProps = new AuthenticationProperties { ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(30), IsPersistent = model.RememberMe }; await HttpContext.Session.LoadAsync(); await HttpContext.SignInAsync("Cookies", claimsPrincipal, authProps); return Ok(); } // 登出接口 public async Task<IActionResult> Logout() { var sessionId = User.FindFirstValue("SessionId"); if (!string.IsNullOrEmpty(sessionId)) { var ticketStore = HttpContext.RequestServices.GetRequiredService<ITicketStore>(); await ticketStore.RemoveAsync(sessionId); } await HttpContext.Session.ClearAsync(); await HttpContext.SignOutAsync("Cookies"); return Ok(); }
2. 修正Cookie头的错误配置
你返回的Set-Cookie头中samesite=string是非法值,正确值应为SameSite=Strict或SameSite=Lax,这个错误可能导致客户端未正确处理Cookie清除,需在认证配置中修正:
options.Cookie.SameSite = SameSiteMode.Strict;
3. 验证[Authorize]属性配置
确保[Authorize]指定了正确的认证方案:
[Authorize(AuthenticationSchemes = "Cookies")] public IActionResult ProtectedApi() { return Ok(); }
总结
启用服务器端会话存储是解决该问题的根本方案,它能让服务器主动管控会话生命周期,即使客户端保留旧Cookie,服务器也会拒绝认证。生产环境建议使用Redis等分布式缓存替代内存存储,避免多服务器部署时的会话同步问题。
内容的提问来源于stack exchange,提问作者David McEleney
相关产品推荐
相关产品推荐

