gpt4 book ai didi

c# - 如何创建密码重置链接?

转载 作者:太空狗 更新时间:2023-10-29 19:52:00 25 4
gpt4 key购买 nike

您建议使用哪种方式在 MVCC# 中创建一个安全 密码重置链接?我的意思是,我会创建一个随机 token ,对吧?在发送给用户之前如何对其进行编码? MD5 是否足够好?您知道其他安全方法吗?

最佳答案

I mean, I'll create a random token, right?

有两种方法:

  • 使用加密安全的随机字节序列,这些字节被保存到数据库(也可以选择散列)并通过电子邮件发送给用户。
    • 这种方法的缺点是您需要扩展您的数据库设计(架构)以拥有一个列来存储此数据。您还应该存储生成字节的 UTC 日期和时间,以使密码重置代码过期。
    • 另一个缺点(或优点)是用户最多只能有 1 个待处理的密码重置。
  • 使用私钥签名a HMAC包含重置用户密码所需的最少详细信息的消息,并且此消息还可以包含到期日期和时间。
    • 这种方法无需在数据库中存储任何内容,但也意味着您无法撤销任何有效生成的密码重置代码,这就是使用较短的到期时间(我估计大约 5 分钟)很重要的原因。
    • 可以将撤销信息存储在数据库中(以及防止多次挂起的密码重置),但这会消除已签名 HMAC 的无状态特性的所有优势用于身份验证。

方法 1:加密安全的随 secret 码重置代码

  • 使用 System.Security.Cryptography.RandomNumberGenerator 这是一个加密安全的 RNG。
    • 不要使用 System.Random ,它不是加密安全的。
    • 使用它来生成随机字节,然后将这些字节转换为人类可读的字符,这些字符将在电子邮件中保留下来并被复制和粘贴(即通过使用 Base16 或 Base64 编码)。
  • 然后存储那些相同的随机字节(或它们的散列,尽管这对安全性没有多大帮助)。
    • 只需在电子邮件中包含该 Base16 或 Base64 字符串即可。
    • 可以在电子邮件中有一个包含查询字符串中的密码重置代码的可点击链接,但是这样做违反了 HTTP 关于 GET 的准则。请求应该能够(因为点击链接总是一个 GET 请求,但是 GET 请求不应该导致持久数据的状态改变,只有 POSTPUTPATCH 请求应该这样做- 这需要让用户手动复制代码并提交 POST 网络表单 - 这不是最好的用户体验。
      • 实际上,更好的方法是让该链接打开一个页面,其中包含查询字符串中的密码重置代码,然后该页面仍然有一个 <form method="POST">。但它是提交用户的新密码,而不是为他们预先生成一个新密码——因此不违反 HTTP 的指导方针,因为在最终 POST 之前不会更改状态。使用新密码。

像这样:

  1. 扩展您的数据库' Users包含密码重置代码列的表格:

    ALTER TABLE dbo.Users ADD
    PasswordResetCode binary(12) NULL,
    PasswordResetStart datetime2(7) NULL;
  2. 在您的 Web 应用程序代码中执行如下操作:

    [HttpGet]
    [HttpHead]
    public IActionResult GetPasswordResetForm()
    {
    // Return a <form> allowing the user to confirm they want to reset their password, which POSTs to the action below.
    }

    static readonly TimeSpan _passwordResetExpiry = TimeSpan.FromMinutes( 5 );

    [HttpPost]
    public IActionResult SendPasswordResetCode()
    {
    // 1. Get a cryptographically secure random number:
    // using System.Security.Cryptography;

    Byte[] bytes;
    String bytesBase64Url; // NOTE: This is Base64Url-encoded, not Base64-encoded, so it is safe to use this in a URL, but be sure to convert it to Base64 first when decoding it.
    using( RandomNumberGenerator rng = new RandomNumberGenerator() ) {

    bytes = new Byte[12]; // Use a multiple of 3 (e.g. 3, 6, 12) to prevent output with trailing padding '=' characters in Base64).
    rng.GetBytes( bytes );

    // The `.Replace()` methods convert the Base64 string returned from `ToBase64String` to Base64Url.
    bytesBase64Url = Convert.ToBase64String( bytes ).Replace( '+', '-' ).Replace( '/', '_' );
    }

    // 2. Update the user's database row:
    using( SqlConnection c = new SqlConnection( CONNECTION_STRING ) )
    using( SqlCommand cmd = c.CreateCommand() )
    {
    cmd.CommandText = "UPDATE dbo.Users SET PasswordResetCode = @code, PasswordResetStart = SYSUTCDATETIME() WHERE UserId = @userId";

    SqlParameter pCode = cmd.Parameters.Add( cmd.CreateParameter() );
    pCode.ParameterName = "@code";
    pCode.SqlDbType = SqlDbType.Binary;
    pCode.Value = bytes;

    SqlParameter pUserId = cmd.Parameters.Add( cmd.CreateParameter() );
    pCode.ParameterName = "@userId";
    pCode.SqlDbType = SqlDbType.Int;
    pCode.Value = userId;

    cmd.ExecuteNonQuery();
    }

    // 3. Send the email:
    {
    const String fmt = @"Greetings {0},
    I am Ziltoid... the omniscient.
    I have come from far across the omniverse.
    You shall fetch me your universe's ultimate cup of coffee... uh... I mean, you can reset your password at {1}
    You have {2:N0} Earth minutes,
    Make it perfect!";

    // e.g. "https://example.com/ResetPassword/123/ABCDEF"
    String link = "https://example.com/" + this.Url.Action(
    controller: nameof(PasswordResetController),
    action: nameof(this.ResetPassword),
    params: new { userId = userId, codeBase64 = bytesBase64Url }
    );

    String body = String.Format( CultureInfo.InvariantCulture, fmt, userName, link, _passwordResetExpiry.TotalMinutes );

    this.emailService.SendEmail( user.Email, subject: "Password reset link", body );
    }

    }

    [HttpGet( "/PasswordReset/ResetPassword/{userId}/{codeBase64Url}" )]
    public IActionResult ResetPassword( Int32 userId, String codeBase64Url )
    {
    // Lookup the user and see if they have a password reset pending that also matches the code:

    String codeBase64 = codeBase64Url.Replace( '-', '+' ).Replace( '_', '/' );
    Byte[] providedCode = Convert.FromBase64String( codeBase64 );
    if( providedCode.Length != 12 ) return this.BadRequest( "Invalid code." );

    using( SqlConnection c = new SqlConnection( CONNECTION_STRING ) )
    using( SqlCommand cmd = c.CreateCommand() )
    {
    cmd.CommandText = "SELECT UserId, PasswordResetCode, PasswordResetStart FROM dbo.Users SET WHERE UserId = @userId";

    SqlParameter pUserId = cmd.Parameters.Add( cmd.CreateParameter() );
    pCode.ParameterName = "@userId";
    pCode.SqlDbType = SqlDbType.Int;
    pCode.Value = userId;

    using( SqlDataReader rdr = cmd.ExecuteReader() )
    {
    if( !rdr.Read() )
    {
    // UserId doesn't exist in the database.
    return this.NotFound( "The UserId is invalid." );
    }

    if( rdr.IsDBNull( 1 ) || rdr.IsDBNull( 2 ) )
    {
    return this.Conflict( "There is no pending password reset." );
    }

    Byte[] expectedCode = rdr.GetBytes( 1 );
    DateTime? start = rdr.GetDateTime( 2 );

    if( !Enumerable.SequenceEqual( providedCode, expectedCode ) )
    {
    return this.BadRequest( "Incorrect code." );
    }

    // Now return a new form (with the same password reset code) which allows the user to POST their new desired password to the `SetNewPassword` action` below.
    }
    }

    [HttpPost( "/PasswordReset/ResetPassword/{userId}/{codeBase64}" )]
    public IActionResult SetNewPassword( Int32 userId, String codeBase64, [FromForm] String newPassword, [FromForm] String confirmNewPassword )
    {
    // 1. Use the same code as above to verify `userId` and `codeBase64`, and that `PasswordResetStart` was less than 5 minutes (or `_passwordResetExpiry`) ago.
    // 2. Validate that `newPassword` and `confirmNewPassword` are the same.
    // 3. Reset `dbo.Users.Password` by hashing `newPassword`, and clear `PasswordResetCode` and `PasswordResetStart`
    // 4. Send the user a confirmatory e-mail informing them that their password was reset, consider including the current request's IP address and user-agent info in that e-mail message as well.
    // 5. And then perform a HTTP 303 redirect to the login page - or issue a new session token cookie and redirect them to the home-page.
    }
    }

方法二:HMAC代码

这种方法不需要更改您的数据库,也不需要保留新状态,但它确实需要您了解 HMAC 的工作原理。

基本上它是一个简短的结构化消息(而不是随机不可预测的字节),其中包含足够的信息以允许系统识别其密码应该被重置的用户,包括到期时间戳 - 以防止伪造此消息是加密签名的只有您的应用程序代码知道的私钥:这可以防止攻击者生成他们自己的密码重置代码(这显然不好!)。

以下是生成用于密码重置的 HMAC 代码的方法,以及验证代码的方法:

private static readonly Byte[] _privateKey = new Byte[] { 0xDE, 0xAD, 0xBE, 0xEF }; // NOTE: You should use a private-key that's a LOT longer than just 4 bytes.
private static readonly TimeSpan _passwordResetExpiry = TimeSpan.FromMinutes( 5 );
private const Byte _version = 1; // Increment this whenever the structure of the message changes.

public static String CreatePasswordResetHmacCode( Int32 userId )
{
Byte[] message = Enumerable.Empty<Byte>()
.Append( _version )
.Concat( BitConverter.GetBytes( userId ) )
.Concat( BitConverter.GetBytes( DateTime.UtcNow.ToBinary() ) )
.ToArray();

using( HMACSHA256 hmacSha256 = new HMACSHA256( key: _privateKey ) )
{
Byte[] hash = hmacSha256.ComputeHash( buffer: message, offset: 0, count: message.Length );

Byte[] outputMessage = message.Concat( hash ).ToArray();
String outputCodeB64 = Convert.ToBase64( outputMessage );
String outputCode = outputCodeB64.Replace( '+', '-' ).Replace( '/', '_' );
return outputCode;
}
}

public static Boolean VerifyPasswordResetHmacCode( String codeBase64Url, out Int32 userId )
{
String base64 = codeBase64Url.Replace( '-', '+' ).Replace( '_', '/' );
Byte[] message = Convert.FromBase64String( base64 );

Byte version = message[0];
if( version < _version ) return false;

userId = BitConverter.ToInt32( message, startIndex: 1 ); // Reads bytes message[1,2,3,4]
Int64 createdUtcBinary = BitConverter.ToInt64( message, startIndex: 1 + sizeof(Int32) ); // Reads bytes message[5,6,7,8,9,10,11,12]

DateTime createdUtc = DateTime.FromBinary( createdUtcBinary );
if( createdUtc.Add( _passwordResetExpiry ) < DateTime.UtcNow ) return false;

const Int32 _messageLength = 1 + sizeof(Int32) + sizeof(Int64); // 1 + 4 + 8 == 13

using( HMACSHA256 hmacSha256 = new HMACSHA256( key: _privateKey ) )
{
Byte[] hash = hmacSha256.ComputeHash( message, offset: 0, count: _messageLength );

Byte[] messageHash = message.Skip( _messageLength ).ToArray();
return Enumerable.SequenceEquals( hash, messageHash );
}
}

这样使用:


// Note there is no `UserId` URL parameter anymore because it's embedded in `code`:

[HttpGet( "/PasswordReset/ResetPassword/{codeBase64Url}" )]
public IActionResult ConfirmResetPassword( String codeBase64Url )
{
if( !VerifyPasswordResetHmacCode( codeBase64Url, out Int32 userId ) )
{
// Message is invalid, such as the HMAC hash being incorrect, or the code has expired.
return this.BadRequest( "Invalid, tampered, or expired code used." );
}
else
{
// Return a web-page with a <form> to POST the code.
// Render the `codeBase64Url` to an <input type="hidden" /> to avoid the user inadvertently altering it.
// Do not reset the user's password in a GET request because GET requests must be "safe". If you send a password-reset link by SMS text message or even by email, then software bot (like link-preview generators) may follow the link and inadvertently reset the user's password!
}
}


[HttpPost( "/PasswordReset/ResetPassword" )]
public IActionResult ConfirmResetPassword( [FromForm] ConfirmResetPasswordForm model )
{
if( !VerifyPasswordResetHmacCode( model.CodeBase64Url, out Int32 userId ) )
{
return this.BadRequest( "Invalid, tampered, or expired code used." );
}
else
{
// Reset the user's password here.
}
}

关于c# - 如何创建密码重置链接?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/12574595/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com