ASP.NET身份验证重置密码

125

如何在新的ASP.NET身份验证系统中获取用户密码?或者在不知道当前密码的情况下重置密码(用户忘记密码)?

10个回答

179
如果您想使用UserManager更改密码,但又不想提供用户的当前密码,则可以生成密码重置令牌并立即使用它。
string resetToken = await UserManager.GeneratePasswordResetTokenAsync(model.Id);
IdentityResult passwordChangeResult = await UserManager.ResetPasswordAsync(model.Id, resetToken, model.NewPassword);

10
这绝对是设置新密码最好、最干净的方式。被认可的答案存在的问题是,通过直接访问密码哈希器来绕过密码复杂性验证。 - Chris
8
请注意,如果您使用上述逻辑,可能会出现“未注册IUserTokenProvider”的错误。请参阅此https://dev59.com/aGEh5IYBdhLWcg3wPhdT。 - Prasad Kanaparthi
1
这仅适用于Microsoft.AspNet.Identity的第2版,我想。在第1版中,您无法找到GeneratePasswordResetTokenAsync方法。 - romanoza
我不明白你如何提示用户输入新密码。如果不将新密码作为参数提供,你如何验证重置令牌?为什么没有单独的方法来验证重置令牌,以便可以在表单上提示用户输入新密码,然后进行重置? - Robert Noack
5
如果您收到“无效令牌(Invalid Token)”的提示,请确保您的用户的SecurityStamp不为null。这可能发生在从其他数据库迁移的用户或未通过UserManager.CreateAsync()方法创建的用户上。 - Alisson Reinaldo Silva
@Alisson 我正在使用MongoDB,并以不同的方式创建用户,...我该如何使用令牌提供程序...重置密码对我来说失败了,而且似乎只有你指出了我的问题。 - Hassan Faghihi

117

当前版本

假设您已经处理了重置忘记密码请求的验证,使用以下代码作为示例代码步骤。

ApplicationDbContext =new ApplicationDbContext()
String userId = "<YourLogicAssignsRequestedUserId>";
String newPassword = "<PasswordAsTypedByUser>";
ApplicationUser cUser = UserManager.FindById(userId);
String hashedNewPassword = UserManager.PasswordHasher.HashPassword(newPassword);
UserStore<ApplicationUser> store = new UserStore<ApplicationUser>();            
store.SetPasswordHashAsync(cUser, hashedNewPassword);

在 AspNet Nightly Build 中

框架已更新以使用 Token 处理请求,例如忘记密码。一旦发布,预计会提供简单的代码指导。

更新:

此更新仅提供更明确的步骤。

ApplicationDbContext context = new ApplicationDbContext();
UserStore<ApplicationUser> store = new UserStore<ApplicationUser>(context);
UserManager<ApplicationUser> UserManager = new UserManager<ApplicationUser>(store);
String userId = User.Identity.GetUserId();//"<YourLogicAssignsRequestedUserId>";
String newPassword = "test@123"; //"<PasswordAsTypedByUser>";
String hashedNewPassword = UserManager.PasswordHasher.HashPassword(newPassword);                    
ApplicationUser cUser = await store.FindByIdAsync(userId);
await store.SetPasswordHashAsync(cUser, hashedNewPassword);
await store.UpdateAsync(cUser);

你知道1.1版本什么时候发布吗? - graycrow
它仍处于alpha版本,1.0版本刚刚发布。因此请假设还需要几个月的时间。https://www.myget.org/gallery/aspnetwebstacknightly - jd4u
11
有趣的是,对我而言,调用store.SetPasswordHashAsync(cUser, hashedNewPassword)方法并没有生效,相反,我不得不手动设置cUser.PasswordHash = hashedNewPassword,然后调用UserManager.UpdateAsync(user)。 - Andy Mehalick
1
只有在用户检索上下文和存储上下文不同的情况下,代码才无法正常工作。该代码仅为示例步骤,不准确。很快会更新答案以避免其他人出现此问题。 - jd4u
1
框架1不提供此功能,但2-alpha框架有一些特性可以提供用于处理密码重置请求的简单过程。http://aspnetidentity.codeplex.com/ - jd4u
显示剩余4条评论

71

已过时

这是原始答案。它可以运行,但存在问题。如果AddPassword失败了怎么办?用户将没有密码。

原始答案:我们可以使用三行代码:

UserManager<IdentityUser> userManager = 
    new UserManager<IdentityUser>(new UserStore<IdentityUser>());

userManager.RemovePassword(userId);

userManager.AddPassword(userId, newPassword);

参见: http://msdn.microsoft.com/en-us/library/dn457095(v=vs.111).aspx

现在推荐的方法

最好使用EdwardBrey提出的答案,然后再参考DanielWright给出的代码示例


1
谢天谢地,我以为我得创建一个新的用户存储才行,直到看到了这个! - Luke
3
使用此功能时需小心,若AddPassword失败(例如密码复杂度不足),则用户将无法设置密码。请注意。 - Chris
@Chris 这个问题及其答案试图解决这个问题:https://dev59.com/IF4b5IYBdhLWcg3wVwJi#29292056 要么创建一个自定义的 ChangePassword 方法,要么使用现有的 AddPassword 并不断尝试直到成功。 - Shaun Luttin
1
最干净的方法是不绕过任何业务规则(因为当您直接访问密码哈希程序时,没有密码复杂性验证),这正是Daniel Wright所提出的建议。 - Chris
@Chris 很棒的建议。我更新了我的答案,推荐了 Edward 的回答(比 Daniel 的早11个月发布的)。 - Shaun Luttin
显示剩余2条评论

30

3
尝试弄清楚为什么ResetPasswordAsync需要用户ID以及从用户获取ID的合理方式,当他们带着令牌出现时。GeneratePasswordReset使用一个超过150个字符的令牌……看起来足够加密存储用户ID,这样我就不必自己实现了。:(重置密码异步操作需要用户ID,并且需要找到一种合理的方法,在用户使用令牌时获取其ID。GeneratePasswordReset使用一个超过150个字符的令牌,似乎可以用来加密存储用户ID,这样就不必自己实现了。 :( - pettys
我认为它正在请求用户ID,以便可以将重置令牌输入到该用户ID的身份数据库中。如果不这样做,框架怎么知道令牌是否有效呢?您应该能够使用User.Identity.GetUserId()或类似方法获取用户ID。 - Ryan Buddicom
1
在API的部分要求用户ID是一个愚蠢的选择,当调用ResetPassword(async)时,令牌已经在数据库中了,只需对输入进行验证就足够了。 - Filip
@Filip,ResetPasswordAsync 接受用户 ID 的优点是,身份提供者只需要索引用户 ID,而不必还索引令牌。如果有许多用户,它可以更好地扩展。 - Edward Brey
2
@Edward Brey 嗯,你如何获取重置调用的用户ID? - Filip
显示剩余3条评论

6

在使用Web API时,重置Asp.Net Core Identity密码的最佳方法。

注意*: Error()和Result()是为内部使用而创建的。您可以返回您想要的内容。

        [HttpPost]
        [Route("reset-password")]
        public async Task<IActionResult> ResetPassword(ResetPasswordModel model)
        {
            if (!ModelState.IsValid)
                return BadRequest(ModelState);
            try
            {
                if (model is null)
                    return Error("No data found!");


                var user = await _userManager.FindByIdAsync(AppCommon.ToString(GetUserId()));
                if (user == null)
                    return Error("No user found!");

                Microsoft.AspNetCore.Identity.SignInResult checkOldPassword =
                    await _signInManager.PasswordSignInAsync(user.UserName, model.OldPassword, false, false);

                if (!checkOldPassword.Succeeded)
                    return Error("Old password does not matched.");

                string resetToken = await _userManager.GeneratePasswordResetTokenAsync(user);
                if (string.IsNullOrEmpty(resetToken))
                    return Error("Error while generating reset token.");

                var result = await _userManager.ResetPasswordAsync(user, resetToken, model.Password);

                if (result.Succeeded)
                    return Result();
                else
                    return Error();
            }
            catch (Exception ex)
            {
                return Error(ex);
            }
        }

2
这对我在 Fx v 4.5 上也起作用了。另一个解决方案不起作用。从根本上讲,这更简单。您甚至不需要真正获取用户,因为所有方法都将接受 id。我只需要它来进行一次性重置我的管理界面,所以我不需要所有的错误检查。 - Steve Hiner

3
我认为微软的ASP.NET Identity指南是一个不错的起点。https://learn.microsoft.com/en-us/aspnet/identity/overview/features-api/account-confirmation-and-password-recovery-with-aspnet-identity 注意:如果您没有使用AccountController并希望重置密码,请使用Request.GetOwinContext().GetUserManager<ApplicationUserManager>();。如果您没有相同的OwinContext,您需要创建一个新的DataProtectorTokenProvider就像OwinContext使用的那样。默认情况下,查看App_Start -> IdentityConfig.cs。应该类似于 new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"));
可以这样创建:
无需Owin:
[HttpGet]
[AllowAnonymous]
[Route("testReset")]
public IHttpActionResult TestReset()
{
    var db = new ApplicationDbContext();
    var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(db));
    var provider = new DpapiDataProtectionProvider("SampleAppName");
    manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(
        provider.Create("SampleTokenName"));

    var email = "test@test.com";

    var user = new ApplicationUser() { UserName = email, Email = email };

    var identityUser = manager.FindByEmail(email);

    if (identityUser == null)
    {
        manager.Create(user);
        identityUser = manager.FindByEmail(email);
    }

    var token = manager.GeneratePasswordResetToken(identityUser.Id);
    return Ok(HttpUtility.UrlEncode(token));
}

[HttpGet]
[AllowAnonymous]
[Route("testReset")]
public IHttpActionResult TestReset(string token)
{
    var db = new ApplicationDbContext();
    var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(db));
    var provider = new DpapiDataProtectionProvider("SampleAppName");
    manager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(
        provider.Create("SampleTokenName"));
    var email = "test@test.com";
    var identityUser = manager.FindByEmail(email);
    var valid = Task.Run(() => manager.UserTokenProvider.ValidateAsync("ResetPassword", token, manager, identityUser)).Result;
    var result = manager.ResetPassword(identityUser.Id, token, "TestingTest1!");
    return Ok(result);
}

使用 Owin:

[HttpGet]
[AllowAnonymous]
[Route("testResetWithOwin")]
public IHttpActionResult TestResetWithOwin()
{
    var manager = Request.GetOwinContext().GetUserManager<ApplicationUserManager>();

    var email = "test@test.com";

    var user = new ApplicationUser() { UserName = email, Email = email };

    var identityUser = manager.FindByEmail(email);

    if (identityUser == null)
    {
        manager.Create(user);
        identityUser = manager.FindByEmail(email);
    }

    var token = manager.GeneratePasswordResetToken(identityUser.Id);
    return Ok(HttpUtility.UrlEncode(token));
}

[HttpGet]
[AllowAnonymous]
[Route("testResetWithOwin")]
public IHttpActionResult TestResetWithOwin(string token)
{
    var manager = Request.GetOwinContext().GetUserManager<ApplicationUserManager>();

    var email = "test@test.com";
    var identityUser = manager.FindByEmail(email);
    var valid = Task.Run(() => manager.UserTokenProvider.ValidateAsync("ResetPassword", token, manager, identityUser)).Result;
    var result = manager.ResetPassword(identityUser.Id, token, "TestingTest1!");
    return Ok(result);
}

DpapiDataProtectionProviderDataProtectorTokenProvider需要使用相同的名称进行创建,才能使重置密码工作。如果使用Owin创建密码重置令牌,然后使用另一个名称创建新的DpapiDataProtectionProvider将不起作用。

我在ASP.NET Identity中使用的代码:

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

3
string message = null;
//reset the password
var result = await IdentityManager.Passwords.ResetPasswordAsync(model.Token, model.Password);
if (result.Success)
{
    message = "The password has been reset.";
    return RedirectToAction("PasswordResetCompleted", new { message = message });
}
else
{
    AddErrors(result);
}

以下代码片段取自AspNetIdentitySample项目,可在Github上获得


2

如果需要重置密码,建议通过发送密码重置令牌到注册用户的电子邮件并要求用户提供新密码来进行重置。我们已经在Identity框架上创建了一个易于使用的.NET库,具有默认配置设置。您可以在博客链接github源代码中找到详细信息。


2
UserManager<TUser, TKey> 中创建方法。
public Task<IdentityResult> ChangePassword(int userId, string newPassword)
{
     var user = Users.FirstOrDefault(u => u.Id == userId);
     if (user == null)
          return new Task<IdentityResult>(() => IdentityResult.Failed());

     var store = Store as IUserPasswordStore<User, int>;
     return base.UpdatePassword(store, user, newPassword);
}

2

我进行了一些调查,对我有效的解决方案是在这篇文章中找到的几个解决方案的混合体。

基本上我正在编译这个解决方案并发布我所使用的内容。在我的情况下,我不想使用任何来自 .net core 的令牌。

public async Task ResetPassword(string userId, string password)
{
    var user = await _userManager.FindByIdAsync(userId);
    var hashPassword= _userManager.PasswordHasher.HashPassword(user, password);
    user.PasswordHash = passwordHash;
    await _userManager.UpdateAsync(user);

}

“对于与安全相关的事情,‘对我有效’并不足够好。我想尽可能多地使用预制的.NET Core。” - Heinzlmaen

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接