ASP.NET MVC4应用单用户登录限制功能失效问题求助
我仔细梳理了你的代码,发现几个核心问题导致多端登录校验逻辑完全失效,咱们一步步拆解问题并给出修复方案:
核心问题分析
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

