为什么 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
对象的列表。我使用该列表来验证 AccountController
的 Register
方法中的密码的想法是什么?
你的 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
方法。
我正在构建一个 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
对象的列表。我使用该列表来验证 AccountController
的 Register
方法中的密码的想法是什么?
你的 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
方法。