当同一个用户ID试图在多个设备上login时,如何终止其他设备上的会话?
我想要做的是限制一个用户ID只能login到一个设备一次。 例如,用户ID“abc”login到他们的计算机。 用户ID“abc”现在尝试从他们的手机login。 我想要发生的是杀死他们的电脑上的会议。
Spotify应用程序完全是这样 – Spotify一次只允许在一个设备上login一个用户ID。
我正在使用ASP.NET成员资格(SqlMembershipProvider)和窗体身份validation。
我已经尝试过会话variables,但我不确定从哪里去。
我想出了一个非常棒的解决scheme。 我实现的是当用户“Bob”从他们的PClogin,然后同一个用户“Bob”从另一个位置login时,来自第一个位置(他们的PC)的login将被终止,同时允许第二个login即可。 用户login后,会将logging插入到我创build的名为“login”的自定义表中。 成功login后,将在表中插入一个logging,其中包含“UserId,SessionId和LoggedIn”的值。 UserId是不言而喻的,SessionId是当前的Session ID(在下面解释如何获得),而LoggedIn只是一个布尔值,当用户login成功时,它初始设置为True。 我在我的AccountController的Login方法中放置了这个“插入”逻辑,在成功validation用户的时候,见下面:
Logins login = new Logins(); login.UserId = model.UserName; login.SessionId = System.Web.HttpContext.Current.Session.SessionID;; login.LoggedIn = true; LoginsRepository repo = new LoginsRepository(); repo.InsertOrUpdate(login); repo.Save();
对于我的情况,我想在我的每个控制器上进行检查,看看当前login的用户是否login到其他地方,如果是的话,杀死其他的会话。 然后,当杀死的会话试图导航任何地方我把这些检查,它会登出他们,并将其redirect到login屏幕。
我有三个主要的方法来做这些检查:
IsYourLoginStillTrue(UserId, SessionId); IsUserLoggedOnElsewhere(UserId, SessionId); LogEveryoneElseOut(UserId, SessionId);
将会话ID保存到会话[“…”]
在这之前,我将AccountID保存到Login
( [HttpPost]
)方法内的AccountController中的Session集合中:
if (Membership.ValidateUser(model.UserName, model.Password)) { Session["sessionid"] = System.Web.HttpContext.Current.Session.SessionID; ...
控制器代码
然后我把我的控制器中的逻辑来控制这三种方法的执行stream程。 注意下面,如果因为某种原因Session["sessionid"]
为null
,它只是简单地为它赋值“empty”。 这是由于某种原因,以防万一它返回为空:
public ActionResult Index() { if (Session["sessionid"] == null) Session["sessionid"] = "empty"; // check to see if your ID in the Logins table has LoggedIn = true - if so, continue, otherwise, redirect to Login page. if (OperationContext.IsYourLoginStillTrue(System.Web.HttpContext.Current.User.Identity.Name, Session["sessionid"].ToString())) { // check to see if your user ID is being used elsewhere under a different session ID if (!OperationContext.IsUserLoggedOnElsewhere(System.Web.HttpContext.Current.User.Identity.Name, Session["sessionid"].ToString())) { return View(); } else { // if it is being used elsewhere, update all their Logins records to LoggedIn = false, except for your session ID OperationContext.LogEveryoneElseOut(System.Web.HttpContext.Current.User.Identity.Name, Session["sessionid"].ToString()); return View(); } } else { FormsAuthentication.SignOut(); return RedirectToAction("Login", "Account"); } }
三种方法
这些是我用来检查你是否仍然login(即确保你没有被另一个login尝试踢出)的方法,如果是,请检查你的用户ID是否logging在别的地方,如果是的话,只需在Logins表中设置它们的LoggedIn状态为false
。
public static bool IsYourLoginStillTrue(string userId, string sid) { CapWorxQuikCapContext context = new CapWorxQuikCapContext(); IEnumerable<Logins> logins = (from i in context.Logins where i.LoggedIn == true && i.UserId == userId && i.SessionId == sid select i).AsEnumerable(); return logins.Any(); } public static bool IsUserLoggedOnElsewhere(string userId, string sid) { CapWorxQuikCapContext context = new CapWorxQuikCapContext(); IEnumerable<Logins> logins = (from i in context.Logins where i.LoggedIn == true && i.UserId == userId && i.SessionId != sid select i).AsEnumerable(); return logins.Any(); } public static void LogEveryoneElseOut(string userId, string sid) { CapWorxQuikCapContext context = new CapWorxQuikCapContext(); IEnumerable<Logins> logins = (from i in context.Logins where i.LoggedIn == true && i.UserId == userId && i.SessionId != sid // need to filter by user ID select i).AsEnumerable(); foreach (Logins item in logins) { item.LoggedIn = false; } context.SaveChanges(); }
编辑我也想补充说,这段代码忽略了“记住我”function的能力。 我的要求没有涉及这个function(事实上,我的客户不想使用它,出于安全原因),所以我只是把它离开了。 虽然有一些额外的编码,但我相当肯定这可以考虑在内。
您将不得不将某人login的信息存储到数据库中。 这将允许您validation用户是否已经有一个现有的会话。 ASP.NET中的表单身份validation模块开箱即用,并且无法在服务器上知道用户是否在其他设备上存在Cookie,除非您将此信息存储在服务器上。
你可能想要做的是当用户login时,你将他们的会话ID保存在数据库的某个地方。 然后在你访问的每个页面上,你必须检查当前会话ID是否与存储在数据库中的ID相同,如果没有,你将它们签出。
您可能会想要创build一个基本控制器在OnAuthorization或OnActionExecuting方法中执行此操作。 另一个select是创build你自己的授权filter(我更喜欢自己,实际上,因为我不喜欢这样的公共基类)。
在这种方法中,您将访问数据库并检查会话ID。
要知道他并不是万无一失的。 有人可能会复制会话cookie并解决这个问题,虽然这很难理解,大多数人可能不知道如何去做,而且讨厌那些不会做的事情。
你也可以使用IP地址,但这是一样的交易。 代理或nat防火墙后面的两个人似乎是同一个用户。
我想指出,设置会话[“SessionID”] =“任何”的一个关键原因是,直到你实际上分配的东西到会话对象的会话ID似乎不断变化的每一个请求。
我用我写的一些分割testing软件碰到了这个问题。
这是一个比公认的答案稍微简单一些的方法。
public static class SessionManager { private static List<User> _sessions = new List<User>(); public static void RegisterLogin(User user) { if (user != null) { _sessions.RemoveAll(u => u.UserName == user.UserName); _sessions.Add(user); } } public static void DeregisterLogin(User user) { if (user != null) _sessions.RemoveAll(u => u.UserName == user.UserName && u.SessionId == user.SessionId); } public static bool ValidateCurrentLogin(User user) { return user != null && _sessions.Any(u => u.UserName == user.UserName && u.SessionId == user.SessionId); } } public class User { public string UserName { get; set; } public string SessionId { get; set; } }
因此,在您validation用户的login过程中,您将创build一个User类的实例,并为其分配用户名和会话ID,并将其保存为Session对象,然后使用它调用RegisterLogin函数。
然后,在每次页面加载时,您将获得会话对象并将其传递给ValidateCurrentLogin函数。
DeregisterLogin函数不是绝对必要的,但会尽可能减小_sessions对象。