You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.15 13:52:40