更改密码后从所有浏览器注销用户
Logout User From all Browser When Password is changed
我有一个重置密码页面:
当用户填写详细信息并单击 Reset Password
按钮时。调用以下控制器:
public ActionResult ResetPassword(ResetPassword model)
{
...
return RedirectToAction("Logout");
}
当用户更改密码时,他们会从浏览器获取 Logged Out
。但是,如果他们同时登录到另一个浏览器,他们将在另一个浏览器上保持登录状态。
我想在用户更改密码时从他们登录的所有浏览器中注销用户。
ASP.NET 身份验证依赖于用户浏览器上的 cookie。因为你用两个不同的浏览器来测试。您将有两种不同的身份验证 cookies.Until cookie 过期用户仍然经过身份验证这就是您获得该结果的原因。
因此您将需要一些自定义实现。
比如经常查看用户是否已经重置密码,并且还没有使用新密码首次登录。如果他们还没有,请注销他们并重定向到登录。当他们登录时,将创建一个新的身份验证 cookie。
我根据 Github 的博客
中的这篇文章建模了我的方法
Modeling your App's User Session
他们使用 Hybrid Cookie Store / DB approach
使用 ruby 但我将它移植到我的 ASP .Net MVC 项目并且工作正常。
用户可以查看所有其他会话并在需要时撤销它们。当用户重置密码时,所有活动会话都将被撤销。
我在基本控制器上使用 ActionFilterAttribute
来检查活动会话 cookie。如果发现会话 cookie 过时,用户将被注销并重定向到登录。
所以我回到家,决定整理一些代码。告诉我代码!!!
我会使用一个处理程序,这样验证总是在用户第一次访问应用程序时完成,并且每次访问操作方法时都在一个地方完成。
想法是当用户重置密码时,应用程序记录用户已重置密码并且不是第一次登录并注销用户。
user.HasResetPassword = true;
user.IsFirstLoginAfterPasswordReset = false;
当用户登录时,应用程序会验证用户之前是否已重置密码并且现在是首次登录。如果这些声明有效,应用程序会更新其记录,说明您没有重置密码并且您不是第一次登录。
步骤 1
向 ApplicationUser 模型添加两个属性
步骤 2
在 Models 文件夹中添加一个 class AuthHandler.cs,实现如下。
在此阶段,您验证用户是否已重置密码并且是否在密码重置后首次登录。如果这是真的,将用户重定向到登录。
步骤 3
在 RouteConfig.cs 中调用 AuthHandler,以便为应用程序的每个传入 http 请求调用它。
步骤 4
在 ResetPassword 方法中添加如下实现。在此步骤中,当用户重置密码时将属性更新为 say ,他们已重置密码并且不是第一次登录。请注意,用户在重置密码时也会明确注销。
步骤 5
在登录方法中添加下面的实现。在此步骤中,如果用户成功登录,请验证他们的密码已重置并且他们首次登录是错误的。如果所有条件都为真,则更新数据库中的属性,以便用户将来重置密码时属性处于准备就绪状态。如此循环确定和更新密码重置的状态,并在重置密码后首次登录。
最后
您的 AspnetUsers table 应如下所示
评论
这就是我的处理方式。我没有测试它,所以如果你遇到异常,你可能已经修改了它。它也都是硬编码的,以显示解决问题的方法。
基于 CodeRealm 的回答...
对于任何遇到在浏览器上对您的应用程序进行 https 访问会抛出空指针异常(即未将对象引用设置为对象的实例)的情况的任何人,这是因为您的应用程序中可能存在现有记录HasResetPassWord and/or IsFirstLoginAfterPasswordReset 为空的数据库。 Http 请求可以,但是 https 请求会失败,不知道为什么。
解决方案:只需手动更新数据库并给两个字段值。最好在两列上都为 false。
我看到你正在使用 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);
否则发起密码更改的 user/session 也将被注销。
要立即注销所有其他会话,您需要降低配置中的检查间隔:
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 应用程序。
- 选择 MVC 模板。
- 编辑 App_Stat/Startup.Auth.cs,第 34 行:将
validateInterval: TimeSpan.FromMinutes(30)
更改为 validateInterval: TimeSpan.FromSeconds(1)
- 编辑Controllers/ManageController.cs,第236行:添加
UserManager.UpdateSecurityStampAsync
方法调用。
- 运行项目,创建用户,登录,打开不同的浏览器,同样登录。
- 更改密码,在其他浏览器中刷新页面:您应该已注销。
甚至 ASP.NET 身份验证也明确表示您必须进行二次检查以确认用户是否仍然是活动登录用户(例如,我们可以阻止该用户,用户可能已更改其密码) , Forms Authentication ticket 不提供任何针对这些东西的安全性。
UserSession和ASP.NETMVC Session没有关系,这里只是起个名字
我实施的解决方案是,
- 在数据库中用
UserSessionID (PK, Identity) UserID (FK) DateCreated, DateUpdated
创建一个UserSessions
table
- 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 class 可以挂接到 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));
}
我有一个重置密码页面:
当用户填写详细信息并单击 Reset Password
按钮时。调用以下控制器:
public ActionResult ResetPassword(ResetPassword model)
{
...
return RedirectToAction("Logout");
}
当用户更改密码时,他们会从浏览器获取 Logged Out
。但是,如果他们同时登录到另一个浏览器,他们将在另一个浏览器上保持登录状态。
我想在用户更改密码时从他们登录的所有浏览器中注销用户。
ASP.NET 身份验证依赖于用户浏览器上的 cookie。因为你用两个不同的浏览器来测试。您将有两种不同的身份验证 cookies.Until cookie 过期用户仍然经过身份验证这就是您获得该结果的原因。
因此您将需要一些自定义实现。
比如经常查看用户是否已经重置密码,并且还没有使用新密码首次登录。如果他们还没有,请注销他们并重定向到登录。当他们登录时,将创建一个新的身份验证 cookie。
我根据 Github 的博客
中的这篇文章建模了我的方法Modeling your App's User Session
他们使用 Hybrid Cookie Store / DB approach
使用 ruby 但我将它移植到我的 ASP .Net MVC 项目并且工作正常。
用户可以查看所有其他会话并在需要时撤销它们。当用户重置密码时,所有活动会话都将被撤销。
我在基本控制器上使用 ActionFilterAttribute
来检查活动会话 cookie。如果发现会话 cookie 过时,用户将被注销并重定向到登录。
所以我回到家,决定整理一些代码。告诉我代码!!!
我会使用一个处理程序,这样验证总是在用户第一次访问应用程序时完成,并且每次访问操作方法时都在一个地方完成。
想法是当用户重置密码时,应用程序记录用户已重置密码并且不是第一次登录并注销用户。
user.HasResetPassword = true;
user.IsFirstLoginAfterPasswordReset = false;
当用户登录时,应用程序会验证用户之前是否已重置密码并且现在是首次登录。如果这些声明有效,应用程序会更新其记录,说明您没有重置密码并且您不是第一次登录。
步骤 1
向 ApplicationUser 模型添加两个属性
步骤 2
在 Models 文件夹中添加一个 class AuthHandler.cs,实现如下。 在此阶段,您验证用户是否已重置密码并且是否在密码重置后首次登录。如果这是真的,将用户重定向到登录。
步骤 3
在 RouteConfig.cs 中调用 AuthHandler,以便为应用程序的每个传入 http 请求调用它。
步骤 4
在 ResetPassword 方法中添加如下实现。在此步骤中,当用户重置密码时将属性更新为 say ,他们已重置密码并且不是第一次登录。请注意,用户在重置密码时也会明确注销。
步骤 5
在登录方法中添加下面的实现。在此步骤中,如果用户成功登录,请验证他们的密码已重置并且他们首次登录是错误的。如果所有条件都为真,则更新数据库中的属性,以便用户将来重置密码时属性处于准备就绪状态。如此循环确定和更新密码重置的状态,并在重置密码后首次登录。
最后
您的 AspnetUsers table 应如下所示
评论
这就是我的处理方式。我没有测试它,所以如果你遇到异常,你可能已经修改了它。它也都是硬编码的,以显示解决问题的方法。
基于 CodeRealm 的回答...
对于任何遇到在浏览器上对您的应用程序进行 https 访问会抛出空指针异常(即未将对象引用设置为对象的实例)的情况的任何人,这是因为您的应用程序中可能存在现有记录HasResetPassWord and/or IsFirstLoginAfterPasswordReset 为空的数据库。 Http 请求可以,但是 https 请求会失败,不知道为什么。
解决方案:只需手动更新数据库并给两个字段值。最好在两列上都为 false。
我看到你正在使用 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);
否则发起密码更改的 user/session 也将被注销。
要立即注销所有其他会话,您需要降低配置中的检查间隔:
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 应用程序。
- 选择 MVC 模板。
- 编辑 App_Stat/Startup.Auth.cs,第 34 行:将
validateInterval: TimeSpan.FromMinutes(30)
更改为validateInterval: TimeSpan.FromSeconds(1)
- 编辑Controllers/ManageController.cs,第236行:添加
UserManager.UpdateSecurityStampAsync
方法调用。 - 运行项目,创建用户,登录,打开不同的浏览器,同样登录。
- 更改密码,在其他浏览器中刷新页面:您应该已注销。
甚至 ASP.NET 身份验证也明确表示您必须进行二次检查以确认用户是否仍然是活动登录用户(例如,我们可以阻止该用户,用户可能已更改其密码) , Forms Authentication ticket 不提供任何针对这些东西的安全性。
UserSession和ASP.NETMVC Session没有关系,这里只是起个名字
我实施的解决方案是,
- 在数据库中用
UserSessionID (PK, Identity) UserID (FK) DateCreated, DateUpdated
创建一个 - FormsAuthenticationTicket 有一个名为 UserData 的字段,您可以在其中保存 UserSessionID。
UserSessions
table
当用户登录时
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 class 可以挂接到 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));
}