更改密码时从所有浏览器中注销用户

Posted

技术标签:

【中文标题】更改密码时从所有浏览器中注销用户【英文标题】:Logout User From all Browser When Password is changed 【发布时间】:2016-05-15 05:53:40 【问题描述】:

我有一个重置密码页面:

当用户填写详细信息并单击Reset Password 按钮时。调用以下控制器:

public ActionResult ResetPassword(ResetPassword model)

    ...
    return RedirectToAction("Logout");

当用户更改密码时,他们会从浏览器获得Logged Out。但是,如果他们同时登录到另一个浏览器,他们仍然在另一个浏览器上登录。

我想在用户更改密码时从他们登录的所有浏览器中注销。

【问题讨论】:

【参考方案1】:

我看到您正在使用 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))
    
);

重现步骤:

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

【讨论】:

这个方法你试过了吗 是的。我在上面的答案中添加了重现的步骤。这是我做的测试。 我一直想知道安全印章是用来做什么的。感谢您的帮助!【参考方案2】:

所以我回到家并决定整理一些代码。给我看代码!!!

我会使用一个处理程序,所以验证总是在用户第一次访问应用程序时完成,并且每次操作方法访问都在一个地方完成。

这个想法是当用户重置密码时,应用程序记录用户已重置密码并且没有第一次登录并注销用户。

user.HasResetPassword = true;
user.IsFirstLoginAfterPasswordReset = false;

当用户登录时,应用程序会验证用户之前是否重置了密码并且现在是第一次登录。如果这些声明有效,则应用程序会更新其记录,说明您尚未重置密码并且您不是第一次登录。

第 1 步

为 ApplicationUser 模型添加两个属性

第 2 步

在 Models 文件夹中添加一个类 AuthHandler.cs,实现如下。 在此阶段,您将验证用户是否已重置密码,并且自密码重置后首次未登录。如果是这样,将用户重定向到登录。

第 3 步

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

第 4 步

在 ResetPassword 方法中添加如下实现。在这一步,当用户重置密码时,更新属性说,他们已经重置密码并且没有第一次登录。请注意,用户在重置密码时也会显式退出。

第 5 步

在 Login 方法中添加下面的实现。在这一步如果用户登录成功,验证他们的密码是否被重置并且他们第一次登录是假的。如果所有条件都为真,则更新数据库中的属性,以便这些属性处于准备好用户将来重置密码时的状态。这样的循环确定和更新密码重置的状态和重置密码后的首次登录。

最后

您的 AspnetUsers 表应如下所示

评论

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

【讨论】:

您似乎将相同的图像粘贴了两次。另外,我相信您的方法仅关闭一个打开的会话,但如果有更多的会话打开(两个、三个)则失败,我相信您的方法是否正确 - 第一个打开的会话重新进行身份验证,但其余的仍然可以在没有身份验证的情况下工作。 @Wiktor 谢谢,我已经更新了图片并添加了一些 cmets。 -1,这没有帮助,这不会阻止在其他浏览器中保存密码。此外,如果您在其他地方再次登录,您会将 HasResetPassword 字段重置为 false,通过这样做,您仍然允许其他登录会话使用相同的旧密码继续。 从任何网络浏览器保存的密码都将不起作用,因为密码已被重置。从您的 cmets 中,我知道您尚未实施或测试它。 @anand 提出了这个问题,从他的评论中可以看出,他对解决方案非常满意。 当我们更改当前密码时,保存的密码是没有用的。但我担心的是这个过程是有效的和通用的。为了使您的答案最好,它需要所有用户 cmets【参考方案3】:

即使 ASP.NET 身份验证清楚地表明您必须进行二次检查以确认用户是否仍然是活跃的登录用户(例如,我们可以阻止用户,用户可能已经更改了他的密码),表单身份验证票对这些事情不提供任何安全保障。

UserSession与ASP.NET MVC Session无关,这里只是一个名字

我实施的解决方案是,

    UserSessionID (PK, Identity) UserID (FK) DateCreated, DateUpdated在数据库中创建UserSessions表 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 类可以连接到 Authorize

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));

【讨论】:

太棒了,我接受了这个想法并实施了自己的解决方案【参考方案4】:

ASP.NET 身份验证依赖于用户浏览器上的 cookie。因为你使用两个不同的浏览器来测试它。您将有两个不同的身份验证 cookie。在 cookie 过期之前,用户仍然经过身份验证这就是您获得该结果的原因。

因此,您将不得不提供一些自定义实现。

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

【讨论】:

对自定义实现有任何想法 是的。您是使用实体框架代码优先还是数据库优先? 我使用的是 EF 6、asp.net 4.5.2、iis-8.5 和 Identity 2【参考方案5】:

我围绕 Github 博客中的这篇文章建模了我的方法

Modeling your App's User Session

他们使用 Hybrid Cookie Store / DB approach 使用 ruby​​,但我将其移植到我的 ASP .Net MVC 项目并且工作正常。

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

我在基本控制器上使用ActionFilterAttribute 来检查活动会话cookie。如果发现会话 cookie 过期,则用户将注销并重定向到登录。

【讨论】:

【参考方案6】:

根据 CodeRealm 的回答...

对于在浏览器上通过 https 访问您的应用程序会引发空指针异常(即未将对象引用设置为对象的实例)的任何人,这是因为您的数据库中可能存在现有记录HasResetPassWord 和/或 IsFirstLoginAfterPasswordReset 为空。 Http请求可以,但是https请求会失败,不知道为什么。

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

【讨论】:

以上是关于更改密码时从所有浏览器中注销用户的主要内容,如果未能解决你的问题,请参考以下文章

更改密码或关闭标签后注销用户

新用户注册/密码更改后强制注销

新用户注册/密码更改后强制注销

Laravel 5 - 从他的所有设备中注销用户

Graphql Mutation 更新用户数据,但更改密码,注销后无法登录

如何防止用户更改密码后阅读?