我想要做的是将用户 ID 限制为一次只能登录一台设备。例如,用户 ID“abc”登录到他们的计算机。用户 ID“abc”现在尝试从他们的手机登录。我想要发生的是终止他们计算机上的会话。
Spotify 应用程序正是这样做的——Spotify 一次只允许一个用户 ID 在一台设备上登录。
我正在使用 ASP.NET 成员资格 (SqlMembershipProvider) 和表单身份验证。
我已经尝试过 Session 变量,但我不确定从这里去哪里。
我想要做的是将用户 ID 限制为一次只能登录一台设备。例如,用户 ID“abc”登录到他们的计算机。用户 ID“abc”现在尝试从他们的手机登录。我想要发生的是终止他们计算机上的会话。
Spotify 应用程序正是这样做的——Spotify 一次只允许一个用户 ID 在一台设备上登录。
我正在使用 ASP.NET 成员资格 (SqlMembershipProvider) 和表单身份验证。
我已经尝试过 Session 变量,但我不确定从这里去哪里。
我想出了一个非常棒的解决方案。我已经实现的是当用户“Bob”从他们的 PC 登录,然后同一个用户“Bob”从另一个位置登录时,从第一个位置(他们的 PC)登录将被杀死,同时允许第二个登录生活。用户登录后,它会在我创建的名为“Logins”的自定义表中插入一条记录。成功登录后,将在此表中插入一条记录,其中包含“UserId、SessionId 和 LoggedIn”的值。UserId 是不言自明的,SessionId 是当前的 Session ID(在下面解释如何获取),LoggedIn 只是一个布尔值,在用户成功登录后最初设置为 True。我把这个“插入”
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();
对于我的情况,我想对我的每个控制器进行检查,以查看当前登录的用户是否在其他地方登录,如果是,则终止其他会话。然后,当被终止的会话试图导航到我放置这些检查的任何位置时,它会将它们注销并将它们重定向到登录屏幕。
我有三种主要的方法来进行这些检查:
IsYourLoginStillTrue(UserId, SessionId);
IsUserLoggedOnElsewhere(UserId, SessionId);
LogEveryoneElseOut(UserId, SessionId);
将会话 ID 保存到 Session["..."]
在这之前,我将 SessionID 保存到 AccountController 中的 Session 集合中,在Login
( [HttpPost]
) 方法中:
if (Membership.ValidateUser(model.UserName, model.Password))
{
Session["sessionid"] = System.Web.HttpContext.Current.Session.SessionID;
...
控制器代码
然后我在我的控制器中放置逻辑来控制这三个方法的执行流程。请注意,如果由于某种原因Session["sessionid"]
是null
,它只会简单地为其分配一个“空”值。这是以防万一由于某种原因它返回为空:
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");
}
}
三种方法
这些是我用来检查您是否仍然登录的方法(即确保您没有被另一次登录尝试踢走),如果是这样,请检查您的用户 ID 是否在其他地方登录,如果是这样,只需false
在 Logins 表中将其 LoggedIn 状态设置为即可启动它们。
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();
}
编辑我还想补充一点,这段代码忽略了“记住我”功能的功能。我的要求没有涉及这个功能(事实上,出于安全原因,我的客户不想使用它)所以我把它省略了。不过,通过一些额外的编码,我很确定可以考虑到这一点。
您必须将某人登录的信息存储到数据库中。这将允许您验证用户是否已经有一个现有会话。开箱即用的 ASP.NET 中的表单身份验证模块与 cookie 一起工作,您无法在服务器上知道用户是否在其他设备上拥有 cookie,除非您当然将此信息存储在服务器上。
您可能想要做的是当用户登录时,您将他们的会话 ID 保存在数据库中的某个位置。然后在您访问的每个页面上,您必须检查当前会话 id 是否与数据库中存储的相同,如果不是,则将其注销。
您可能希望创建一个在 OnAuthorization 或 OnActionExecuting 方法中执行此操作的基本控制器。另一种选择是创建自己的授权过滤器(实际上,我更喜欢我自己,因为我不喜欢这样的通用基类)。
在该方法中,您将访问数据库并检查会话 ID。
请注意,他并非万无一失。有人可能会复制会话 cookie 并绕过这个问题,尽管这很模糊,大多数人可能不知道该怎么做,而且很烦人,以至于那些这样做的人不会打扰。
您也可以使用 IP 地址,但同样如此。代理或 nat 防火墙后面的两个人似乎是同一个用户。
这是一种比接受的答案稍微简单的方法。
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; }
}
这样,在验证用户后的登录过程中,创建 User 类的实例并为其分配用户名和会话 ID,将其保存为 Session 对象,然后使用它调用 RegisterLogin 函数。
然后,在每次页面加载时,您都会获取会话对象并将其传递给 ValidateCurrentLogin 函数。
DeregisterLogin 函数不是绝对必要的,但会使 _sessions 对象尽可能小。
我想指出,设置 Session["SessionID"] = "anything" 的一个关键原因是,在您实际将某些内容分配给会话对象之前,会话 ID 似乎在每次请求时都在不断变化。
我使用我编写的一些拆分测试软件遇到了这个问题。
sessionId
我将通过创建一个用户数组和一个activeSessionId
变量来处理这种情况,该变量将存储sessionId
最近成功请求的设备。然后在每个请求上,我都会检查:
sessionId
发出此请求的设备存在且有效。activeSessionId
。如果是,那么我知道这个请求来自不同的设备,因此我将从以前的设备注销用户并activeSessionId
使用当前设备的更新。