Как реализовать сброс пароля?

Я работаю над приложением в ASP.NET, и было интересно, как конкретно я могу реализовать Password Reset функция, если я хотел свернуть свой собственный.

в частности, у меня есть следующие вопросы:

  • каков хороший способ создания уникального идентификатора, который трудно взломать?
  • должен ли быть таймер, прикрепленный к нему? Если да, то как долго это должно продолжаться?
  • должен ли я записывать IP-адрес? Разве это имеет значение?
  • что информацию следует запросить под экраном "сброс пароля"? Просто адрес электронной почты? Или, может быть, адрес электронной почты плюс некоторая информация, которую они "знают"? (Любимая команда, имя щенка и т. д.)

есть ли другие соображения, о которых мне нужно знать?

NB: другие вопросы есть умалчивается техническая реализация полностью. Действительно, принятый ответ затушевывает кровавые детали. Я надеюсь что этот вопрос и последующие ответы войдут в кровавые подробности, и я надеюсь, сформулировав этот вопрос гораздо более узко, что ответы будут менее "пушистыми" и более "кровавыми".

редактировать: ответы, которые также касаются того, как такая таблица будет моделироваться и обрабатываться в SQL Server или любом ASP.NET ссылки MVC на ответ будут оценены.

7 ответов


много хороших ответов здесь, я не буду повторять все это...

за исключением одного вопроса, который повторяется почти каждый ответ здесь, хотя его не так:

GUID (реалистично) уникальны и статистически невозможно угадать.

Это неверно, GUID - очень слабые идентификаторы и должны не используется для доступа к учетной записи пользователя.
Если вы изучите структуру, вы получите в общей сложности 128 биты в большинстве... что в наши дни не считается большим достижением.
Из которых первая половина является типичным инвариантом (для генерирующей системы), а половина того, что осталось, зависит от времени (или что-то еще подобное).
В целом, его очень слабый и легко bruteforced механизм.

Так что не используйте это!

вместо этого просто используйте криптографически сильный генератор случайных чисел (System.Security.Cryptography.RNGCryptoServiceProvider), и получить по крайней мере 256 бит сырой энтропии.

все остальное, как многочисленные другие ответы.


EDIT 2012/05/22: в дополнение к этому популярному ответу я больше не использую GUIDs в этой процедуре. Как и другой популярный ответ, я теперь использую свой собственный алгоритм хэширования для генерации ключа для отправки URL. Это имеет преимущество быть короче. Загляни в систему.Безопасность.Криптография для их генерации, которую я обычно использую и соль.

во-первых, не сразу сбросить пароль пользователя.

во-первых, не сразу сбрасывается пользователя пароль, когда они запрашивают его. Это нарушение безопасности, поскольку кто-то может угадать адреса электронной почты (т. е. ваш адрес электронной почты в компании) и сбросить пароли по прихоти. Рекомендации в эти дни обычно включают ссылку "подтверждение", отправленную на адрес электронной почты пользователя, подтверждая, что они хотят сбросить его. По этой ссылке вы хотите отправить уникальную ключевую ссылку. Я отправляю свой со ссылкой вроде:domain.com/User/PasswordReset/xjdk2ms92

да, установите тайм-аут по ссылке и сохраните ключ и тайм-аут на своем бэкэнде (и соль, если вы используете один). Тайм-ауты 3 дней-это норма, и обязательно уведомляйте пользователя о 3 днях на веб-уровне, когда они запрашивают сброс.

используйте уникальный хэш-ключ

мой предыдущий ответ сказал использовать GUID. Теперь я редактирую это, чтобы посоветовать всем использовать случайно сгенерированный хэш, например, используя RNGCryptoServiceProvider. И, убедитесь, чтобы исключить любые "реальные слова" из хэша. Я помню специальный телефонный звонок в 6 утра, где женщина получила определенное слово "c" в ней "предположим, что это случайный" хэшированный ключ, который сделал разработчик. Дох!

вся процедура

  • пользователь нажимает кнопку "Сброс" пароля.
  • пользователь по электронной почте.
  • пользователь вводит электронную почту и нажимает отправить. Не подтверждайте и не отрицайте электронную почту, так как это плохая практика. Просто скажите :" мы отправили запрос на сброс пароля, если письмо проверено.- или что-то загадочное.
  • вы создаете хэш из RNGCryptoServiceProvider, хранить его как отдельная сущность в ut_UserPasswordRequests таблица и ссылка обратно к пользователю. Таким образом, вы можете отслеживать старые запросы и сообщать пользователю, что старые ссылки истекли.
  • отправить ссылку на электронную почту.

пользователь получает ссылку, как http://domain.com/User/PasswordReset/xjdk2ms92 , и нажмет на него.

если ссылка проверена, вы запрашиваете новый пароль. Просто, и пользователь получает, чтобы установить свой собственный пароль. Или установите свой собственный зашифрованный пароль здесь и сообщите им о своем новом пароле здесь (и отправьте его по электронной почте им).


во-первых, мы должны знать, что вы уже знаете о пользователе. Очевидно, у вас есть имя пользователя и старый пароль. Что еще ты знаешь? У вас есть адрес электронной почты? У вас есть данные о любимом цветке пользователя?

предполагая, что у вас есть имя пользователя, пароль и рабочий адрес электронной почты, вам нужно добавить два поля в таблицу пользователя (предполагая, что это таблица базы данных): дату с именем 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=>

Если эта ссылка не работает, вы можете перейти кhttp://example.com/yourscript.lang и введите в форму следующее:

Если вы не запрашивали сброс пароля, вы можете игнорировать это сообщение.

спасибо, бла-бла-бла

теперь, кодирование yourscript.Lang: этому сценарию нужна форма. Если обновление var прошло по URL-адресу, форма просто запрашивает имя пользователя и адрес электронной почты. Если обновление не прошло, он запрашивает имя пользователя, адрес электронной почты и id-код, отправленный по электронной почте. Вы также просите новый пароль (дважды, конечно).

чтобы проверить новый пароль пользователя, убедитесь, что имя пользователя, адрес электронной почты и id-код совпадают, что срок действия запроса не истек и что два новых пароля совпадают. В случае успеха измените пароль пользователя на новый пароль и очистите поля сброса пароля в таблице пользователь. Также не забудьте выйти из системы / очистить любой логин, связанный cookies и перенаправить пользователя на страницу входа в систему.

по сути, поле new_passwd_id-это пароль, который работает только на странице сброса пароля.

одно потенциальное улучшение: вы можете удалить из электронной почты. "Кто-то запросил сброс пароля для учетной записи на этот адрес электронной почты...."Таким образом, имя пользователя что-то только пользователь знает, если электронная почта перехвачена. Я не начинал так, потому что если кто-то атакует учетную запись, они уже знать имя пользователя. Эта добавленная неясность останавливает атаки человека-в-середине возможности в случае, если кто-то злонамеренный случается перехватить электронную почту.

что касается ваших вопросов:

генерация случайной строки: она не должна быть чрезвычайно случайной. Достаточно любого генератора GUID или даже md5(concat(salt,current_timestamp ())), где salt-это что-то в записи пользователя, например, была создана учетная запись timestamp. Это должно быть что-то, что пользователь не может видеть.

таймер: Да, вам нужно это, чтобы сохранить вашу базу данных в здравом уме. Не более недели действительно необходимо, но по крайней мере 2 дня, так как вы никогда не знаете, как долго может длиться задержка электронной почты.

IP-адрес: поскольку электронная почта может быть задержана на несколько дней, IP-адрес полезен только для ведения журнала, а не для проверки. Если вы хотите зарегистрировать его, сделайте это, иначе он вам не понадобится.

сброс экрана: см. выше.

надеюсь, что это покрывает его. Удача.


идентификатор GUID, отправленный на адрес электронной почты записи, вероятно, достаточно для большинства запущенных приложений-с таймаутом еще лучше.

В конце концов, если emailbox пользователей был скомпрометирован(т. е. хакер имеет логин/пароль для адреса электронной почты), вы не так много можете сделать об этом.


вы можете отправить электронное письмо пользователю со ссылкой. Эта ссылка будет содержать некоторую трудно угадываемую строку (например, GUID). На стороне сервера вы также сохраните ту же строку, что и отправленную пользователю. Теперь, когда пользователь нажимает на ссылку, вы можете найти в своей записи БД с той же секретной строкой и сбросить пароль.


1) для генерации уникального идентификатора вы можете использовать безопасный алгоритм хэша. 2) таймер прилагается? Ты имел в виду истечения дуо ссылку сбросить? Да, у вас может быть срок действия 3) Вы можете запросить дополнительную информацию, отличную от emailid для проверки.. Как дата рождения или некоторые вопросы безопасности 4) вы также можете генерировать случайные символы и просить ввести их вместе с запрос.. чтобы убедиться, что запрос пароля не автоматизирован некоторыми шпионскими программами или такими вещами, как что..


Я думаю, что руководство Microsoft для ASP.NET идентичность-хорошее начало.

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; }
}