如何实施密码重置?

Posted

技术标签:

【中文标题】如何实施密码重置?【英文标题】:How to Implement Password Resets? 【发布时间】:2010-10-14 11:04:00 【问题描述】:

我正在使用 ASP.NET 开发一个应用程序,并且想知道如果我想自己实现一个 Password Reset 函数,我该如何实现。

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

生成难以破解的唯一 ID 的好方法是什么? 是否应该附加一个计时器?如果是,应该多长时间? 我应该记录 IP 地址吗?这还重要吗? 我应该在“密码重置”屏幕下询问哪些信息?只是电子邮件地址?或者可能是电子邮件地址加上他们“知道”的一些信息? (最喜欢的球队、小狗的名字等)

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

NB:Other questions 完全拥有glossed over 技术实现。事实上,公认的答案掩盖了血腥的细节。我希望这个问题和随后的答案会涉及血腥的细节,我希望通过更狭隘地表述这个问题,答案是更少的“绒毛”和更多的“血腥”。

编辑:如果回答还涉及如何在 SQL Server 中建模和处理此类表或任何指向答案的 ASP.NET MVC 链接,我们将不胜感激。

【问题讨论】:

ASP.NET MVC 使用默认的 ASP.NET 身份验证提供程序,因此您找到的任何代码示例都应该与您的目的相关。 【参考方案1】:

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

【讨论】:

更多细节会有所帮助。【参考方案2】:

发送到记录的电子邮件地址的 GUID 可能足以满足大多数普通应用程序的需要 - 超时甚至更好。

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

【讨论】:

【参考方案3】:

首先,我们需要了解您对用户的了解。显然,您有一个用户名和一个旧密码。你还知道什么?你有电子邮件地址吗?你有关于用户最喜欢的花的数据吗?

假设您有一个用户名、密码和工作电子邮件地址,您需要向您的用户表(假设它是一个数据库表)添加两个字段:一个名为 new_passwd_expire 的日期和一个字符串 new_passwd_id。

假设您有用户的电子邮件地址,当有人请求重置密码时,您更新用户表如下:

new_passwd_expire = now() + some number of days
new_passwd_id = some random string of characters (see below)

接下来,您向该地址的用户发送一封电子邮件:

亲爱的某某

有人在 处请求用户帐户 的新密码。如果您确实要求重设密码,请点击此链接:

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

如果该链接不起作用,您可以转到 http://example.com/yourscript.lang 并在表单中输入以下内容:

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

谢谢,亚达亚达

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

要验证用户的新密码,您需要验证用户名、电子邮件地址和 id 代码都匹配,请求未过期,并且两个新密码匹配。如果成功,您将用户密码更改为新密码并从用户表中清除密码重置字段。还要确保将用户注销/清除任何与登录相关的 cookie 并将用户重定向到登录页面。

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

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

至于你的问题:

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

timer:是的,你需要这个只是为了让你的数据库保持健全。不超过一周确实是必要的,但至少需要 2 天,因为您永远不知道电子邮件延迟会持续多长时间。

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

重置屏幕:见上文。

希望涵盖它。祝你好运。

【讨论】:

难道潜在的攻击者不能使用当前日期戳的 MD5 进入吗? 我强烈建议不要通过网络在电子邮件中发送密码。大多数用户未删除这些电子邮件,这是一个安全漏洞——他们中的一些人希望每次都从他们“最喜欢的”电子邮件中复制粘贴。用户公司邮件服务器证书过期,流量被嗅探怎么办?为了尽量减少这种可能的违规行为,请 (1) 设置此特定密码的较短到期时间 - 1 小时,以及 (2) 强制用户在下次登录时对其进行更新。 Ognyan,电子邮件中发送的密码只能使用一次。他们必须在登录后更改密码,并且电子邮件不包含用户登录名。所以,不,他们不能每次都复制粘贴。不删除电子邮件不是安全问题,因为它只是一串无意义的字母/数字,重置密码后攻击者将一无所获。【参考方案4】:

1) 要生成唯一 ID,您可以使用安全哈希算法。 2) 附定时器?您的意思是重置密码链接的到期吗? 是的,你可以有一个到期集 3)您可以要求提供除 emailId 之外的更多信息来验证.. 比如出生日期或一些安全问题 4)您还可以生成随机字符并要求输入 request.. 以确保密码请求不会被某些间谍软件或类似的东西自动化..

【讨论】:

【参考方案5】:

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

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

首先,当用户请求密码时,不要立即重置。这是一个安全漏洞,因为有人可能会猜测电子邮件地址(即您在公司的电子邮件地址)并随心所欲地重置密码。这些天的最佳实践通常包括发送到用户电子邮件地址的“确认”链接,确认他们想要重置它。此链接是您要发送唯一密钥链接的位置。我发送我的链接如下:domain.com/User/PasswordReset/xjdk2ms92

是的,在链接上设置一个超时并将密钥和超时存储在您的后端(如果您使用的是盐,则存储盐)。 3 天的超时是常态,请确保在用户请求重置时在 Web 级别通知用户 3 天。

使用唯一的哈希键

我之前的回答说要使用 GUID。我现在正在编辑它以建议每个人使用随机生成的哈希,例如使用RNGCryptoServiceProvider。并且,确保从散列中消除任何“真实单词”。我记得早上 6 点的一个特殊电话,一位女士在她的“假设是随机的”散列密钥中收到了某个“c”字,这是开发人员所做的。呵呵!

整个过程

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

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

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

【讨论】:

我在想,如果实际的用户密码经过哈希处理,为什么要生成一个新的哈希密钥?向用户发送一封电子邮件,其中包含通过哈希密码重置密码的链接是不正确的吗?哈希后的密码不可恢复,当用户点击链接时,服务器会收到哈希后的密码,与实际存储的密码进行比较,然后允许用户修改密码。 还有一个非常棒的地方,就是你不需要设置Timeout,一旦用户更改密码,旧链接将自动失效,因为散列密码存储在数据库中已更改。 @Daniel 这真是个坏主意。我认为您需要在 Google 上搜索“蛮力攻击”一词。此外,您确实希望它过期的原因是,如果某人的电子邮件在一年后被泄露(并且他们从未重置过),黑客将获得更改密码的权利。 @educan911。我知道暴力攻击,而且,要访问散列密钥,有恶意的人必须访问电子邮件,如果他可以访问,则无需还原散列密码。此外,为了使它几乎不可能,您可以散列散列密码,或者更好的是,用更多的东西散列密码。我不是不同意你,我只是想对此进行头脑风暴【参考方案6】:

这里有很多好的答案,我不会费心重复它......

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

Guids(实际上)是唯一的,并且在统计上是无法猜测的。

这不是真的,GUID 是非常弱的标识符,应该用于允许访问用户的帐户。 如果你检查结构,你最多可以得到 128 位......现在这并不多。 其中前半部分是典型的不变量(对于生成系统),剩下的一半是时间相关的(或其他类似的)。 总而言之,它是一个非常薄弱且容易被暴力破解的机制。

所以不要使用它!

相反,只需使用加密强的随机数生成器 (System.Security.Cryptography.RNGCryptoServiceProvider),并获得至少 256 位的原始熵。

所有其余的,正如提供的许多其他答案一样。

【讨论】:

完全同意,据我所知,GUID 从未设计为具有强大的加密功能且无法猜测。 说得好,AFAIK MSDN 明确指出不应将 GUID 用于安全性。 自 2000 年起在 Windows 中使用版本 4 UUID:How are .NET 4 GUIDs generated? - Stack Overflow。它们有 122 个随机位,我认为这符合 NIST 的建议。有一个非常严重的本地攻击漏洞,根据CryptGenRandom - Wikipedia,到 2008 年,该漏洞在 Vista 和 XP 中已修复。那么您在哪里发现当前使用 GUID 的问题? 那篇“Old New Thing”博客描述了已弃用的版本 1 UUID,并引用了一篇于 1998 年到期的 Internet 草案(你永远不应该做的事情),比博文早 10 年。我将来会对他们持怀疑态度。我们很久以前就打过那些仗,而且似乎赢得了大部分。我仍然同意对加密随机源使用干净的 API 调用要好得多,但在第 4 版的 GUID/UUID 上不要那么难。 对于它的价值,这并不能回答“如何重置密码”的问题。你刚刚吐槽了关于 GUID 的重要观点。【参考方案7】:

我认为 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; 

【讨论】:

以上是关于如何实施密码重置?的主要内容,如果未能解决你的问题,请参考以下文章

如何重置或破解Ubuntu 20.04的用户密码

如何重置或破解Ubuntu 20.04的用户密码

通过电子邮件发送临时密码重置密码

ADSelfService Plus自动密码重置和解锁帐户及密码管理的好处

无法为 Django 的重置密码流程创建集成测试

如何重置CentOS 7的Root密码