当密码更改时,注销用户在所有浏览器中的登录状态

21

我有一个重置密码页面:enter image description here

当用户填写详细信息并单击“重置密码”按钮时,将调用以下控制器:

public ActionResult ResetPassword(ResetPassword model)
{
    ...
    return RedirectToAction("Logout");
}
当用户更改密码时,他们会从浏览器中被“Logged Out”。但是,如果他们同时在另一个浏览器中登录,则仍然在其他浏览器中保持登录状态。
我希望在用户更改密码时,能够让其从所有已登录的浏览器中注销。
6个回答

22

我看到你正在使用ASP.NET Identity 2。你尝试做的已经内置了。你只需要更改SecurityStamp,所有先前的身份验证cookie将不再有效。

在更改密码后,您还需要更改SecurityStamp

await UserManager.ChangePasswordAsync(User.Identity.GetUserId(), model.OldPassword, model.NewPassword);
await UserManager.UpdateSecurityStampAsync(User.Identity.GetUserId());

如果您希望用户保持登录状态,您需要重新发布一个新的身份验证 cookie(登入):

    await SignInManager.SignInAsync(user, isPersistent: false, rememberBrowser: false);
否则,发起密码更改的用户/会话也将被注销。
为了立即注销所有其他会话,您需要在配置中降低检查间隔:
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath = new PathString("/Account/Login"),
    Provider = new CookieAuthenticationProvider
    {
        // Enables the application to validate the security stamp when the user logs in.
        // This is a security feature which is used when you change a password or add an external login to your account.  
        OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
            validateInterval: TimeSpan.FromSeconds(1),
            regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
    }
});

复现步骤:

  1. 在VS2015中创建一个新的Asp.Net Web应用程序。
  2. 选择MVC模板。
  3. 编辑App_Stat/Startup.Auth.cs,第34行:将validateInterval: TimeSpan.FromMinutes(30)更改为validateInterval: TimeSpan.FromSeconds(1)
  4. 编辑Controllers/ManageController.cs,第236行:添加UserManager.UpdateSecurityStampAsync方法调用。
  5. 运行项目,创建用户,登录,使用另一个浏览器也登录。
  6. 更改密码,在另一个浏览器中刷新页面:您应该已注销。

你尝试过这个方法吗? - anand
是的。我在上面的回答中添加了“重现步骤”。这就是我测试时所做的。 - Chris
我一直想知道安全戳是用来做什么的。感谢你提供的有用答案! - mem27

8
所以我回家后决定编写一些代码。给我看代码!
我会使用处理程序,这样验证总是在用户首次访问应用程序时进行,并且在每个操作方法访问时都在一个地方完成。
这个想法是当用户重置密码时,应用程序记录用户已重置密码并且还没有第一次登录,并签出用户。
user.HasResetPassword = true;
user.IsFirstLoginAfterPasswordReset = false;

当用户登录时,应用程序会验证用户以前是否重置了密码,并且现在是第一次登录。如果这些语句有效,则应用程序会更新其记录以表示您尚未重置密码并且不是第一次登录。 步骤1 向ApplicationUser模型添加两个属性。

enter image description here

步骤二

在Models文件夹中添加一个名为AuthHandler.cs的类,并实现以下内容。 在这个阶段,您需要验证用户是否已重置密码并且自密码重置以来尚未首次登录。如果是,则将用户重定向到登录页面。

enter image description here

第三步

在RouteConfig.cs中调用AuthHandler,以便为应用程序的每个传入HTTP请求调用它。enter image description here

第四步

在ResetPassword方法中添加以下实现。在此步骤中,当用户重置密码时,更新属性以表示他们已重置密码并且尚未首次登录。请注意,当用户重置密码时,他们也会明确退出登录。

enter image description here

步骤 5

在登录方法中添加以下实现。在此步骤中,如果用户成功登录,则验证其密码是否已重置,并且首次登录为 false。如果所有条件都为真,则更新数据库中的属性,以便当用户在将来重置密码时,属性处于准备就绪的状态。因此,这是一种循环确定和更新密码重置和首次登录后重置密码的状态。

enter image description here

最后
您的 AspnetUsers 表应如下所示。

enter image description here

评论

这是我处理它的方式。我还没有测试过,如果遇到异常,你可能需要修改它。它也是硬编码的,以展示解决问题的方法。


你似乎贴了两次完全相同的图片。此外,我没错地认为你的方法只关闭了一个已打开的会话,但如果有更多的会话打开(两个、三个),那么它就会失败——第一个打开的会话重新进行身份验证,但其余的会话仍然可以在没有身份验证的情况下工作。 - Wiktor Zychla
@Wiktor 感谢,我已更新了图片并添加了一些注释。 - Julius Depulla
3
-1,这并没有帮助,也不能防止在其他浏览器中保存密码。而且,如果你在其他地方重新登录,你会将HasResetPassword字段重置为false,这样做仍然允许其他已登录的会话继续使用相同的旧密码。 - Akash Kava
任何网络浏览器中保存的密码都将无法使用,因为密码已被重置。从您的评论中,我知道您尚未实施或测试它。@anand提出了问题,正如您从他的评论中所看到的,他对解决方案非常满意。 - Julius Depulla
当我们更改当前密码时,保存的密码就无用了。但我的担忧是这个过程是否高效且通用。为了使您的答案最佳,需要考虑所有用户的评论。 - anand
抱歉,我的意思是,这个选项不支持“记住我”功能,即在服务器端保留cookie。只有在不使用持久性cookie的情况下才能正常工作。 - Akash Kava

1
即使ASP.NET身份验证清楚地表示,您必须进行二次检查以确认用户仍然是活动的已登录用户(例如,我们可以阻止用户,用户可能已更改密码),但Forms身份验证票证不提供对这些内容的任何安全性保护。
UserSession与ASP.NET MVC Session无关,这里只是一个名称。
我实现的解决方案是:
1. 在数据库中创建一个“UserSessions”表,其中包含“UserSessionID(PK,Identity)UserID(FK)DateCreated,DateUpdated”。 2. FormsAuthenticationTicket有一个名为UserData的字段,您可以在其中保存UserSessionID。
当用户登录时:
public void DoLogin(){

     // do not call this ...
     // FormsAuthentication.SetAuthCookie(....

     DateTime dateIssued = DateTime.UtcNow;

     var sessionID = db.CreateSession(UserID);
     var ticket = new FormsAuthenticationTicket(
            userName,
            dateIssued,
            dateIssued.Add(FormsAuthentication.Timeout),
            iSpersistent,
            // userData
            sessionID.ToString());

     HttpCookie cookie = new HttpCookie(
         FormsAuthentication.CookieName,
         FormsAuthentication.Encrypt(ticket));
     cookie.Expires = ticket.Expires;
     if(FormsAuthentication.CookieDomain!=null)
         cookie.Domain = FormsAuthentication.CookieDomain;
     cookie.Path = FormsAuthentication.CookiePath;
     Response.Cookies.Add(cookie);

}

授权用户

Global.asax类可以让我们钩入授权。

public void Application_Authorize(object sender, EventArgs e){
     var user = Context.User;
     if(user == null)   
         return;

     FormsIdentity formsIdentity = user.Identity as FormsIdentity;
     long userSessionID = long.Parse(formsIdentity.UserData);

     string cacheKey = "US-" + userSessionID;

     // caching to improve performance
     object result = HttpRuntime.Cache[cacheKey];
     if(result!=null){
         // if we had cached that user is alright, we return..
         return;
     }

     // hit the database and check if session is alright
     // If user has logged out, then all UserSessions should have been
     // deleted for this user
     UserSession session = db.UserSessions
           .FirstOrDefault(x=>x.UserSessionID == userSessionID);
     if(session != null){

          // update session and mark last date
          // this helps you in tracking and you
          // can also delete sessions which were not
          // updated since long time...
          session.DateUpdated = DateTime.UtcNow;
          db.SaveChanges();

          // ok user is good to login
          HttpRuntime.Cache.Add(cacheKey, "OK", 
               // set expiration for 5 mins
               DateTime.UtcNow.AddMinutes(5)..)

         // I am setting cache for 5 mins to avoid
         // hitting database for all session validation
         return;
     }

     // ok validation is wrong....


     throw new UnauthorizedException("Access denied");

}

当用户退出登录时。
public void Logout(){

    // get the ticket..
    FormsIdentity f = Context.User.Identity as FormsIdentity;
    long sessionID = long.Parse(f.UserData);

    // this will prevent cookie hijacking
    var session = db.UserSessions.First(x=>x.UserSessionID = sessionID);
    db.UserSession.Remove(session);
    db.SaveChanges();

    FormsAuthentication.Signout();
}

当用户更改密码、被阻止或被删除时...
public void ChangePassword(){

    // get the ticket..
    FormsIdentity f = Context.User.Identity as FormsIdentity;
    long sessionID = long.Parse(f.UserData);

    // deleting Session will prevent all saved tickets from
    // logging in
    db.Database.ExecuteSql(
        "DELETE FROM UerSessions WHERE UserSessionID=@SID",
        new SqlParameter("@SID", sessionID));
}

太棒了,我借鉴了这个想法并实现了自己的解决方案。 - Khalid

0

基于CodeRealm的回答...

对于任何遇到浏览器访问应用程序时抛出空指针异常(即对象引用未设置为对象的实例)的情况,这是因为您的数据库中可能存在HasResetPassWord和/或IsFirstLoginAfterPasswordReset为空的记录。HTTP请求将正常工作,但HTTPS请求将失败,原因不明。

解决方案:只需手动更新数据库并为两个字段赋值。最好在两个列上都设置为false。


0

ASP.NET Identity身份验证依赖于用户浏览器上的cookie。因为您使用了两个不同的浏览器进行测试,所以您将获得两个不同的身份验证cookie。在cookie过期之前,用户仍然被认证,这就是您得到这些结果的原因。

因此,您需要进行一些自定义实现。

例如,始终检查用户是否已重置密码并且尚未使用新密码首次登录。如果他们没有,请注销并重定向到登录页面。当他们登录时,将创建一个新的auth cookie。


有没有关于自定义实现的想法? - anand
是的。您正在使用Entity Framework Code First还是Database First? - Julius Depulla
我正在使用EF 6,asp.net 4.5.2,iis-8.5和Identity 2。 - anand

0

我基于Github博客上的这篇文章来设计我的方法。

模拟你的应用程序用户会话

他们使用了一个混合Cookie存储/数据库方法,使用Ruby编写,但我将其移植到了我的ASP .Net MVC项目中,并且运行良好。

用户可以查看所有其他会话并在需要时撤销它们。当用户重置密码时,任何活动会话都将被撤销。

我在基础控制器上使用了一个来检查活动会话cookie。如果发现会话cookie已过期,则用户将被注销并重定向到登录页面。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接