C# 如何实现密码重置?
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/664673/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to Implement Password Resets?
提问by George Stocker
I'm working on an application in ASP.NET, and was wondering specifically how I could implement a Password Reset
function if I wanted to roll my own.
我正在 ASP.NET 中开发一个应用程序,并且特别想知道Password Reset
如果我想推出自己的功能,如何实现一个功能。
Specifically, I have the following questions:
具体来说,我有以下问题:
- What is a good way of generating a Unique ID that is hard to crack?
- Should there be a timer attached to it? If so, how long should it be?
- Should I record the IP address? Does it even matter?
- What information should I ask for under the "Password Reset" screen ? Just Email address? Or maybe email address plus some piece of information that they 'know'? (Favorite team, puppy's name, etc)
- 生成难以破解的唯一 ID 的好方法是什么?
- 应该有一个计时器吗?如果是这样,应该多长时间?
- 我应该记录IP地址吗?它甚至重要吗?
- 我应该在“密码重置”屏幕下询问什么信息?只是电子邮件地址?或者也许是电子邮件地址加上他们“知道”的一些信息?(最喜欢的球队,小狗的名字等)
Are there any other considerations I need to be aware of?
还有什么我需要注意的其他注意事项吗?
NB: Other questionshave glossed overtechnical 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'.
注意:其他问题完全掩盖了技术实现。事实上,公认的答案掩盖了血腥的细节。我希望这个问题和随后的答案会深入到血腥的细节,我希望通过更狭隘地表述这个问题,答案会少一些“浮夸”,多一些“血腥”。
Edit: Answers that also go into how such a table would be modeled and handled in SQL Server or any ASP.NET MVC links to an answer would be appreciated.
编辑:也将涉及如何在 SQL Server 或任何 ASP.NET MVC 链接中对此类表进行建模和处理的答案将不胜感激。
采纳答案by AviD
Lots of good answers here, I wont bother repeating it all...
这里有很多好的答案,我不会费心重复所有...
Except for one issue, which is repeated by almost every answer here, even though its wrong:
除了一个问题,这里几乎每个答案都重复了这个问题,即使它是错误的:
Guids are (realistically) unique and statistically impossible to guess.
向导(实际上)是独一无二的,并且在统计上无法猜测。
This is not true, GUIDs are very weak identifiers, and should NOTbe used to allow access to a user's account.
If you examine the structure, you get a total of 128 bits at most... which is not considered a lot nowadays.
Out of which the first half is typical invariant (for the generating system), and half of whats left is time-dependant (or something else similar).
All in all, its a very weak and easily bruteforced mechanism.
事实并非如此,GUID 是非常弱的标识符,不应用于允许访问用户的帐户。
如果你检查结构,你最多会得到 128 位......这在现在并不多见。
其中前半部分是典型的不变性(对于生成系统),剩下的一半是时间相关的(或其他类似的东西)。
总而言之,它是一种非常脆弱且易于暴力破解的机制。
So don't use that!
所以不要用那个!
Instead, simply use a cryptographically strong random number generator (System.Security.Cryptography.RNGCryptoServiceProvider
), and get at least 256 bits of raw entropy.
相反,只需使用密码强的随机数生成器 ( System.Security.Cryptography.RNGCryptoServiceProvider
),并获得至少 256 位的原始熵。
All the rest, as the numerous other answers provided.
其余的,正如众多其他答案所提供的那样。
回答by Sergej Andrejev
You could send an email to user with a link. This link would contain some hard to guess string (like GUID). On server side you would also store the same string as you sent to user. Now when user presses on link you can find in your db entry with a same secret string and reset its password.
您可以通过链接向用户发送电子邮件。此链接将包含一些难以猜测的字符串(如 GUID)。在服务器端,您还将存储与发送给用户相同的字符串。现在,当用户按下链接时,您可以在 db 条目中找到具有相同秘密字符串的条目并重置其密码。
回答by E.J. Brennan
A GUID sent to the email address of record is likely enough for most run-of-the-mill applications - with timeout even better.
对于大多数普通应用程序来说,发送到记录的电子邮件地址的 GUID 可能就足够了 - 超时甚至更好。
After all, if the users emailbox has been compromised(i.e. a hacker has the logon/password for the email address), there is not much you can do about that.
毕竟,如果用户的邮箱已被盗用(即黑客拥有电子邮件地址的登录名/密码),您对此无能为力。
回答by jmucchiello
First, we need to know what you already know about the user. Obviously, you have a username and an old password. What else do you know? Do you have an email address? Do you have data regarding the user's favorite flower?
首先,我们需要了解您已经了解的用户信息。显然,您有一个用户名和一个旧密码。你还知道什么?你有电子邮件地址吗?你有关于用户最喜欢的花的数据吗?
Assuming you have a username, password and working email address, you need to add two fields to your user table (assuming it is a database table): a date called new_passwd_expire and a string new_passwd_id.
假设您有用户名、密码和工作电子邮件地址,您需要向您的用户表(假设它是一个数据库表)添加两个字段:一个名为 new_passwd_expire 的日期和一个字符串 new_passwd_id。
Assuming you have the user's email address, when someone requests a password reset, you update the user table as follows:
假设你有用户的电子邮件地址,当有人请求重置密码时,你更新用户表如下:
new_passwd_expire = now() + some number of days
new_passwd_id = some random string of characters (see below)
Next, you send an email to the user at that address:
接下来,您向该地址的用户发送电子邮件:
Dear so-and-so
Someone has requested a new password for user account <username> at <your website name>. If you did request this password reset, follow this link:
http://example.com/yourscript.lang?update=<new_password_id>
If that link does not work you can go to http://example.com/yourscript.langand enter the following into the form: <new_password_id>
If you did not request a password reset, you may ignore this email.
Thanks, yada yada
亲爱的某某
有人为 <您的网站名称> 上的用户帐户 <用户名> 请求了新密码。如果您确实要求重置密码,请点击以下链接:
http://example.com/yourscript.lang?update=<new_password_id>
如果该链接不起作用,您可以访问http://example.com/yourscript.lang并在表单中输入以下内容:<new_password_id>
如果您没有要求重置密码,则可以忽略此电子邮件。
谢谢,亚达亚达
Now, coding yourscript.lang: This script needs a form. If the var update passed on the URL, the form just asks for the user's username and email address. If update is not passed, it asks for username, email address, and the id code sent in the email. You also ask for a new password (twice of course).
现在,编写 yourscript.lang:这个脚本需要一个表单。如果在 URL 上传递了 var 更新,则表单只要求提供用户的用户名和电子邮件地址。如果更新没有通过,它会询问用户名、电子邮件地址和电子邮件中发送的 ID 代码。您还要求输入新密码(当然两次)。
To verify the user's new password, you verify the username, email address, and the id code all match, that the request has not expired, and that the two new passwords match. If successful, you change the user's password to the new password and clear the password reset fields from the user table. Also be sure to log the user out/clear any login related cookies and redirect the user to the login page.
要验证用户的新密码,您需要验证用户名、电子邮件地址和 ID 代码都匹配、请求未过期以及两个新密码匹配。如果成功,您将用户的密码更改为新密码并清除用户表中的密码重置字段。还要确保将用户注销/清除任何与登录相关的 cookie 并将用户重定向到登录页面。
Essentially, the new_passwd_id field is a password that only works on the password reset page.
本质上,new_passwd_id 字段是一个仅适用于密码重置页面的密码。
One potential improvement: you could remove <username> from the email. "Someone has request a password reset for an account at this email address...." Thus making the username something only the user knows if the email is intercepted. I didn't start off that way because if someone is attacking the account, they already know the username. This added obscurity stops man-in-the-middle attacks of opportunity in case someone malicious happens to intercept the email.
一项潜在的改进:您可以从电子邮件中删除 <username>。“有人要求为这个电子邮件地址的帐户重置密码......”因此,如果电子邮件被拦截,则用户名只有用户知道。我不是这样开始的,因为如果有人攻击该帐户,他们已经知道用户名。这种增加的隐蔽性可以阻止中间人的机会攻击,以防有人恶意拦截电子邮件。
As for your questions:
至于你的问题:
generating the random string: It doesn't need to be extremely random. Any GUID generator or even md5(concat(salt,current_timestamp())) is sufficient, where salt is something on the user record like timestamp account was created. It has to be something the user can't see.
生成随机字符串:它不需要非常随机。任何 GUID 生成器甚至 md5(concat(salt,current_timestamp())) 就足够了,其中 salt 是用户记录上的某些内容,例如创建的时间戳帐户。它必须是用户看不到的东西。
timer: Yes, you need this just to keep your database sane. No more than a week is really necessary but at least 2 days since you never know how long an email delay might last.
计时器:是的,你需要这个只是为了保持你的数据库健全。真正需要的时间不超过一周,但至少需要 2 天,因为您永远不知道电子邮件延迟可能会持续多长时间。
IP Address: Since the email could be delayed by days, IP address is only useful for logging, not for validation. If you want to log it, do so, otherwise you don't need it.
IP 地址:由于电子邮件可能会延迟数天,因此 IP 地址仅用于日志记录,而不用于验证。如果你想记录它,就这样做,否则你不需要它。
Reset Screen: See above.
重置屏幕:见上文。
Hope that covers it. Good luck.
希望涵盖它。祝你好运。
回答by Java Guy
1) For generating the unique id you could use Secure Hash Algorithm. 2) timer attached? Did you mean an Expiry for the reset pwd link? Yes you can have an Expiry set 3) You can ask for some more information other than the emailId to validate.. Like date of birth or some security questions 4) You could also generate random characters and ask to enter that also along with the request.. to make sure the password request is not automated by some spyware or things like that..
1) 要生成唯一 id,您可以使用安全哈希算法。2)定时器附加?您的意思是重置密码链接的到期日吗?是的,您可以设置过期时间 3) 您可以要求提供除 emailId 以外的更多信息以进行验证。例如出生日期或一些安全问题 4) 您还可以生成随机字符并要求输入该信息以及请求.. 以确保密码请求不是由某些间谍软件或类似软件自动完成的..
回答by eduncan911
EDIT 2012/05/22: As a follow-up to this popular answer, I no longer use GUIDs myself in this procedure. Like the other popular answer, I now use my own hashing algorithm to generate the key to send in the URL. This has the advantage of being shorter as well. Look into System.Security.Cryptography to generate them, which I usually use a SALT as well.
编辑 2012/05/22:作为这个流行答案的后续,我在这个过程中不再使用 GUID。与其他流行的答案一样,我现在使用自己的散列算法来生成要发送到 URL 中的密钥。这也具有较短的优点。查看 System.Security.Cryptography 以生成它们,我通常也使用 SALT。
First, do not immediately reset the user's password.
首先,不要立即重置用户的密码。
First, do not immediately reset the user's password when they request it. This is a security breach as someone could guess email addresses (i.e. your email address at the company) and reset passwords at whim. Best practices these days usually include a "confirmation" link sent to the user's email address, confirming they want to reset it. This link is where you want to send the unique key link. I send mine with a link like: domain.com/User/PasswordReset/xjdk2ms92
首先,不要在用户要求时立即重置用户密码。这是一种安全漏洞,因为有人可以猜测电子邮件地址(即您在公司的电子邮件地址)并随心所欲地重置密码。这些天的最佳实践通常包括发送到用户电子邮件地址的“确认”链接,确认他们想要重置它。此链接是您要发送唯一键链接的位置。我用以下链接发送我的:domain.com/User/PasswordReset/xjdk2ms92
Yes, set a timeout on the link and store the key and timeout on your backend (and salt if you are using one). Timeouts of 3 days is the norm, and make sure to notify the user of 3 days at the web level when they request to reset.
是的,在链接上设置超时并将密钥和超时存储在您的后端(如果您使用的是盐)。3 天的超时是常态,并确保在用户请求重置时在 Web 级别通知用户 3 天。
Use a unique hash key
使用唯一的哈希键
My previous answer said to use a GUID. I'm now editing this to advise everyone to use a randomly generated hash, e.g. using the RNGCryptoServiceProvider
. And, make sure to eliminate any "real words" from the hash. I recall a special 6am phone call of where a woman received a certain "c" word in her "suppose to be random" hashed key that a developer did. Doh!
我之前的回答说要使用 GUID。我现在正在编辑它以建议每个人使用随机生成的哈希,例如使用RNGCryptoServiceProvider
. 并且,确保从哈希中消除任何“真实单词”。我记得早上 6 点的一个特殊电话,一位女士在她的“假设是随机的”散列密钥中收到了某个“c”字,这是开发人员所做的。哦!
Entire procedure
整个过程
- User clicks "reset" password.
- User is asked for an email.
- User enters email and clicks send. Do not confirm or deny the email as this is bad practice as well. Simply say, "We have sent a password reset request if the email is verified." or something cryptic alike.
- You create a hash from the
RNGCryptoServiceProvider
, store it as a separate entity in anut_UserPasswordRequests
table and link back to the user. So this so you can track old requests and inform the user that older links has expired. - Send the link to the email.
- 用户点击“重置”密码。
- 要求用户提供电子邮件。
- 用户输入电子邮件并单击发送。不要确认或否认电子邮件,因为这也是不好的做法。只需说,“如果电子邮件经过验证,我们已发送密码重置请求。” 或类似的神秘事物。
- 您从 中创建一个散列
RNGCryptoServiceProvider
,将其作为单独的实体存储在ut_UserPasswordRequests
表中并链接回用户。因此,您可以跟踪旧请求并通知用户旧链接已过期。 - 将链接发送到电子邮件。
User gets the link, like http://domain.com/User/PasswordReset/xjdk2ms92
, and clicks it.
用户获取链接,例如http://domain.com/User/PasswordReset/xjdk2ms92
,然后单击它。
If the link is verified, you ask for a new password. Simple, and the user gets to set their own password. Or, set your own cryptic password here and inform them of their new password here (and email it to them).
如果链接经过验证,您会要求输入新密码。很简单,用户可以设置自己的密码。或者,在此处设置您自己的神秘密码,并在此处通知他们新密码(并通过电子邮件发送给他们)。
回答by Ogglas
I think Microsoft guide for ASP.NET Identity is a good start.
我认为微软的 ASP.NET Identity 指南是一个好的开始。
Code that I use for ASP.NET Identity:
我用于 ASP.NET Identity 的代码:
Web.Config:
网络配置:
<add key="AllowedHosts" value="example.com,example2.com" />
AccountController.cs:
账户控制器.cs:
[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; }
}