2

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

更新:-

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

      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);
    }
}

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

谢谢!!!

4

1 回答 1

0

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

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

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

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

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

不幸的是,我对 IS4 并不熟悉,我无法告诉您是否有可能完全通过使用它的 API 来解决这个问题,相反,我可以给您一些实用的建议。

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

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

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

于 2020-10-25T18:17:59.333 回答