如何实施密码重置?


81

我正在开发ASP.NET中的应用程序,并且特别想知道Password Reset如果我想自己动手实现该功能。

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

  • 生成难以破解的唯一ID的好方法是什么?
  • 应该附有计时器吗?如果是这样,应该持续多久?
  • 我应该记录IP地址吗?有关系吗
  • 在“密码重置”屏幕下,我需要什么信息?只是电子邮件地址?还是电子邮件地址以及他们“知道”的一些信息?(最喜欢的团队,小狗的名字等)

我还有其他需要注意的事项吗?

注意其他问题完全掩盖了技术实施。实际上,公认的答案掩盖了血腥的细节。我希望这个问题和随后的答案会深入到细节中,我希望通过更狭义地表述这个问题,使答案不再那么“蓬松”,而更多的是“刺痛”。

编辑:答案也将进入如何在SQL Server或任何答案的ASP.NET MVC链接中建模和处理这种表的方式。


ASP.NET MVC使用默认的ASP.NET身份验证提供程序,因此您找到的任何代码示例都应与您的目的相关。
paulwhit

Answers:


66

这里有很多好的答案,我不会再重复一遍...

除了一个问题,这里的每个答案都会重复出现,即使它是错误的:

指导(实际上)是唯一的,从统计上讲不可能进行猜测。

这是不正确的,GUID是非常弱的标识符,应用于允许访问用户帐户。
如果您检查结构,则最多可获得128位……这在当今并不多见。
前半部分是典型的不变式(对于发电系统),剩下的一半是时间相关的(或其他类似的东西)。
总而言之,它是一个非常脆弱且容易被暴力破解的机制。

所以不要使用它!

取而代之的是,只需使用加密强度高的随机数生成器(System.Security.Cryptography.RNGCryptoServiceProvider),并获得至少256位的原始熵。

所有其他内容,以及提供的许多其他答案。


6
据我所知,绝对同意,GUID从未设计为具有强大的密码功能且无法猜测。
Jan Soltis 2009年

5
可以说,AFAIK MSDN明确指出不应将GUID用于安全性。
博士 邪恶的

2
从2000年开始在Windows中使用版本4 UUID:.NET 4 GUID是如何生成的?-堆栈溢出。它们中有122个随机位,我认为这符合NIST的建议。根据CryptGenRandom的说法,本地攻击存在一个非常严重的漏洞-维基百科已于2008年在Vista和XP中修复。您在哪里看到当前使用GUID的问题?
nealmcb 2011年

4
那个“ Old New Thing”博客描述的是不推荐使用的版本1 UUID,并引用了Internet草案(您永远不会做的事),该草案于1998年到期,距博客文章的发布时间已经十年了。将来我会怀疑他们。我们很久以前就打过这些仗,似乎赢得了大多数胜利。我仍然认为,用干净的API调用一个加密随机源是好多了,但不要这么辛苦GUID /在第4版的UUID
nealmcb

1
对于它的价值,它不能回答“如何重设密码”问题。您刚刚吐出了关于GUID的要点。
Rex Whitten 2014年

67

编辑2012/05/22:作为对此流行解答的后续,我不再在此过程中使用GUID。像其他流行的答案一样,我现在使用自己的哈希算法来生成要在URL中发送的密钥。这具有缩短的优点。查看System.Security.Cryptography生成它们,我通常也使用SALT。

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

首先,不要在用户要求密码时立即重设密码。这是一项安全漏洞,因为有人可能会猜出电子邮件地址(即您在公司的电子邮件地址)并一时重设密码。这些天的最佳做法通常包括发送到用户电子邮件地址的“确认”链接,以确认他们要重置该电子邮件地址。该链接是您要发送唯一键链接的位置。我给我的链接是这样的:domain.com/User/PasswordReset/xjdk2ms92

是的,在链接上设置超时,并将密钥和超时存储在后端(如果使用的话,则加盐)。超时通常为3天,请确保在用户要求网络级别的3天后通知用户。

使用唯一的哈希键

我以前的回答是使用GUID。我现在正在编辑此代码,以建议所有人使用随机生成的哈希,例如使用RNGCryptoServiceProvider。并且,请确保从哈希中消除所有“实词”。我记得早上6点钟打过一个特殊的电话,那是一个女人在她的“假设是随机的”散列密钥中收到某个开发人员所做的“ c”字。h!

整个程序

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

用户获得链接,如http://domain.com/User/PasswordReset/xjdk2ms92,然后单击它。

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


1
我很想知道,如果对实际的用户密码进行了哈希处理,为什么还要生成一个新的HASH密钥?向用户发送一封电子邮件,其中包含通过哈希密码重置密码的链接是否正确?哈希密码无法还原,当用户单击链接时,服务器将收到哈希密码,并与实际存储的密码进行比较,然后允许用户更改密码。
丹尼尔(Daniel)

另一个有趣的好处是,您无需设置超时,一旦用户更改了密码,旧链接将自动不再有效,因为存储在数据库中的哈希密码已更改。
丹尼尔(Daniel)

@Daniel这是一个非常糟糕的主意。我认为您需要使用Google术语“蛮力攻击”。另外,您希望它过期的原因是,如果某人的电子邮件在以后的一年中遭到入侵(并且他们从未重置过),那么黑客将获得更改密码的权利。
eduncan911

@ educan911。我知道暴力攻击,但是,要想访问哈希键(恶意目的者),就必须要访问电子邮件,如果他可以访问,则无需还原哈希密码。另外,要使其几乎不可能,您可以对哈希密码进行哈希处理,甚至更好,还可以对密码进行哈希处理。我不同意您的意见,我只是想就此进行头脑风暴
丹尼尔(Daniel)

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>的新密码。如果您确实要求重设密码,请点击以下链接:

http://example.com/yourscript.lang?update= < new_password_id >

如果该链接不起作用,则可以转到http://example.com/yourscript.lang,然后在表单中输入以下内容:<new_password_id>

如果您不要求重设密码,则可以忽略此电子邮件。

谢谢,yada yada

现在,编码yourscript.lang:此脚本需要一个表单。如果在URL上传递了var更新,则表单仅询问用户的用户名和电子邮件地址。如果未通过更新,它将询问用户名,电子邮件地址和电子邮件中发送的ID码。您还要求输入新密码(当然是两次)。

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

本质上,new_passwd_id字段是仅在密码重置页面上有效的密码。

一项潜在的改进:您可以从电子邮件中删除<用户名>。“有人请求为此电子邮件地址的帐户重设密码...。”因此,如果用户名被截获,则只有用户知道用户名。我并不是从那开始的,因为如果有人攻击了该帐户,他们已经知道用户名。这种增加的模糊性阻止了恶意的中间人攻击机会,以防有人恶意拦截电子邮件。

至于您的问题:

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

计时器:是的,您只需要保持数据库正常即可。真正需要的时间不超过一周,但至少需要两天,因为您不知道电子邮件延迟可能持续多长时间。

IP地址:由于电子邮件可能会延迟几天,因此IP地址仅用于日志记录,而不用于验证。如果要记录,请这样做,否则不需要它。

重置屏幕:请参见上文。

希望能掩盖它。祝好运。


潜在的攻击者无法使用当前日期戳的MD5进入吗?
George Stocker

我强烈建议您不要通过电子邮件发送密码。大多数用户会将这些电子邮件保留为未删除状态,这是一项安全漏洞-他们中的某些人每次都希望从“收藏夹”电子邮件中复制粘贴该电子邮件。如果用户公司邮件服务器的证书已过期并且流量被监听怎么办?为了最大程度地减少这种可能的破坏,是(1)设置此特定密码的有效期很短-1小时,并且(2)强制用户在下次登录时对其进行更新。
Ognyan Dimitrov 2014年

Ognyan,通过电子邮件发送的密码只能使用一次。他们必须在登录后更改密码,并且电子邮件中不包含用户登录名。因此,没有,他们不能每次都复制粘贴。不删除电子邮件不是安全问题,因为它只是无意义的字母/数字字符串,在重置密码后,攻击者将一无所获。
jmucchiello 2014年

3

发送到记录的电子邮件地址的GUID对于大多数常规应用程序可能已经足够-超时甚至更好。

毕竟,如果用户的电子邮件箱已被盗用(即黑客拥有电子邮件地址的登录名/密码),那么您将无能为力。


2

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


更多细节会有所帮助。
George Stocker

2

1)要生成唯一ID,您可以使用安全哈希算法。2)附有计时器吗?您是说重置密码链接过期吗?是的,您可以设置一个有效期3)您可以要求提供除emailId之外的其他更多信息以进行验证。。例如出生日期或一些安全性问题4)您还可以生成随机字符并要求将其与请求一起输入..以确保某些间谍软件或类似工具不会自动执行密码请求。


0

我认为Microsoft ASP.NET Identity指南是一个好的开始。

https://docs.microsoft.com/zh-cn/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; }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.