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

如何在Identity Server中防止并发登录?UpdateSecurityStampAsync无效问题

解决Identity Server并发登录强制登出问题

你遇到的核心问题是:UpdateSecurityStampAsync仅更新了用户的安全戳,但Identity Server的自包含Token(Access Token、ID Token)在过期前仍有效,且旧会话未被主动终结,导致第一个浏览器的登录状态未被清除。以下是具体解决方案:

1. 确保请求时实时验证安全戳

默认情况下,Identity Server可能仅在登录时验证安全戳,后续请求不会主动检查。需要在Cookie验证流程中添加安全戳校验逻辑,每次请求时对比当前用户的安全戳与凭证中的安全戳,不一致则强制登出:

services.AddAuthentication()
    .AddCookie(options =>
    {
        options.Events.OnValidatePrincipal = async context =>
        {
            var userManager = context.HttpContext.RequestServices.GetRequiredService<UserManager<ApplicationUser>>();
            var user = await userManager.GetUserAsync(context.Principal);
            
            if (user == null)
            {
                context.RejectPrincipal();
                return;
            }

            // 获取数据库中最新的安全戳
            var currentSecurityStamp = await userManager.GetSecurityStampAsync(user);
            // 获取当前凭证中的安全戳
            var principalSecurityStamp = context.Principal.FindFirstValue("AspNet.Identity.SecurityStamp");

            if (!string.Equals(currentSecurityStamp, principalSecurityStamp))
            {
                // 安全戳不一致,拒绝当前凭证并登出
                context.RejectPrincipal();
                await context.HttpContext.SignOutAsync();
            }
        };
    });

2. 主动终结用户的旧会话

更新安全戳后,需要主动清理用户的旧会话。使用Identity Server的ISessionService查询用户的所有会话,排除当前会话后批量注销:

public async Task RevokeUserOldSessions(string userId, string currentSessionId)
{
    var sessionService = _serviceProvider.GetRequiredService<ISessionService>();
    // 查询该用户的所有会话
    var userSessions = await sessionService.ListSessionsAsync(new SessionFilter { SubjectId = userId });

    foreach (var session in userSessions)
    {
        // 跳过当前活跃的会话
        if (session.SessionId != currentSessionId)
        {
            await sessionService.RemoveSessionAsync(session.SessionId);
        }
    }
}

调用此方法时,可从HttpContext中获取当前会话ID:HttpContext.User.FindFirstValue("session_id")。

3. 失效旧的Refresh Token

如果客户端使用了Refresh Token,即使Access Token过期,客户端仍可通过旧的Refresh Token获取新Token。需要在更新安全戳后,清理该用户的所有旧Refresh Token:

public async Task InvalidateUserRefreshTokens(string userId)
{
    var context = _serviceProvider.GetRequiredService<IdentityServerDbContext>();
    // 删除该用户所有未过期的Refresh Token
    var refreshTokens = context.RefreshTokens.Where(t => t.SubjectId == userId && !t.IsExpired);
    context.RefreshTokens.RemoveRange(refreshTokens);
    await context.SaveChangesAsync();
}

4. 前端配合处理

后端处理后,前端可能仍持有旧Token,需确保:

  • 前端拦截401 Unauthorized响应,自动跳转到登录页
  • 若使用实时通信工具(如SignalR),可推送强制登出通知,让前端主动清除本地缓存的Token并跳转

关键注意事项

  • 缩短Access Token的有效期(建议15-30分钟),减少旧Token的有效时间窗口
  • 确保分布式缓存(如Redis)配置正确,安全戳的更新能被及时读取到,避免缓存不一致
  • 若使用Identity Server的持久化会话,需确保数据库操作的事务一致性,避免会话清理不彻底

内容的提问来源于stack exchange,提问作者Rahul

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.16 09:25:38