如何实施密码重置?

时间:2009-03-20 01:23:20

标签: c# asp.net asp.net-mvc security

我正在使用ASP.NET中的一个应用程序,并且特别想知道如果我想自己动手,我将如何实现Password Reset函数。

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

  • 生成难以破解的唯一ID的好方法是什么?
  • 是否应该连接定时器?如果是这样,它应该多久?
  • 我应该记录IP地址吗?它甚至重要吗?
  • 在“密码重置”屏幕下我应该要求哪些信息?只是电邮地址?或者也许是电子邮件地址加上他们“知道”的一些信息? (最喜欢的球队,小狗的名字等)

我需要注意哪些其他注意事项?

  

NB Other questions完全有glossed over技术实施。事实上,接受的答案掩盖了血腥的细节。我希望这个问题和随后的答案能够进入血腥的细节,我希望通过更加狭隘地解释这个问题,答案不是“绒毛”而是“血腥”。

编辑:这些答案也会介绍如何在SQL Server中建模和处理这样的表格或任何ASP.NET MVC链接到答案。

7 个答案:

答案 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指南是一个好的开始。

https://docs.microsoft.com/en-us/aspnet/identity/overview/features-api/account-confirmation-and-password-recovery-with-aspnet-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; }
}