How to Implement Password Resets?
我正在ASP.NET中开发一个应用程序,特别想知道如果我想自己滚动的话,如何实现
具体来说,我有以下问题:
- 生成难以破解的唯一ID的好方法是什么?
- 它是否应该有一个计时器?如果是,应该多长时间?
- 我应该记录IP地址吗?这甚至重要吗?
- 在"密码重置"屏幕下,我应该询问哪些信息?只是电子邮件地址?或者电子邮件地址加上一些他们"知道"的信息?(最喜欢的队、小狗的名字等)
我还有什么需要注意的吗?
NB: Other questions have glossed over technical implementation entirely. Indeed the accepted answer glosses over the gory details. I hope that this question and subsequent answers will go into the gory details, and I hope by phrasing this question much more narrowly that the answers are less 'fluff' and more 'gore'.
编辑:还将讨论如何在SQL Server中建模和处理这样一个表,或任何到答案的ASP.NET MVC链接的答案都将受到赞赏。
编辑2012/05/22:作为这个流行答案的后续行动,我不再在这个过程中使用guid。和另一个流行的答案一样,我现在使用我自己的哈希算法来生成发送到URL的密钥。这也有缩短的优点。查看system.security.cryptography以生成它们,我通常也使用salt。
首先,不要立即重置用户密码。首先,不要在用户请求时立即重置其密码。这是一个安全漏洞,因为有人可能会猜测电子邮件地址(即您在公司的电子邮件地址),并一时兴起重置密码。这些天的最佳实践通常包括发送到用户电子邮件地址的"确认"链接,确认他们想要重置它。此链接是要发送唯一键链接的位置。我用一个链接发送我的:
是的,在链接上设置一个超时,并在后端存储密钥和超时(如果您使用的是salt)。3天的超时是正常的,当用户请求重置时,请确保在Web级别通知用户3天。
使用唯一的哈希键我以前的回答是使用一个guid。我现在编辑这篇文章是为了建议每个人使用随机生成的哈希,例如使用
- 用户点击"重置"密码。
- 要求用户提供电子邮件。
- 用户输入电子邮件并单击发送。不要确认或拒绝电子邮件,因为这也是不好的做法。简单地说,"我们已经发送了一个密码重置请求,如果电子邮件被验证了。"或者类似的秘密。
- 从
RNGCryptoServiceProvider 创建一个散列,将它作为一个单独的实体存储在ut_UserPasswordRequests 表中,并链接回用户。这样您就可以跟踪旧的请求并通知用户旧的链接已经过期。 - 将链接发送到电子邮件。
用户获取链接,如
如果链接已验证,则需要新密码。简单,用户可以设置自己的密码。或者,在这里设置您自己的密码,并在这里告知他们新密码(并通过电子邮件发送给他们)。
这里有很多很好的答案,我不想再重复一遍…
除了一个问题外,几乎每个答案都会重复这个问题,尽管它是错误的:
Guids are (realistically) unique and statistically impossible to guess.
这不是真的,guid是非常弱的标识符,不应该用于允许访问用户的帐户。如果检查结构,最多可以得到128位…这在现在被认为是不多的。其中前半部分是典型的不变量(对于生成系统),剩下的一半是时间依赖的(或类似的)。总之,这是一个非常脆弱和容易被野蛮改造的机制。
所以不要用那个!
相反,只需使用一个加密的强随机数生成器(
其余的,正如其他许多答案所提供的那样。
首先,我们需要知道您对用户已经了解了什么。显然,您有一个用户名和一个旧密码。你还知道什么?你有电子邮件地址吗?你有关于用户最喜欢的花的数据吗?
假设您有用户名、密码和工作电子邮件地址,您需要向用户表中添加两个字段(假设它是数据库表):名为new-passwd-expire的日期和字符串new-passwd-id。
假设您拥有用户的电子邮件地址,当有人请求密码重置时,您将更新用户表,如下所示:
1 2 | new_passwd_expire = now() + some number of days new_passwd_id = some random string of characters (see below) |
接下来,您将向该地址的用户发送电子邮件:
Dear so-and-so
Someone has requested a new password for user account
at . If you did request this password reset, follow this link: http://example.com/yourscript.lang?update=
If that link does not work you can go to http://example.com/yourscript.lang and enter the following into the form:
If you did not request a password reset, you may ignore this email.
Thanks, yada yada
现在,对yourscript.lang进行编码:这个脚本需要一个表单。如果在URL上传递了var更新,那么表单只要求输入用户的用户名和电子邮件地址。如果未通过更新,则要求输入用户名、电子邮件地址和电子邮件中发送的ID代码。您还需要一个新密码(当然是两次)。
要验证用户的新密码,请验证用户名、电子邮件地址和ID代码是否都匹配、请求是否未过期以及两个新密码是否匹配。如果成功,您可以将用户密码更改为新密码,并从用户表中清除密码重置字段。还要确保注销用户/清除任何与登录相关的cookie,并将用户重定向到登录页面。
基本上,新的密码字段是一个只在密码重置页上工作的密码。
一个潜在的改进:您可以从电子邮件中删除
关于你的问题:
生成随机字符串:它不需要是非常随机的。任何guid生成器甚至md5(concat(salt,current_timestamp())都是足够的,其中salt是用户记录上的内容,如timestamp帐户已创建。一定是用户看不见的东西。
定时器:是的,你需要这个只是为了保持数据库的正常运行。不超过一个星期确实是必要的,但至少2天,因为你永远不知道电子邮件延迟可能会持续多久。
IP地址:由于电子邮件可能会延迟几天,因此IP地址只对日志记录有用,而不用于验证。如果你想记录它,就这样做,否则你不需要它。
重置屏幕:见上文。
希望能覆盖它。祝你好运。
发送到记录的电子邮件地址的guid对于大多数运行的工厂应用程序来说可能已经足够了——超时甚至更好。
毕竟,如果用户邮箱被泄露(即黑客拥有电子邮件地址的登录名/密码),那么您就没有什么可以做的了。
1)为了生成唯一的ID,可以使用安全哈希算法。2)计时器连接?您的意思是重置pwd链接过期吗?是的,你可以有一套有效期3)您可以要求除emailid之外的其他信息进行验证。比如出生日期或者一些安全问题4)您还可以生成随机字符并要求输入该字符以及请求…以确保密码请求不会被一些间谍软件或类似的东西自动执行。
您可以通过链接向用户发送电子邮件。此链接将包含一些难以猜测的字符串(如guid)。在服务器端,您还将存储与发送给用户的字符串相同的字符串。现在,当用户按下链接时,您可以在数据库条目中找到具有相同秘密字符串的链接,并重置其密码。
我认为微软的ASP.NET标识指南是一个好的开始。
https://docs.microsoft.com/en-us/aspnet/identity/overview/features-api/account-confirmation-and-password-recovery-with-aspnet-identity
我用于ASP.NET标识的代码:
Web.CONFIG:
1 |
会计总监:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 | [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>" + "Password reset" + $"<p> Hi {user.FullName}, please click this link to reset your password </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>" + "Your password for Client was reset" + $"<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; } } |