如果用户从 IdentityServer4 中的另一个浏览器/设备登录,如何检测并从应用程序中注销用户?

Posted

技术标签:

【中文标题】如果用户从 IdentityServer4 中的另一个浏览器/设备登录,如何检测并从应用程序中注销用户?【英文标题】:How to detect and logout the user from an application if he logins from another browser/device in IdentityServer4? 【发布时间】:2021-01-30 18:06:13 【问题描述】:

我已经在我的应用程序中实现了 IdentityServer4 SSO。 SSO 可以正常工作以及所有客户端的注销,但是有一个新要求,如果用户已经登录到应用程序并且如果他尝试再次登录(从不同的设备/浏览器),那么他应该自动注销以前的浏览器。我不明白这一点。如何实现这一点以及是否可以跟踪用户登录会话?

更新:-

我们尝试了以下方法,我们使用“Action”过滤属性将会话信息添加到全局静态变量中。这里我们存储了用户登录后的登录会话信息。

      private class LoginSession
        
            internal string UserId  get; set; 
            internal string SessionId  get; set; 
            internal string AuthTime  get; set; 
            internal DateTimeOffset AuthDateTime
            
                get
                
                    if (!string.IsNullOrEmpty(AuthTime))
                        return DateTimeOffset.FromUnixTimeSeconds(long.Parse(AuthTime));
                    else
                        return DateTimeOffset.UtcNow;
                
            
        

        private static List<LoginSession> LoginSessions = new List<LoginSession>();

在“操作过滤器”方法中,我们检查用户的会话 ID 是否已经存在。如果会话存在并且它的 SessionId 与声明会话 id 不匹配,那么我们检查会话的登录时间。如果登录时间小于当前登录时间,则用户将退出系统,否则我们使用最新的会话 ID 和登录时间更新登录会话。由于第二次登录的这个工作流程,登录会话将被更新,因为登录时间总是大于保存的登录会话信息。对于旧的登录会话,用户将退出系统,因为登录时间总是少于更新的会话信息。

public class SessionValidationAttribute : ActionFilterAttribute
        
    public override Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
    
        string action = context.RouteData.Values["action"].ToString();

        if (!string.IsNullOrEmpty(action) &&
            context.Controller.GetType().GetMethod(action).GetCustomAttributes(typeof(AllowAnonymousAttribute), true).Length == 0)
        
            var claims = ((ClaimsIdentity)((Microsoft.AspNetCore.Mvc.ControllerBase)context.Controller).User.Identity).Claims;

            var sessionId = claims.Where(x => x.Type == "sid").First().Value; // context.HttpContext.Request.Cookies.TryGetValue("idsrv.session", out var sessionId);
            var userId = claims.Where(x => x.Type == "sub").First().Value;
            var authTime = claims.Where(x => x.Type == "auth_time").First().Value;
            var authDateTime = DateTimeOffset.FromUnixTimeSeconds(long.Parse(authTime));

            if (LoginSessions.Where(x => x.UserId.Contains(userId)).Count() > 0) // if already logged in 
            
                var latestLogin = LoginSessions.Where(x => x.UserId == userId).OrderByDescending(x => x.AuthDateTime).First();

                if (sessionId != latestLogin.SessionId)
                
                   if(authDateTime > latestLogin.AuthDateTime) // login using new browser(session)
                   
                       latestLogin.SessionId = sessionId; // assign latest sessionId
                       latestLogin.AuthTime = authTime; // assign latest authTime
                   
                   else if (authDateTime < latestLogin.AuthDateTime) // login using old browser(session)
                   
                     LoginSessions.RemoveAll(x => x.UserId == userId && x.SessionId!=latestLogin.SessionId);

                    context.Result = ((Microsoft.AspNetCore.Mvc.ControllerBase)context.Controller)
                                            .RedirectToAction(actionName: "Logout", controllerName: "Home",
                                            routeValues: new  tenant = string.Empty, isRemoteError = false );
                   
                
            
            else
            
                var newLogin = new LoginSession()  UserId = userId, SessionId = sessionId, AuthTime = authTime ;
                LoginSessions.Add(newLogin);
            
        
        return base.OnActionExecutionAsync(context, next);
    

这适用于我们为少数用户进行的测试,但该解决方案是否适用于有数千名用户登录系统的实际场景?全局使用静态变量来存储会话信息是个好主意吗?使用这个有什么潜在的缺点。请建议。我们也对新想法持开放态度,如果有任何实现此功能的新方法,请告诉我们。

谢谢!!!

【问题讨论】:

【参考方案1】:

免责声明:我没有 IS4 的实践经验。

您可能有充分的理由,但我不明白为什么您在验证当前最新登录时会覆盖 latestLogin 会话的详细信息?

如果我没记错的话,这一行将循环遍历您应用程序中的 所有 个会话,您在后面的行中有多个类似的会话。

if (LoginSessions.Where(x => x.UserId.Contains(userId)).Count() > 0)

这确实是您不希望在您希望扩展的应用程序中做的事情。

很遗憾,我对 IS4 并不熟悉,我无法告诉你是否有可能完全利用它的 API 来解决这个问题,但我可以给你一些实用的建议。

您可以使用单独的集中式存储,可能性是无穷无尽的,但类似于memcached 的东西是完美的。那么算法就相当简单了:

    每当用户尝试登录时,从存储中检索存储在用户 ID 下的值。 如果存在,那将是当前会话 ID,然后在 IS4 中将其销毁并继续。 创建新的登录会话并将会话 ID 存储在用户 ID 下的 memcached 中。

这样,给定用户的会话永远不会超过 1 个,并且您已成功将算法的复杂性从 O(n) 或更糟的情况降低到 O(1)。

【讨论】:

以上是关于如果用户从 IdentityServer4 中的另一个浏览器/设备登录,如何检测并从应用程序中注销用户?的主要内容,如果未能解决你的问题,请参考以下文章

从 Oauth2 中的另一个资源获取资源

如何在 identityserver4 中没有用户名和密码的情况下从令牌端点获取令牌?

是否可以将 IdentityServer4 中的用户注销为其他用户?

IdentityServer4主题之确认

IdentityServer4 的用户注册过程

从Caliburn.Micro,WPF,MVVM中的另一个窗口获取信息