我正在使用ASP.NET中的一个应用程序,并且特别想知道如果我想自己动手,我将如何实现Password Reset
函数。
具体来说,我有以下问题:
我需要注意哪些其他注意事项?
NB :Other questions完全有glossed over技术实施。事实上,接受的答案掩盖了血腥的细节。我希望这个问题和随后的答案能够进入血腥的细节,我希望通过更加狭隘地解释这个问题,答案不是“绒毛”而是“血腥”。
编辑:这些答案也会介绍如何在SQL Server中建模和处理这样的表格或任何ASP.NET MVC链接到答案。
答案 0 :(得分:67)
编辑2012/05/22:作为这个流行答案的后续内容,我不再在此程序中使用GUID。与其他流行的答案一样,我现在使用自己的哈希算法生成要在URL中发送的密钥。这具有更短的优点。查看System.Security.Cryptography以生成它们,我通常也使用SALT。
首先,请勿在请求时立即重置用户密码。这是一个安全漏洞,因为有人可以猜到电子邮件地址(即您在公司的电子邮件地址)并随意重置密码。这些天的最佳做法通常包括发送到用户电子邮件地址的“确认”链接,确认他们想要重置它。此链接是您要发送唯一密钥链接的位置。我发送的链接如下:domain.com/User/PasswordReset/xjdk2ms92
是的,在链接上设置超时并在后端存储密钥和超时(如果使用的话,则为salt)。超时3天是常态,并确保在用户请求重置时在网络级别通知用户3天。
我之前的回答说要使用GUID。我现在正在编辑它以建议每个人使用随机生成的哈希,例如使用RNGCryptoServiceProvider
。并且,确保从哈希中消除任何“真实的单词”。我记得一个特殊的早上6点的电话,其中一位女士在她的“假设是随机的”字样中收到了某个“c”字,这是开发人员所做的。卫生署!
RNGCryptoServiceProvider
创建哈希,将其作为单独的实体存储在ut_UserPasswordRequests
表中,并链接回用户。因此,您可以跟踪旧请求并通知用户旧链接已过期。用户获取http://domain.com/User/PasswordReset/xjdk2ms92
之类的链接,然后点击它。
如果验证链接,则要求输入新密码。很简单,用户可以设置自己的密码。或者,在此设置您自己的神秘密码,并在此处告知他们的新密码(并通过电子邮件发送给他们)。
答案 1 :(得分:66)
这里有很多好的答案,我不打算重复这一切...
除了一个问题,这里几乎每个答案都重复这个问题,即使它错了:
Guids(实际上)是唯一的,统计上无法猜测。
不是这样,GUID是非常弱的标识符,应该 NOT 用于允许访问用户的帐户。
如果你检查一下这个结构,你最多总共得到128位......现在不太考虑了
其中前半部分是典型的不变量(对于发电系统),剩下的一半是时间依赖的(或类似的东西)。
总而言之,它是一种非常脆弱且易于强制的机制。
所以不要使用它!
相反,只需使用加密强大的随机数生成器(System.Security.Cryptography.RNGCryptoServiceProvider
),并获得至少256位的原始熵。
所有其他的,正如众多其他答案所提供的那样。
答案 2 :(得分:8)
首先,我们需要知道您对用户的了解。显然,你有一个用户名和一个旧密码。你还知道什么?你有电子邮件地址吗?你有关于用户最喜欢的花的数据吗?
假设您有用户名,密码和工作电子邮件地址,则需要在用户表中添加两个字段(假设它是数据库表):名为new_passwd_expire的日期和字符串new_passwd_id。
假设您拥有用户的电子邮件地址,当有人请求重置密码时,您可以按如下方式更新用户表:
new_passwd_expire = now() + some number of days
new_passwd_id = some random string of characters (see below)
接下来,您通过该地址向用户发送电子邮件:
亲爱的某某
有人请求了用户帐户的新密码< username> at<您的网站名称>。如果您确实要求重置密码,请点击以下链接:
http://example.com/yourscript.lang?update=<new_password_id&GT;
如果该链接无效,您可以转到http://example.com/yourscript.lang并在表单中输入以下内容:&lt; new_password_id&gt;
如果您未请求重置密码,则可以忽略此电子邮件。
谢谢,yada yada
现在,编码yourscript.lang:这个脚本需要一个表单。如果在URL上传递了var更新,则表单只询问用户的用户名和电子邮件地址。如果未通过更新,则会询问用户名,电子邮件地址以及电子邮件中发送的ID代码。您还需要一个新密码(当然是两次)。
要验证用户的新密码,请验证用户名,电子邮件地址和所有匹配的ID代码,请求未过期,以及两个新密码是否匹配。如果成功,则将用户密码更改为新密码,并清除用户表中的密码重置字段。还要确保将用户注销/清除任何与登录相关的cookie,并将用户重定向到登录页面。
实际上,new_passwd_id字段是一个仅适用于密码重置页面的密码。
一个潜在的改进:你可以删除&lt; username&gt;从电子邮件。 “有人要求在此电子邮件地址为帐户重置密码....”因此,只有当用户知道电子邮件是否被拦截时,用户名才会知道。我没有这样开始,因为如果有人攻击该帐户,他们已经知道用户名。如果有人恶意拦截电子邮件,这种增加的模糊性会阻止机会中间人的攻击。</ p>
关于你的问题:
生成随机字符串:它不需要非常随机。任何GUID生成器甚至md5(concat(salt,current_timestamp()))都足够了,其中salt是用户记录上的内容,如创建时间戳帐户。它必须是用户看不到的东西。
计时器:是的,你需要这个只是为了让你的数据库保持理智。不超过一周是必要的,但至少2天,因为你永远不知道电子邮件延迟可能会持续多长时间。
IP地址:由于电子邮件可能会延迟数天,因此IP地址仅用于记录,而不用于验证。如果要记录它,请执行此操作,否则您不需要它。
重置屏幕:见上文。
希望涵盖它。祝你好运。
答案 3 :(得分:3)
发送到记录电子邮件地址的GUID对于大多数普通应用程序来说已经足够了 - 超时甚至更好。
毕竟,如果用户的电子邮箱遭到入侵(即黑客拥有该电子邮件地址的登录名/密码),那么您无能为力。
答案 4 :(得分:2)
您可以通过链接向用户发送电子邮件。此链接将包含一些难以猜测的字符串(如GUID)。在服务器端,您还将存储发送给用户的相同字符串。现在,当用户按下链接时,您可以在db条目中找到具有相同密码的字符串并重置其密码。
答案 5 :(得分:2)
1)为了生成唯一ID,您可以使用安全散列算法。 2)计时器附加?你的意思是重置密码链接到期了吗? 是的,您可以设置到期日 3)除了emailId之外,你可以要求更多信息来验证.. 像出生日期或一些安全问题 4)你也可以生成随机字符,并要求输入随机字符 请求..以确保密码请求不会被某些间谍软件或类似的东西自动化。
答案 6 :(得分:0)
我认为Microsoft ASP.NET Identity指南是一个好的开始。
我用于ASP.NET身份验证的代码:
Web.Config:
<add key="AllowedHosts" value="example.com,example2.com" />
AccountController.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; }
}