8.1标识框架

ASP.NET Core提供了标识框架,采用RBAC(基于角色的访问控制),内置了对用户、角色等表的管理及相关接口,框架中提供了​​IdentityUser<TKey>​​​和​​IdentityRole<TTKey>​​两个实体类型,Tkey为主键类型。

使用步骤:

1. NuGet安装​​Microsoft.AspNetCore.Identity.EntityFrameworkCore​

2. 创建用户实体类和角色实体类

//每次直接使用IdentityUser<long>都要说明主键类型,所以直接继承
//IdentityUser类中已经内置了用户名、密码、邮箱等属性,如果想增加自己的属性,也可以使用继承
public class User : IdentityUser<long>
{
public DateTime CreationTime { get; set; }//增加创建时间和昵称两个自定义属性
public string? NickName { get; set; }
}

public class Role : IdentityRole<long>
{
}

除了IdentityUser和IdentityRole,标识框架中还有IdentityRoleClaim、IdentityUserToken等实体类,这些实体类都有默认的表名,如果要修改,可以使用IEntityTypeConfiguration来对实体类进行配置

3. 创建标识上下文类,继承自IdentityDbContext

//泛型参数分别代表用户类型、角色类型、主键类型
public class IdDbContext : IdentityDbContext<User, Role, long>
{
public IdDbContext(DbContextOptions<IdDbContext> options)
: base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);
modelBuilder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
}

可以通过这个类操作数据库,但是标识框架提供了​​RoleManager​​​、​​UserManager​​类简化对数据库的操作,这些类封装了对IdentityDbContext的操作。

标识框架中的方法有执行失败的可能,所以有些方法可以通过​​Task<IdentityResult>​​​的返回结果来验证是否失败,IdentityResult的​​Succeeded​​​属性表示是否操作成功,如果失败,则可以从​​Errors​​属性中获取错误信息,

RoleManager常用方法:

方法

说明

​Task<IdentityResult> CreateAsync(TRole role)​

创建角色

​Task<IdentityResult> DeleteAsync(TRole role)​

删除角色

​Task<bool> RoleExistsAsync(string roleName)​

指定名字的角色是否存在

​Task<TRole> FindByNameAsync(string roleName)​

根据角色名字获取角色对象

UserManager常用方法:

方法

说明

​Task<IdentityResult> CreateAsync(TUser user,string password)​

创建用户

​Task<IdentityResult> UpdateAsync(TUser user)​

更新用户

​Task<IdentityResult> DeleteAsync(TUser user)​

删除用户

​Task<IUser> FindByIdAsync(string userId)​

根据Id查找用户

​Task<IUser> FindByNameAsync(string userName)​

根据name查找用户

​Task<Bool> CheckPasswordAsync(TUser user,string password)​

检查用户密码是否正确,如果失败则调用AccessFailedAsync记录失败次数

​Task<IdentityResult> ChangePasswordAsync(TUser user,string currentPassword,string newPassword)​

修改密码

​Task<string> GeneratePasswordResetTokenAsync(TUser user)​

生成令牌,用来重置密码

​Task<IdentityResult> ResetPasswordAsync(TUser user,string token,string newPassword)​

重置密码

​Task<IdentityResult> AddToRoleAsync(TUser user,string role)​

为用户增加角色

​Task<IdentityResult> RemoveFromRoleAsync(TUser user,string role)​

为用户删除角色

​Task<IList<string>> GetRolesAsync(TUser user)​

用户所拥有的所有角色

​Task<bool> IsInRoleAsync(TUser user,string role)​

判断用户是否具有某个角色

​Task<bool> IsLockedOutAsync(TUser user)​

判断用户是否被锁定

​Task<DataTimeOffset?> GetLockoutEndDataAsync(TUser user)​

获取锁定时间

​Task<DataTimeOffset> SetLockoutEndDataAsync(TUser user,DataTimeOffset? lockoutEnd)​

设置用户锁定时间

​Task<IdentityResult> AccessFailedAsync(TUser user)​

记录用户登陆失败次数,多次失败应当锁定一段时间

4. 向容器中注册服务

IServiceCollection services = builder.Services;
//对IdDbContext进行设置
services.AddDbContext<IdDbContext>(opt => {
string connStr = builder.Configuration.GetConnectionString("Default");
opt.UseSqlServer(connStr);
});
services.AddDataProtection();
//添加标识框架的一些重要基础服务,如密码几位,是否要求有大小写
services.AddIdentityCore<User>(options =>
{
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequiredLength = 6;
//密码重置时所需要令牌
options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider;
//账户验证时所需要令牌(注册)
options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider;
});
var idBuilder = new IdentityBuilder(typeof(User), typeof(Role), services);
//注册各种服务
idBuilder.AddEntityFrameworkStores<IdDbContext>()
.AddDefaultTokenProviders()
.AddRoleManager<RoleManager<Role>>()
.AddUserManager<UserManager<User>>();

5. 使用​​Add-Migration​​​,​​Update-database​​生成数据库

6. 编写控制器代码,对角色、用户进行操作

public class Test1Controller : ControllerBase
{
private readonly ILogger<Test1Controller> logger;//注册日志
private readonly RoleManager<Role> roleManager;
private readonly UserManager<User> userManager;
public Test1Controller(ILogger<Test1Controller> logger,
RoleManager<Role> roleManager, UserManager<User> userManager)
{
this.logger = logger;
this.roleManager = roleManager;
this.userManager = userManager;
}
[HttpPost]
public async Task<ActionResult> CreateUserRole()
{
bool roleExists = await roleManager.RoleExistsAsync("admin");//判断admin账户是否存在
if (!roleExists)
{
Role role = new Role { Name="Admin"};
var r = await roleManager.CreateAsync(role);
if (!r.Succeeded)//框架会存在创建失败的情况,一般都要进行判断是否成功
{
return BadRequest(r.Errors);
}
}
User user = await this.userManager.FindByNameAsync("yzk");//查找用户
if (user == null)
{
//EmailConfirmed设置为true
//使用邮箱注册时,发送验证码到邮箱,用户输入验证码后才能确认这个邮箱可用,EmailConfirmed属性表示邮箱是否确认过
//如果邮箱确认是存在的,则可以像下面 这样直接使用
//如果创建用户的时候不确定邮箱是否可用,则需要先调用UserManager的GenerateEmailConfirmationTokenAsync创建
//一个字符串作为“确认令牌”,服务器将确认令牌发送到用户邮箱,用户在输入确认令牌的时候,调用UserManager的
//ConfirmEmailAsync方法来验证令牌
user = new User{UserName="yzk",Email="yangzhongke8@gmail.com",EmailConfirmed=true};
var r = await userManager.CreateAsync(user, "123456");//创建用户
if (!r.Succeeded)
{
return BadRequest(r.Errors);
}
r = await userManager.AddToRoleAsync(user, "admin");//增加角色
if (!r.Succeeded)
{
return BadRequest(r.Errors);
}
}
return Ok();
}
}

7. 编写登陆请求的操作方法

public record LoginRequest(string UserName,string Password);

[HttpPost]
public async Task<ActionResult> Login(LoginRequest req)
{
string userName = req.UserName;
string password = req.Password;
var user = await userManager.FindByNameAsync(userName);
if (user == null)
{
return NotFound($"用户名不存在{userName}");
}
if (await userManager.IsLockedOutAsync(user))
{
return BadRequest("LockedOut");
}
var success = await userManager.CheckPasswordAsync(user, password);//验证密码是否正确
if (success)
{
return Ok("Success");
}
else
{
//密码错误则记录一次登陆失败,达到次数后就锁定账户一段时间,防止暴力破解
//失败次数和锁定时间可以在AddIdentityCore中设定
//option.Lockout.DefaultLockoutTimesSpan和option.Lockout.MaxFailedAccessAttempts来修改
var r = await userManager.AccessFailedAsync(user);
if (!r.Succeeded)
{
return BadRequest("AccessFailed failed");
}
return BadRequest("Failed");
}
}

实现密码重置

发送重置密码的请求

public record SendResetPasswordTokenRequest(string Email);

[HttpPost]
public async Task<IActionResult> SendResetPasswordToken(
SendResetPasswordTokenRequest req)
{
string email = req.Email;
var user = await userManager.FindByEmailAsync(email);
if (user == null)
{
return NotFound($"邮箱不存在{email}");
}
//生成密码令牌
string token = await userManager.GeneratePasswordResetTokenAsync(user);
logger.LogInformation($"向邮箱{user.Email}发送Token={token}");
return Ok();
}

重置密码

public record VerifyResetPasswordRequest(string Email,string token,string newPassword);

public async Task<IActionResult> VerifyResetPassword(
SendResetPasswordTokenRequest req)
{
string email = req.Email;
var user = await userManager.FindByEmail(email);
string token = req.Token;
string password = req.NewPassword;
var r = await userManager.ResetPasswordAsync(user,token,password);//重置密码
return Ok();
}