如何实现口令重置?

我正在使用 ASP.NET 开发一个应用程序,我特别想知道如果我想自己开发一个 Password Reset函数,我该如何实现它。

具体来说,我有以下问题:

  • 生成难以破解的唯一 ID 的好方法是什么?
  • 应该有一个计时器连接到它吗? 如果是这样,它应该有多长时间?
  • 我应该记录 IP 地址吗? 这有关系吗?
  • 在「重设密码」界面下,我应要求提供什么资料?只有邮箱地址吗?或者也许是电子邮件地址加上一些他们“知道”的信息?(最喜欢的球队、小狗的名字等)

还有什么需要我注意的吗?

注意: 其他问题完全有 被掩盖了的技术实现。事实上,公认的答案掩盖了血淋淋的细节。我希望这个问题和随后的答案会进入血淋淋的细节,我希望通过措辞这个问题更狭窄的答案是少了’毛茸茸的’和更多的’血腥’。

编辑 : 如果您能告诉我们如何在 SQL Server 或任何 ASP.NET MVC 链接中建模和处理这样的表,我们将不胜感激。

31900 次浏览

你可以发送一封带链接的电子邮件给用户。这个链接将包含一些难以猜测的字符串(如 GUID)。在服务器端,还将存储与发送给用户的字符串相同的字符串。现在,当用户按下链接时,您可以在数据库条目中找到相同的秘密字符串并重置其密码。

发送到记录的电子邮件地址的 GUID 对于大多数普通的应用程序来说可能已经足够了——有超时甚至更好。

毕竟,如果用户的电子邮箱已经被入侵(例如,黑客拥有电子邮件地址的登录/密码) ,您对此无能为力。

首先,我们需要了解您已经知道的关于该用户的信息。显然,您有一个用户名和一个旧密码。你还知道什么?你有电子邮件地址吗?你有关于用户最喜欢的花的数据吗?

假设您有一个用户名、密码和工作电子邮件地址,您需要向您的用户表中添加两个字段(假设它是一个数据库表) : 一个名为 new _ passwd _ 過期的日期和一个字符串 new _ passwd _ id。

假设您有用户的电子邮件地址,当有人请求重置密码时,您将按以下方式更新用户表:

new_passwd_expire = now() + some number of days
new_passwd_id = some random string of characters (see below)

接下来,向该地址的用户发送一封电子邮件:

亲爱的某某

有人要求在 < 你的网站名称 > 为用户帐户 < 用户名 > 设置新密码。如果您确实要求重置此密码,请点击以下链接:

http://example.com/yourscript.lang?update=&lt;new\_password\_id >

如果该链接不起作用,您可以转到 http://example.com/yourscript.lang并在表单中输入以下内容: < new _ password _ id >

如果您没有要求重置密码,您可以忽略此电子邮件。

谢谢,等等等等

现在,编写脚本。 lang: 这个脚本需要一个表单。如果 var 更新传递了 URL,那么表单只会询问用户的用户名和电子邮件地址。如果更新没有通过,它会询问用户名、电子邮件地址和在电子邮件中发送的 ID 代码。您还要求输入一个新密码(当然是两次)。

要验证用户的新密码,需要验证用户名、电子邮件地址和 id 代码是否匹配,请求是否没有过期,以及两个新密码是否匹配。如果成功,则将用户的密码更改为新密码,并清除用户表中的密码重置字段。还要确保将用户注销/清除任何与登录相关的 cookie,并将用户重定向到登录页面。

实际上,new _ passwd _ id 字段是一个只能在密码重置页面上工作的密码。

一个潜在的改进是: 您可以从电子邮件中删除 < username > 。“有人请求在这个电子邮件地址的帐户密码重置... ...”因此,如果电子邮件被拦截,只有用户才知道用户名。我没有这样开始,因为如果有人正在攻击帐户,他们已经知道用户名。这种增加的隐蔽性阻止了中间人的攻击,以防有人恶意拦截电子邮件。

至于你的问题:

生成随机字符串: 它不需要非常随机。任何 GUID 生成器甚至 md5(concat (salt,current _ time戳()))都足够了,其中 salt 是用户记录上的内容,比如创建了时间戳帐户。必须是用户看不到的东西。

计时器: 是的,您需要这个,只是为了保持您的数据库正常。不超过一个星期是真正必要的,但至少2天,因为你永远不知道多长时间的电子邮件延迟可能持续。

IP 地址: 由于电子邮件可能会被延迟数天,IP 地址只有用于日志,而不是验证。如果你想记录它,那就这样做,否则你就不需要它了。

重置屏幕: 见上文。

1)为了生成唯一的 id,你可以使用 SHA家族。 2)计时器连接? 你的意思是一个重置 pwd 链接的有效期? 是的,你可以有一套有效期 3)你可以询问除了 emailId 之外的更多信息来验证. 。 比如出生日期或者一些安全问题 4)您也可以生成随机字符,并要求输入,也随着 请求. . 以确保密码请求不是由一些间谍软件或类似的东西自动化。

编辑2012/05/22: 作为这个流行答案的后续,我不再在这个过程中使用 GUID。像其他流行的答案一样,我现在使用我自己的哈希算法来生成发送 URL 的密钥。这样做的好处也是更短。查查系统。保安。生成它们的密码学,我通常也使用 SALT。

首先,不要立即重置用户的密码。

首先,当用户请求密码时,不要立即重置该用户的密码。这是一个安全漏洞,因为有人可以随意猜测电子邮件地址(比如你在公司的电子邮件地址)并重置密码。现在的最佳实践通常包括发送到用户电子邮件地址的“确认”链接,确认他们想要重置它。此链接是要发送唯一密钥链接的位置。我发送的链接类似于: example.com/User/PasswordReset/xjdk2ms92

是的,在链接上设置超时,并将密钥和超时存储在后端(如果正在使用,则使用 salt)。超时3天是标准,并确保通知用户3天的网络水平时,他们要求重置。

使用唯一的哈希键

我之前的回答是使用 GUID。我现在正在编辑这个建议每个人使用一个随机生成的散列,例如使用 RNGCryptoServiceProvider。并且,确保从散列表中删除任何“真正的单词”。我记得在一个特殊的早上6点的电话中,一个女人接到了一个开发人员在她的“假设是随机的”散列键中的一个“ C”字。噢!

整个过程

  • 用户点击“重置”密码。
  • 用户被要求发送电子邮件。
  • 用户输入电子邮件并点击发送。不要确认或否认电子邮件,因为这也是不好的做法。简单地说,“我们已经发送了一个密码重置请求,如果电子邮件被验证。”或者类似的东西。
  • 您可以从 RNGCryptoServiceProvider创建一个散列,将它作为一个单独的实体存储在 ut_UserPasswordRequests表中,然后链接回用户。因此,这样您就可以跟踪旧的请求,并通知用户旧的链接已过期。
  • 发送链接到电子邮件。

用户获取链接,如 http://example.com/User/PasswordReset/xjdk2ms92,并单击它。

如果链接经过验证,则要求输入新密码。很简单,用户可以设置自己的密码。或者,在这里设置您自己的密码,并通知他们的新密码在这里(并通过电子邮件发送给他们)。

这里有很多很好的答案,我不想再重复了..。

除了一个问题,几乎每个答案都在重复这个问题,尽管它是错误的:

导游(实际上)是独一无二的,从统计学上来说是不可能猜到的。

这是不正确的,GUID 是非常弱的标识符,应该使用 没有来允许访问用户的帐户。
如果你检查这个结构,你最多只能得到128位,这在现在并不算多。
其中前半部分是典型的不变量(对于生成系统) ,剩下的一半是时间依赖性的(或类似的东西)。
总而言之,这是一个非常脆弱和容易强迫的机制。

所以别用那个!

相反,只需使用加密强随机数生成器(System.Security.Cryptography.RNGCryptoServiceProvider) ,就可以获得至少256位的原始熵。

其余的,正如其他许多答案所提供的那样。

我认为微软的 ASP.NET 身份指南是一个良好的开端。

Https://learn.microsoft.com/en-us/aspnet/identity/overview/features-api/account-confirmation-and-password-recovery-with-aspnet-identity

我用于 ASP.NET 标识的代码:

返回文章页面

<add key="AllowedHosts" value="example.com,2.example" />

返回文章页面

[Route("RequestResetPasswordToken/{email}/")]
[HttpGet]
[AllowAnonymous]
public async Task<IHttpActionResult> GetResetPasswordToken([FromUri]string email)
{
if (!ModelState.IsValid)
return BadRequest(ModelState);


var user = await UserManager.FindByEmailAsync(email);
if (user == null)
{
Logger.Warn("Password reset token requested for non existing email");
// Don't reveal that the user does not exist
return NoContent();
}


//Prevent Host Header Attack -> Password Reset Poisoning.
//If the IIS has a binding to accept connections on 80/443 the host parameter can be changed.
//See https://security.stackexchange.com/a/170759/67046
if (!ConfigurationManager.AppSettings["AllowedHosts"].Split(',').Contains(Request.RequestUri.Host)) {
Logger.Warn($"Non allowed host detected for password reset {Request.RequestUri.Scheme}://{Request.Headers.Host}");
return BadRequest();
}


Logger.Info("Creating password reset token for user id {0}", user.Id);


var host = $"{Request.RequestUri.Scheme}://{Request.Headers.Host}";
var token = await UserManager.GeneratePasswordResetTokenAsync(user.Id);
var callbackUrl = $"{host}/resetPassword/{HttpContext.Current.Server.UrlEncode(user.Email)}/{HttpContext.Current.Server.UrlEncode(token)}";


var subject = "Client - Password reset.";
var body = "<html><body>" +
"<h2>Password reset</h2>" +
$"<p>Hi {user.FullName}, <a href=\"{callbackUrl}\"> please click this link to reset your password </a></p>" +
"</body></html>";


var message = new IdentityMessage
{
Body = body,
Destination = user.Email,
Subject = subject
};


await UserManager.EmailService.SendAsync(message);


return NoContent();
}


[HttpPost]
[Route("ResetPassword/")]
[AllowAnonymous]
public async Task<IHttpActionResult> ResetPasswordAsync(ResetPasswordRequestModel model)
{
if (!ModelState.IsValid)
return NoContent();


var user = await UserManager.FindByEmailAsync(model.Email);
if (user == null)
{
Logger.Warn("Reset password request for non existing email");
return NoContent();
}


if (!await UserManager.UserTokenProvider.ValidateAsync("ResetPassword", model.Token, UserManager, user))
{
Logger.Warn("Reset password requested with wrong token");
return NoContent();
}


var result = await UserManager.ResetPasswordAsync(user.Id, model.Token, model.NewPassword);


if (result.Succeeded)
{
Logger.Info("Creating password reset token for user id {0}", user.Id);


const string subject = "Client - Password reset success.";
var body = "<html><body>" +
"<h1>Your password for Client was reset</h1>" +
$"<p>Hi {user.FullName}!</p>" +
"<p>Your password for Client was reset. Please inform us if you did not request this change.</p>" +
"</body></html>";


var message = new IdentityMessage
{
Body = body,
Destination = user.Email,
Subject = subject
};


await UserManager.EmailService.SendAsync(message);
}


return NoContent();
}


public class ResetPasswordRequestModel
{
[Required]
[Display(Name = "Token")]
public string Token { get; set; }


[Required]
[Display(Name = "Email")]
public string Email { get; set; }


[Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 10)]
[DataType(DataType.Password)]
[Display(Name = "New password")]
public string NewPassword { get; set; }


[DataType(DataType.Password)]
[Display(Name = "Confirm new password")]
[Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}