You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ASP.NET MVC4应用单用户登录限制功能失效问题求助

解决单用户单登录校验失效问题:多端登录时校验始终返回true的排查与修复

我仔细梳理了你的代码,发现几个核心问题导致多端登录校验逻辑完全失效,咱们一步步拆解问题并给出修复方案:


核心问题分析

1. SessionId字段被错误赋值为UserId

这是最致命的问题!在登录代码的CreateAuthenticationTicket方法里,你把sessionid设置成了用户的UserId:

int sessionid = new UserBL().GetByUserName(username).UserId;
oLogin.SessionId = sessionid.ToString();

同一个用户的所有登录记录都会用同一个UserId作为SessionId,这就导致后续校验方法里的SessionId != sid判断永远不成立——所有记录的SessionId都一样,自然会认为没有其他设备登录,校验逻辑直接失效。

2. 登录时未清理旧的活跃登录记录

用户新登录时,你没有先把该用户之前的LoggedIn=true的记录置为false,导致数据库里存在多条同一用户的活跃登录记录,校验时无法正确识别冲突。

3. 校验时获取SessionId的方式错误

在HomeController.Index里,你从oUser.UserId.ToString()获取sid,这和登录时的错误逻辑呼应,进一步加剧了校验失效的问题。


分步修复方案

第一步:修复SessionId的生成与存储

放弃用UserId当SessionId,生成一个唯一的会话标识(用Guid最可靠,避免Session.SessionID因Cookie禁用而变化的问题),并把它存入Forms票据的UserData中,方便后续校验时获取:

修改CreateAuthenticationTicket方法:

// 新增参数接收RememberMe
public void CreateAuthenticationTicket(string username, bool rememberMe)
{
    Users oUsers = new Users();
    oUsers.Email = username;
    oUsers.Role = "User";
    
    // 生成唯一会话标识
    string uniqueSessionId = Guid.NewGuid().ToString();
    
    // 把SessionId加入UserData,方便后续校验时取出
    var userDataObj = new 
    { 
        Email = username, 
        Role = "User", 
        SessionId = uniqueSessionId 
    };
    string userData = JsonConvert.SerializeObject(userDataObj);

    FormsAuthenticationTicket authTicket = new FormsAuthenticationTicket(
        1, 
        username, 
        DateTime.Now, 
        DateTime.Now.AddYears(1), 
        rememberMe, // 用传入的RememberMe参数,之前硬编码false是错误的
        userData
    );

    string encTicket = FormsAuthentication.Encrypt(authTicket);
    var isSsl = Request.IsSecureConnection;
    HttpCookie faCookie = new HttpCookie(FormsAuthentication.FormsCookieName, encTicket) 
    { 
        HttpOnly = false, 
        Secure = isSsl, 
        Expires = rememberMe ? DateTime.Now.AddYears(1) : DateTime.Now.AddMinutes(30) // 根据RememberMe设置过期时间
    };
    Response.Cookies.Add(faCookie);

    // 登录记录存入数据库,使用生成的唯一SessionId
    LoginsRepository oLogin = new LoginsRepository();
    oLogin.UserName = username;
    oLogin.SessionId = uniqueSessionId; // 这里用新生成的会话ID
    oLogin.LoggedIn = true;
    oLogin.CreatedOn = Utility.CommonFunction.DateTime_Now();
    oLogin.IPAddress = HttpContext.Request.ServerVariables["REMOTE_ADDR"];
    oLogin.Status = En_LoginStatus.SingleUser.ToString();
    new LoginRepositoryBL().Add(oLogin);
}

第二步:修复Login方法,先清理旧登录记录

用户登录时,先把该用户所有旧的活跃登录记录置为失效:

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginViewModel oLoginViewModel)
{
    try
    {
        bool Result = new UserBL().ValidateUser(oLoginViewModel.UserName, oLoginViewModel.Password);
        if (Result)
        {
            // 先失效该用户所有旧的登录记录
            new LoginRepositoryBL().InvalidateAllOldLogins(oLoginViewModel.UserName);
            
            FormsService.SignIn(oLoginViewModel.UserName, oLoginViewModel.RememberMe);
            CreateAuthenticationTicket(oLoginViewModel.UserName, oLoginViewModel.RememberMe); // 传入RememberMe参数
            return RedirectToLocal(Request.Form["returnUrl"]);
        }
        else
        {
            ViewBag.Error = "Invalid Username or Password / Due to simultaneous login you get blocked.";
            return View();
        }
    }
    catch (Exception ex)
    {
        throw ex;
    }
}

在LoginRepositoryBL中新增清理旧登录的方法:

public void InvalidateAllOldLogins(string username)
{
    try
    {
        using (var ctx = new CnSiteEntities())
        {
            var oldLogins = ctx.LoginsRepository.Where(i => 
                i.LoggedIn == true 
                && i.UserName == username);
            foreach (var item in oldLogins)
            {
                item.LoggedIn = false;
            }
            ctx.SaveChanges();
        }
    }
    catch (Exception)
    {
        throw;
    }
}

第三步:修复HomeController的校验逻辑

从Forms票据中取出当前会话的唯一SessionId,而非从UserId获取:

public class HomeController : CustomerBaseController 
{
    public ActionResult Index() 
    {
        // 从Forms身份票据中获取当前会话信息
        FormsIdentity identity = User.Identity as FormsIdentity;
        if (identity == null)
        {
            FormsService.SignOut();
            return RedirectToAction("Login", "Account");
        }
        
        FormsAuthenticationTicket authTicket = identity.Ticket;
        var userData = JsonConvert.DeserializeObject<dynamic>(authTicket.UserData);
        string currentSessionId = userData.SessionId;
        string username = authTicket.Name;

        Users oUser = new UserBL().GetByUserName(username);
        if (oUser == null)
        {
            FormsService.SignOut();
            return RedirectToAction("Login", "Account");
        }

        // 检查当前会话的登录记录是否有效
        if (new LoginRepositoryBL().IsYourLoginStillTrue(username, currentSessionId))
        {
            // 检查是否有其他活跃登录
            if (new LoginRepositoryBL().IsUserLoggedOnElsewhere(username, currentSessionId))
            {
                // 踢掉其他设备的登录
                new LoginRepositoryBL().LogEveryoneElseOut(username, currentSessionId);
                // 可选:添加提示信息告知用户已踢掉其他登录
                TempData["Message"] = "检测到其他设备登录,已将其强制下线";
            }
            Answers oAnswer = new Answers();
            return View(oAnswer);
        }
        else
        {
            // 当前会话已失效,锁定用户并退出
            oUser.Status = En_Status.Inactive.ToString();
            new UserBL().update(oUser);
            FormsService.SignOut();
            FormsAuthentication.SignOut();
            return RedirectToAction("Login", "Account");
        }
    }
}

第四步:修复数据访问层的校验方法

确保方法逻辑基于正确的SessionId判断:

public bool IsYourLoginStillTrue(string username, string currentSessionId) 
{
    try 
    {
        using (var ctx = new CnSiteEntities()) 
        {
            return ctx.LoginsRepository.Any(i => 
                i.LoggedIn == true 
                && i.UserName == username 
                && i.SessionId == currentSessionId);
        }
    } 
    catch (Exception) 
    {
        throw;
    }
}

public bool IsUserLoggedOnElsewhere(string username, string currentSessionId) 
{
    try 
    {
        using (var ctx = new CnSiteEntities()) 
        {
            return ctx.LoginsRepository.Any(i => 
                i.LoggedIn == true 
                && i.UserName == username 
                && i.SessionId != currentSessionId);
        }
    } 
    catch (Exception) 
    {
        throw;
    }
}

public void LogEveryoneElseOut(string username, string currentSessionId) 
{
    try 
    {
        using (var ctx = new CnSiteEntities()) 
        {
            var otherLogins = ctx.LoginsRepository.Where(i => 
                i.LoggedIn == true 
                && i.UserName == username 
                && i.SessionId != currentSessionId);
            foreach (var item in otherLogins) 
            {
                item.LoggedIn = false;
            }
            ctx.SaveChanges();
        }
    } 
    catch (Exception) 
    {
        throw;
    }
}

额外优化建议

  • 可以给LoginsRepository表添加ExpiresOn字段,定时清理过期的登录记录,避免数据冗余。
  • 考虑把校验逻辑放到自定义的AuthorizeAttribute中,这样所有需要登录的控制器都能自动触发校验,不用在每个Action里重复写。

内容的提问来源于stack exchange,提问作者Shubham Mondal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 06:42:23