39

我想要做的是将用户 ID 限制为一次只能登录一台设备。例如,用户 ID“abc”登录到他们的计算机。用户 ID“abc”现在尝试从他们的手机登录。我想要发生的是终止他们计算机上的会话。

Spotify 应用程序正是这样做的——Spotify 一次只允许一个用户 ID 在一台设备上登录。

我正在使用 ASP.NET 成员资格 (SqlMembershipProvider) 和表单身份验证。

我已经尝试过 Session 变量,但我不确定从这里去哪里。

4

6 回答 6

42

我想出了一个非常棒的解决方案。我已经实现的是当用户“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();
}

编辑我还想补充一点,这段代码忽略了“记住我”功能的功能。我的要求没有涉及这个功能(事实上,出于安全原因,我的客户不想使用它)所以我把它省略了。不过,通过一些额外的编码,我很确定可以考虑到这一点。

于 2013-04-11T02:51:08.273 回答
8

您必须将某人登录的信息存储到数据库中。这将允许您验证用户是否已经有一个现有会话。开箱即用的 ASP.NET 中的表单身份验证模块与 cookie 一起工作,您无法在服务器上知道用户是否在其他设备上拥有 cookie,除非您当然将此信息存储在服务器上。

于 2013-04-09T13:44:37.293 回答
8

您可能想要做的是当用户登录时,您将他们的会话 ID 保存在数据库中的某个位置。然后在您访问的每个页面上,您必须检查当前会话 id 是否与数据库中存储的相同,如果不是,则将其注销。

您可能希望创建一个在 OnAuthorization 或 OnActionExecuting 方法中执行此操作的基本控制器。另一种选择是创建自己的授权过滤器(实际上,我更喜欢我自己,因为我不喜欢这样的通用基类)。

在该方法中,您将访问数据库并检查会话 ID。

请注意,他并非万无一失。有人可能会复制会话 cookie 并绕过这个问题,尽管这很模糊,大多数人可能不知道该怎么做,而且很烦人,以至于那些这样做的人不会打扰。

您也可以使用 IP 地址,但同样如此。代理或 nat 防火墙后面的两个人似乎是同一个用户。

于 2013-04-09T13:47:30.963 回答
3

这是一种比接受的答案稍微简单的方法。

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 对象尽可能小。

于 2017-02-27T19:12:18.140 回答
1

我想指出,设置 Session["SessionID"] = "anything" 的一个关键原因是,在您实际将某些内容分配给会话对象之前,会话 ID 似乎在每次请求时都在不断变化。

我使用我编写的一些拆分测试软件遇到了这个问题。

于 2014-01-27T00:35:09.887 回答
0

sessionId我将通过创建一个用户数组和一个activeSessionId变量来处理这种情况,该变量将存储sessionId最近成功请求的设备。然后在每个请求上,我都会检查:

  • 如果sessionId发出此请求的设备存在且有效。
  • 如果一样的话activeSessionId

如果是,那么我知道这个请求来自不同的设备,因此我将从以前的设备注销用户并activeSessionId使用当前设备的更新。

于 2021-09-26T05:55:09.793 回答