为什么 ASP.NET 核心密码验证规则无效?

Why do the ASP.NET Core password validation rules have no effect?

我正在构建一个 ASP.NET Core 5 Web API 使用 ASP.NET Core Identity 在使用 Dapper ORM 的自定义数据访问层之上。从根本上说,一切都按预期工作,但我意识到身份框架提供的密码验证规则没有任何效果,我不明白发生了什么。这是我拥有的:

首先,因为我依赖自定义数据访问层,所以我提供了 Identity 的 IUserStore 接口的自定义实现。

public class UserStore : IUserStore<AppUser>,
                         IUserPasswordStore<AppUser>,
                         IUserEmailStore<AppUser>
{
  private IRepository<AppUser> _repository;

  public UserStore(IConfiguration configuration)
  {
    _repository = new AppUserRepository(configuration.GetConnectionString("MyConnectionString"));
  }

  // IUserStore implementation
  // IUserPasswordStore implementation
  // IUserEmailStore implementation
}

接下来,有一个绑定模型,用于提交创建新帐户所需的信息。

public class RegisterBindingModel
{
  [Required]
  [Display(Name = "UserName")]
  public string UserName
  {
    get;
    set;
  }

  [Required]
  [DataType(DataType.Password)]
  [Display(Name = "Password")]
  public string Password
  {
    get;
    set;
  }

  [DataType(DataType.Password)]
  [Display(Name = "Confirm password")]
  [Compare(nameof(Password), ErrorMessage = "The password and confirmation password do not match.")]
  public string ConfirmPassword
  {
    get;
    set;
  }

  // remaining required properties
}

接下来,通过 AccountController 创建新帐户:

[Authorize]
[ApiController]
[Route("api/Accounts")]
public class AccountController
{
  private readonly UserManager<AppUser>     _userManager;
  private readonly IPasswordHasher<AppUser> _passwordHasher;

  public AccountController(UserManager<AppUser> userManager, IPasswordHasher<AppUser> passwordHasher)
  {
    _userManager    = userManager;
    _passwordHasher = passwordHasher;
  }

  [AllowAnonymous]
  [HttpPost]
  [Route("Register")]
  public async Task<ActionResult> Register([FromBody]RegisterBindingModel model)
  {
    if(model == null)
    {
      return BadRequest();
    }

    if(!ModelState.IsValid)
    {
      return BadRequest(ModelState);
    }

    var user = new AppUser()
    {
      UserName  = model.UserName,
      Firstname = model.Firstname,
      Lastname  = model.Lastname,
      Email     = model.Email,
      Gender    = model.Gender
    };

    user.PasswordHash     = _passwordHasher.HashPassword(user, model.Password);
    IdentityResult result = await _userManager.CreateAsync(user);

    return GetHttpResponse(result);
  }

  [AllowAnonymous]
  [HttpPost]
  [Route("Token")]
  public async Task<IActionResult> Login([FromForm]LoginBindingModel model)
  {
    if(model == null)
    {
      return BadRequest();
    }

    if(!ModelState.IsValid)
    {
      return BadRequest(ModelState);
    }

    AppUser user = await _userManager.FindByNameAsync(model.UserName);

    if(user == null || !await _userManager.CheckPasswordAsync(user, model.Password))
    {
      return Unauthorized();
    }

    DateTime                currentTime     = DateTime.UtcNow;
    JwtSecurityTokenHandler jwtTokenHandler = new();
    SecurityTokenDescriptor tokenDescriptor = new()
    {
      Subject            = new ClaimsIdentity(new[] { new Claim(ClaimTypes.NameIdentifier, user.AppUserId.ToString()) }),
      IssuedAt           = currentTime,
      Expires            = currentTime.AddHours(_accessTokenValidityInHours),
      SigningCredentials = _signingCredentialsProvider.GetSigningCredentials()
    };

    return Ok(jwtTokenHandler.WriteToken(jwtTokenHandler.CreateToken(tokenDescriptor)));
  }

  ...
}

最后,连接在一起的东西如下:

public class Startup
{
  public Startup(IConfiguration configuration)
  {
    Configuration = configuration;
  }

  public IConfiguration Configuration
  {
    get;
  }

  public void ConfigureServices(IServiceCollection services)
  {
    services.AddIdentityCore<AppUser>(options => Configuration.GetSection(nameof(IdentityOptions)).Bind(options));
    services.AddScoped<IPasswordHasher<AppUser>, Identity.PasswordHasher<AppUser>>();
    services.AddTransient<IUserStore<AppUser>, UserStore>();
    ...
  }
}

对应的设置保存在appsettings.json文件中:

{
  "IdentityOptions": {
    "Password": {
      "RequiredLength": 6,
      "RequiredUniqueChars": 6,
      "RequireNonAlphanumeric": true,
      "RequireUppercase": true,
      "RequireLowercase": true,
      "RequireDigit": true
    },
    "Lockout": {
      "AllowedForNewUsers": true,
      "MaxFailedAccessAttempts ": 5,
      "DefaultLockoutTimeSpan ": "00:05:00"
    }
  },
  ...
}

如果我发送带有必要帐户数据的 HTTP POST 请求,那么实际上密码是什么并不重要。即使我只是将 1 作为明显违反密码规则的密码,调用也会成功。语句 if(!ModelState.IsValid) 高兴地告诉我模型一切正常。

据我所知,ASP.NET Core Identity 提供了一个 PasswordValidator,显然应该根据提供的设置验证密码。从我得到的结果来看,该验证器没有 运行 在我的设置中。

我不清楚事情是应该按它们本来的方式工作,还是我需要实施一些我不知道的事情。有没有人有更多见识,可以告诉我我在这里缺少什么?

编辑:

我刚刚意识到默认的 UserManager 公开了一个 IPasswordValidator 对象的列表。我使用该列表来验证 AccountControllerRegister 方法中的密码的想法是什么?

你的 if(!ModelState.IsValid) 没有发现任何错误的原因是你的模型 RegisterBindingModel 中的 Password 参数不包含与你的选项相同的验证appsettings.json,你只验证它是必需的(所以一个字符就可以了)。

如果您想要进行相同的验证,您需要在 Password 参数上添加更多属性。我建议你看看这个 https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-5.0

我最终修改了 Register 方法如下:

[AllowAnonymous]
[HttpPost]
[Route("Register")]
public async Task<ActionResult> Register([FromBody]RegisterBindingModel model)
{
  if(model == null)
  {
    return BadRequest();
  }

  if(!ModelState.IsValid)
  {
    return BadRequest(ModelState);
  }

  AppUser user = new()
  {
    UserName  = model.UserName,
    Firstname = model.Firstname,
    Lastname  = model.Lastname,
    Email     = model.Email,
    Gender    = model.Gender
  };

  IdentityResult result;

  foreach(IPasswordValidator<AppUser> passwordValidator in _userManager.PasswordValidators)
  {
    result = await passwordValidator.ValidateAsync(_userManager, user, model.Password);

    if(!result.Succeeded)
    {
      return BadRequest(result.Errors);
    }
  }

  user.PasswordHash = _userManager.PasswordHasher.HashPassword(user, model.Password);
  result            = await _userManager.CreateAsync(user);

  return GetHttpResponse(result);
}

默认 UserManager 包含 PasswordValidators 属性,它允许我访问所有 PasswordValidators。我只是遍历它们并在用户提交的密码上调用 ValidateAsync 方法。