ASP.NET身份验证更改密码

103

我需要管理员能够修改用户的密码。因此,管理员不应该输入用户的当前密码,而是应该有能力设置一个新密码。我查看了ChangePasswordAsync方法,但是这个方法要求输入旧密码,因此这个方法对于这个任务来说不合适。因此,我通过以下方式实现了它:

    [HttpPost]
    public async Task<ActionResult> ChangePassword(ViewModels.Admin.ChangePasswordViewModel model)
    {
        var userManager = HttpContext.GetOwinContext().GetUserManager<ApplicationUserManager>();
        var result = await userManager.RemovePasswordAsync(model.UserId);
        if (result.Succeeded)
        {
            result = await userManager.AddPasswordAsync(model.UserId, model.Password);
            if (result.Succeeded)
            {
                return RedirectToAction("UserList");
            }
            else
            {
                ModelState.AddModelError("", result.Errors.FirstOrDefault());
            }
        }
        else
        {
            ModelState.AddModelError("", result.Errors.FirstOrDefault());
        }
        return View(model);
    }

它能工作,但理论上我们可能会在AddPasswordAsync方法上收到错误。因此,旧密码将被删除但新密码未设置。这不好。有没有办法在“一个事务”中完成它? 附注:我看到了带有重置令牌的ResetPasswordAsync方法,似乎更安全(因为用户不会出现不稳定情况),但无论如何,它都需要2个操作。


2
这个问题的关键在于要在一个事务中完成。如果您愿意分两个事务来完成,并且不断尝试直到第二个成功,那么您会满意吗?如果不行,您可能需要编写自己的更改密码实现。 - Shaun Luttin
11个回答

158

编辑:我知道楼主要求一次性完成任务的答案,但我认为这段代码对人们很有用。

所有答案都直接使用 PasswordHasher,这并不是一个好主意,因为你将失去一些内置功能(验证等)。

另一种方法(我认为是推荐的方法)是创建一个密码重置令牌,然后使用该令牌来更改密码。例如:

var user = await UserManager.FindByIdAsync(id);

var token = await UserManager.GeneratePasswordResetTokenAsync(user);

var result = await UserManager.ResetPasswordAsync(user, token, "MyN3wP@ssw0rd");

10
这似乎是正确的方式,并且开启了许多新的可能性。谢谢! - peter_the_oak
3
我更喜欢这个答案。 - Joe Lu
3
当然,您也可以使用UserManager中定义的“PasswordValidator”属性,在设置密码之前进行验证。但这不是原始问题的要求。 - Tseng
1
这似乎是更好的方法,谢谢 - 我仍想使用内置的安全检查和IdentityResult。直接更新值似乎有些靠不住。 - gen
5
非常好的回答。需要注意一点:如果您没有注册令牌提供程序,它将会抛出异常。我必须在Program.cs文件中的IdentityBuilder流畅链中添加.AddDefaultTokenProviders()(例如,在builder.Services.AddIdentityCore之后链接)。 - BCA

74

这种方法对我起作用:

public async Task<IHttpActionResult> changePassword(UsercredentialsModel usermodel)
{
  ApplicationUser user = await AppUserManager.FindByIdAsync(usermodel.Id);
  if (user == null)
  {
    return NotFound();
  }
  user.PasswordHash = AppUserManager.PasswordHasher.HashPassword(usermodel.Password);
  var result = await AppUserManager.UpdateAsync(user);
  if (!result.Succeeded)
  {
    //throw exception......
  }
  return Ok();
}

13
虽然这段代码可能有效,但一个好的回答应该解释它是如何工作的以及为什么这是个好的解决方案。 - Blackwood
3
在UserManager类中更改默认的密码修改方法时,首先尝试验证密码并进行安全检查,但我直接像普通字段一样更新了密码值,没有对其施加特殊约束。 - bryan c
1
什么是UsercredentialsModel? - Dov Miller
1
UsercredentialsModel是一个简单的自定义类,作为用户模型。在我的情况下,它看起来像这样:'public class UsercredentialsModel { public string Id { get; set; } public string PhoneNumber { get; set; } public string VerificationCode { get; set; } public string Password { get; set; } }' @Dov Miller - bryan c
2
虽然我感觉这不是正确的修改密码方式,但在无法生成重置令牌和验证令牌的情况下,它帮助了我解决不同的问题。 - Sabir Hossain

46

ApplicationUserManager是由ASP.NET模板生成的类。

这意味着您可以编辑它并添加任何尚未具备的功能。UserManager类有一个受保护的属性名为Store,它存储对UserStore类(或其任何子类,取决于您如何配置ASP.NET身份验证或是否使用自定义用户存储实现,例如如果您使用不同的数据库引擎,如MySQL)的引用。

public class AplicationUserManager : UserManager<....> 
{
    public async Task<IdentityResult> ChangePasswordAsync(TKey userId, string newPassword) 
    {
        var store = this.Store as IUserPasswordStore;
        if(store==null) 
        {
            var errors = new string[] 
            { 
                "Current UserStore doesn't implement IUserPasswordStore"
            };

            return Task.FromResult<IdentityResult>(new IdentityResult(errors) { Succeeded = false });
        }

        if(PasswordValidator != null)
        {
            var passwordResult = await PasswordValidator.ValidateAsync(password);
            if(!password.Result.Success)
                return passwordResult;
        }

        var newPasswordHash = this.PasswordHasher.HashPassword(newPassword);

        await store.SetPasswordHashAsync(userId, newPasswordHash);
        return Task.FromResult<IdentityResult>(IdentityResult.Success);
    }
}
UserManager不过是对底层UserStore的包装。请在MSDN上查看IUserPasswordStore接口文档以获取可用方法。 编辑: PasswordHasher也是UserManager类的公共属性,可以在此处查看接口定义。 编辑2: 由于一些人“天真地”认为不能通过这种方式进行密码验证,因此我已经更新了内容。 PasswordValidator属性也是UserManager的一个属性,并且只需添加两行代码即可实现密码验证(原始问题并没有要求)。

1
这是一个有趣的方法。我在想我们能否实现一种方式,即使管理员不知道原始密码,也可以在一次交易中更改密码。 - Shaun Luttin
3
这种方法只需要进行一次交易。您不需要旧密码,因为新实现不使用旧功能,并直接从“UserStore”的实现中调用“SetPasswordHashAsync”,如果有的话。您只需要知道“userId”和“newPassword”(可以随机生成)。 - Tseng
3
我使用了这个答案,但是除非我在执行SetPasswordHashAsync方法后添加await store.UpdateAsync(user, cancellation);,否则密码不会更新到数据库表中。附注:我稍微调整了一下,_user_是class ApplicationUser: IdentityUser的对象。 为什么会发生这种情况? - Endri

16

在 .net core 3.0 中

var token = await UserManager.GeneratePasswordResetTokenAsync(user);
var result = await UserManager.ResetPasswordAsync(user, token, password);

12

我认为解决方案要简单得多。

  1. 生成密码令牌,
  2. 使用生成的令牌重置密码...
public async Task<IdentityResult> ResetPasswordAsync(ApplicationUser user, string password)
{
    string token = await userManager.GeneratePasswordResetTokenAsync(user);
    return await userManager.ResetPasswordAsync(user, token, password);
}

1
我认为这个参数应该是 user.Id,用于 GeneratePasswordResetTokenAsync() 方法。 - Literate Corvette

7

这只是对@Tseng提供的答案进行的精细化改进(我必须进行微调才能使其正常工作)。

public class AppUserManager : UserManager<AppUser, int>
{
    .
    // standard methods...
    .

    public async Task<IdentityResult> ChangePasswordAsync(AppUser user, string newPassword)
    {
        if (user == null)
            throw new ArgumentNullException(nameof(user));

        var store = this.Store as IUserPasswordStore<AppUser, int>;
        if (store == null)
        {
            var errors = new string[] { "Current UserStore doesn't implement IUserPasswordStore" };
            return IdentityResult.Failed(errors);
        }

        var newPasswordHash = this.PasswordHasher.HashPassword(newPassword);
        await store.SetPasswordHashAsync(user, newPasswordHash);
        await store.UpdateAsync(user);
        return IdentityResult.Success;
    }
}

注意:这仅适用于使用 int 作为用户和角色的主键的修改设置。我相信只需要删除 <AppUser,int> 类型参数即可使其与默认的 ASP.NET Identity 设置一起正常工作。

3
public async Task<IActionResult> ChangePassword(ChangePwdViewModel usermodel)
        {           
            var userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
            var user = await _userManager.FindByIdAsync(userId);            
            var result = await _userManager.ChangePasswordAsync(user, usermodel.oldPassword, usermodel.newPassword);
            if (!result.Succeeded)
            {
                //throw exception......
            }
            return Ok();
        }

public class ChangePwdViewModel
    {  
        [DataType(DataType.Password), Required(ErrorMessage ="Old Password Required")]
        public string oldPassword { get; set; }

        [DataType(DataType.Password), Required(ErrorMessage ="New Password Required")]
        public string newPassword { get; set; }
    }

注意:此处的UserId是我从当前登录的用户中检索出来的。

2
对于使用ASP.NET Core 3.1的用户来说,这是对 @Tseng 和 @BCA 提供的优秀答案的现代化迭代。 PasswordValidator 不再是 UserManager 的一个属性 - 取而代之的是一个 IList PasswordValidators 属性。此外,Identity 现在有一个 protected UpdatePasswordHash 方法,可以更改密码而无需直接访问 UserStore,这消除了手动哈希和保存密码的需要。 UserManager 还有一个公共属性,bool SupportsUserPassword,它取代了测试 Store 是否实现了 IUserPasswordStore 的需要(在内部,这正是 UserManagerSupportsUserPassword getter 中所做的)。
由于 UpdatePasswordHashprotected 的,因此仍然需要扩展基本的 UserManager。它的签名是: protected Task<IdentityResult> UpdatePasswordHash(TUser user, string newPassword, bool validatePassword) 其中 validatePassword 表示是否运行密码验证。不幸的是,默认情况下,它没有默认值 true,所以需要提供它。实现如下:
public async Task<IdentityResult> ChangePasswordAsync(ApplicationUser user, string newPassword)
{
    if (!SupportsUserPassword)
    {
        return IdentityResult.Failed(new IdentityError
        {
            Description = "Current UserStore doesn't implement IUserPasswordStore"
        });
    }
            
    var result = await UpdatePasswordHash(user, newPassword, true);
    if (result.Succeeded)
        await UpdateAsync(user);

    return result;
}

与以前一样,首要任务是确保当前的UserStore支持密码。

然后,只需使用ApplicationUser、新密码和true调用UpdatePasswordHash来更新该用户的密码并进行验证。如果更新成功,仍需要保存用户,因此调用UpdateAsync


这听起来不错,但是,当使用扩展的UserManager时,如何在各处注入它呢?我有一个在控制器中使用UserManager的.NET Core应用程序,所以我需要注入它。运行时,会抛出一个错误,告诉我没有服务可以注入自定义用户管理器。 - undefined

1
如果您没有用户的当前密码但仍想更改密码,您可以先删除用户的密码,然后再添加新密码。这样,您就可以在不需要该用户的当前密码的情况下更改用户的密码。
await UserManager.RemovePasswordAsync(user);
await UserManager.AddPasswordAsync(user, model.Password);

3
问题在于,如果新密码未通过验证,您已经删除了现有密码。您可以尝试在删除旧密码之前验证密码,但仍存在添加密码失败的风险,这会使用户没有密码,可能无法登录。 - Zac Faragher

-1
public async Task<ActionResult> ResetUserPassword(string id, string Password)
{
    //     Find User
    var user = await context.Users.Where(x => x.Id == id).SingleOrDefaultAsync();
    if (user == null)
    {
        return RedirectToAction("UserList");
    }
    await UserManager.RemovePasswordAsync(id);
    //     Add a user password only if one does not already exist
    await UserManager.AddPasswordAsync(id, Password);
    return RedirectToAction("UserDetail", new { id = id });
}

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