Identity 框架:
1.采用基于角色的访问控制(Role-Based Access Control, 简称 RABC) 策略,内置了对用户,角色等表的管理以及相关的接口 。
2. Identity 框架使用 EF Core 对数据库进行操作。
Identity 框架使用
由于 Identity 框架是使用 EF Core 对数据库进行操作的,所以在操作上与定义 EF Core 有着类似支出。
1.定义实体类:
Identity 框架已经帮助我们定义了两个实体类,IdentityUser<TKey> 和 IdentityRole<TKey>, 其中 TKey 代表主键的类型。虽然定义的实体类里面的属性已经足够我们去使用,但是如果有需要我们还是会定义继承 IdentityUser<TKey> 和 IdentityRole<TKey> 的两个实体类,在其中用来增加自定义属性。
IdentityRole 实体类:用于定义角色数据,比如不同角色,他们的操作权限也不一样
/// <summary>
/// 验证用户是否有权限做一些操作
/// </summary>
public class MyRole : IdentityRole<long>
{
//IdentityRole 实体中也定义了许多的列,可以在子类中添加
}
IdentityUser 实体类:用户定义用户信息,用于验证用户登录等
//验证用户是否登录
public class MyUser : IdentityUser<long>
{
//IdentityUser 实体中已经定义了许多的列
public string? WeiChart { get; set; }
}
2.安装 Identity 框架所使用的 NuGet 包
Install-Package microsoft.AspNetCore.Identity.EntityFrameworkCore
Identity 框架 Nuget 包
3.定义实体配置类
和 EF Core 一样,定义实体的配置类,当然这里也可以不配置,直接叫他使用默认的配置
IdentityRole 实体配置类:
public class MyRoleConfig : IEntityTypeConfiguration<MyRole>
{
public void Configure(EntityTypeBuilder<MyRole> builder)
{
builder.ToTable("T_MyRole");//指定表名
}
}
IdentityUser 实体配置类:
public class MyUserConfig : IEntityTypeConfiguration<MyUser>
{
public void Configure(EntityTypeBuilder<MyUser> builder)
{
builder.ToTable("T_MyUser");//指定表名
}
}
4.创建 Identity 框架的 DbContext 类
继承 IdentityDbContext 泛型类,其中第一个泛型是 IdentityUser<TKey> 或者继承该实体的类,第二个泛型是 IdentityRole<TKey> 类型 或继承该实体的类,第三个泛型是 TKey,也就是主键类型
//继承IdentityDbContext<MyUser, MyRole, long>
//这里的继承类后面需要加上这三个泛型, 前两个添加是为了使用我们实体里面所自定义的属性,最后一个是主键的类型
//你不添加的话,他就默认你的实体类是 IdentityUser, IdentityRole, 而不是继承类之后的类
public class MyIdentityDbContext : IdentityDbContext<MyUser, MyRole, long>
{
//为了让我在其他程序集中调用该 DBContext, 使用 DbContextOptions 类型
public MyIdentityDbContext(DbContextOptions dbContextOptions): base(dbContextOptions)
{
}
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.ApplyConfigurationsFromAssembly(this.GetType().Assembly);
}
}
在 EF Core 中,我们都是通过去实例化该 DbContext 类来进行对数据库的操作,但是在 Identity 框架中,给我们提供了 UserManager<T1>, 和 RoleManager<T2> 这两个泛型类,通过调用里面的方法,可以更加简便的对数据库进行操作,其中 T1 和 T2 分别是 IdentityUser<TKey> 或者继承该实体的类,第二个泛型是 IdentityRole<TKey> 类型 或继承该实体的类,比如我这里就是 MyUser 实体类和 MyRole 实体类。
5.调用 Migration 工具进行迁移
我这里由于是将 Identity 对于数据库的配置以及定义单独放在一个程序集中的,方便其他程序集调用,且可以更加灵活的使用不同的数据库,不需要在 DbContext 类中 重写 OnConfiguring(DbContextOptionsBuilder optionsBuilder) 对调用那个数据库写死,这样虽然更灵活,但是对于我们迁移就会更加的麻烦,需要在该程序集中单独定义一个类,来帮助我们迁移,操作如下
调用 Migration 工具包
Install-Package Microsoft.EntityFrameworkCore.Tools
定义继承 IDesignTimeDbContextFactory<T> 接口的类, T 即指你需要迁移的那个 DbContext,在该类中定义你对数据库的引用,使用什么数据库就调用该数据库对应的包即可
public class IdentityDesignTimeDbContextFactory : IDesignTimeDbContextFactory<MyIdentityDbContext>
{
public MyIdentityDbContext CreateDbContext(string[] args)
{
DbContextOptionsBuilder<MyIdentityDbContext> dbContextOptionsBuilder = new DbContextOptionsBuilder<MyIdentityDbContext>();
//我这里使用的是 SqlServer 数据库,调用的 Microsoft.EntityFrameworkCore.SqlServer 包
dbContextOptionsBuilder.UseSqlServer("Data Source=.;Initial Catalog=demo1;Integrated Security=true;TrustServerCertificate=true");
MyIdentityDbContext myDBContext = new MyIdentityDbContext(dbContextOptionsBuilder.Options);
return myDBContext;
}
}
首先前提是在 DbContext 中声明参数为 DbContextOptions类型的构造器,如上述第四步所示。这样就可以在此处使用以及在其他程序集里去注册该服务,书写对数据库的配置,如下文第 6 步
//此处为第四步中 MyIdentityDbContext 类中的构造器,仅做示范
public MyIdentityDbContext(DbContextOptions dbContextOptions): base(dbContextOptions)
{
}
//注册 DbContextOptions 服务,来在其他程序集中书写对数据库的配置,仅做示范,具体到第六步代码开头
builder.Services.AddDbContext<MyIdentityDbContext>(options =>
{
options.UseSqlServer("Data Source=.;Initial Catalog=demo1;Integrated Security=SSPI;TrustServerCertificate=true");
});
将该程序集设为启动项,然后在程序包管理器控制台中调用 Add-Migration 名字, Update-Database 等命令即可(当然还有其他命令这里不做赘述)
在调用 Update-database 命令,成功即到数据库中查看对应表有没有被生成
6.将 Identity 所需要的服务注册进去
具体如下述代码
注意:对于服务器的配置需要在 Programs 里面配置(如下),不能在 DbContext 里面配置,不然对于 Identity 框架接下来的服务注册会报错。
builder.Services.AddDbContext(options =>
{
options.UseSqlServer(“Data Source=.;Initial Catalog=demo1;Integrated Security=SSPI;TrustServerCertificate=true”);
});
//注册 Identity 框架-----------------------
//首先将 写在其他程序集的 DbContext 给注册进来,书写对数据库的配置,更加灵活
builder.Services.AddDbContext<MyIdentityDbContext>(options =>
{
options.UseSqlServer("Data Source=.;Initial Catalog=demo1;Integrated Security=SSPI;TrustServerCertificate=true");
});
//注册进行数据保护
builder.Services.AddDataProtection();
//builder.Services.AddIdentity(); 这种一般使用在 MVC 里面,而不是前后端分离中,它里面会提供一些界面等的操作
//这里需要添加上对登录验证操作做一些变化,比如我这里登录用的实体是 MyUser
builder.Services.AddIdentityCore<MyUser>(options =>
{
//如果使用他原本的密码的操作,会比较麻烦,他原本的密码需要你搞得创造的老复杂了,所以这里对其密码进行简化,也可以不设置
//下面都是对密码等做的一些操作,可以去查文档
//这里设置密码输入错误几次,写几个举例
options.Lockout.MaxFailedAccessAttempts = 5;//输错几次锁定
options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromSeconds(10);//锁定时间
//
options.Password.RequireDigit = false;
options.Password.RequireLowercase = false;
options.Password.RequireNonAlphanumeric = false;
options.Password.RequireUppercase = false;
options.Password.RequiredLength = 6;
/*
PasswordResetTokenProvider : 重置密码是发送给你的 Token,也就是 验证码
注意:
这里在注册时候将 options.Tokens.PasswordResetTokenProvider 设置为 TokenOptions.DefaultEmailProvider, 返回的才是数值
options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider;
设置该默认值结果:403797
不设置该值的结果:CfDJ8LYtD6oWUe1GqPBiQxTKiPC1v0835ryQmJ8kuK86CKme6Ik6FDhmfY1uAIf01HpxshNC0/Rv4FWbwf3PNE9TYQqtYt1lX8JHwJ1ekAk6b0H6qi6gAViIMxOUFwhy4c5hMc8Fziw9r2plFzfnEwok3ps8TJaqyH+WVcNp9O4rsBvzBgg9g8dnFgC5qFi/Jvmfeg==
不设置该值时一般是需要生成一个链接的时候使用的
*/
options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider;
options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider;
});
//接下来和实体建立起关系
//参数: 登录验证, 角色权限验证, 服务集合
IdentityBuilder iBuilder = new IdentityBuilder(typeof(MyUser), typeof(MyRole), builder.Services);
//将 实体与 DbContext 连接在一起: iBuilder.AddEntityFrameworkStores<MyIdentityDbContext>()
//iBuilder.AddUserManager<UserManager<MyUser>>(): 添加登录验证的管理
//iBuilder.AddRoleManager<RoleManager<MyRole>>(): 添加角色权限的管理
//在此处添加对 UserManager 和 RoleManager 类的服务,方便我们使用
iBuilder.AddEntityFrameworkStores<MyIdentityDbContext>().AddDefaultTokenProviders().AddUserManager<UserManager<MyUser>>().AddRoleManager<RoleManager<MyRole>>();
//看上面哪一行代码,之前我们知道,可以直接实例化 DbContext 来调用里面的 实体来完成操作,但是这里可以不这么做
//为什么后面调用 .AddUserManager<UserManager<MyUser>>().AddRoleManager<RoleManager<MyRole>>();,就是因为我们可以使用 Identity 框架给我们提供的 UserManager 和 UserManager 类来操作对应的实体
//----------------------------------------
7.使用 UserManager 和 RoleManager 来操纵表数据
下面为示例,具体方法使用的时候去查一下文档或者到该类中去看即可,英文意思即字面意思
[Route("api/[controller]/[action]")]
[ApiController]
public class ValuesController : ControllerBase
{
//将需要的类注入进来,这里就不需要使用 DbContext
private readonly UserManager<MyUser> userManager;
private readonly RoleManager<MyRole> roleManager;
private readonly IHostEnvironment hostEn;
public ValuesController(RoleManager<MyRole> roleManager, UserManager<MyUser> userManager, IHostEnvironment hostEn)
{
this.userManager = userManager;
this.roleManager = roleManager;
this.hostEn = hostEn;
}
[HttpPost]
public async Task<ActionResult<ObjectResult>> Test()
{
//查看有没有 admin 角色,如果没有,就给其加入
bool isHas = await this.roleManager.RoleExistsAsync("admin");
if (!isHas)
{
MyRole myRole = new MyRole();
myRole.Name = "admin";
myRole.NormalizedName = "admin";
myRole.ConcurrencyStamp = "1";
//添加进去,创建改行并添加进去
IdentityResult result = await this.roleManager.CreateAsync(myRole);//创建角色
//如果没有成功返回个报错
if (!result.Succeeded)
{
//BadResult 是 Controller 父类中的方法,其返回值继承于 ObjectResult
//按道理不应该将报错信息反馈上去,应该用日志记录,报错反馈个字符串即可
return BadRequest(result.Errors);
}
}
//查看有没有 pengmingxing 的用户,如果有,就返回,没有就创建,并且其角色为 admin
MyUser user = await this.userManager.FindByNameAsync("pengmingxing");//根据用户名查找对应用户
if (user == null)
{
MyUser myUser = new MyUser();
myUser.UserName = "pengmingxing";
myUser.Email = "2785218977@qq.com";
myUser.EmailConfirmed = true;//输入密码需要邮箱验证
myUser.PhoneNumber = "88888888888";
myUser.WeiChart = "88888888";
//这里使用的 CreateAsync 创建用户这个方法可以不设置密码,比如手机验证码登录
//IdentityResult result = await this.userManager.CreateAsync(myUser);//创建用户
//如果相设置密码,可以使用另外一个 重写
IdentityResult result = await this.userManager.CreateAsync(myUser, "888888");//创建用户
if (!result.Succeeded)
{
return BadRequest(result.Errors);
}
//将该用户和 admin 角色绑定在一起
result = await this.userManager.AddToRoleAsync(myUser, "admin");//把用户添加到角色
if (!result.Succeeded)
{
return BadRequest(result.Errors);
}
}
else
{
//判断用户是不是某个角色,没有则创建
bool isRole = await userManager.IsInRoleAsync(user, "admin");
if (!isRole)
{
//将该用户和 admin 角色绑定在一起
IdentityResult result = await this.userManager.AddToRoleAsync(user, "admin");//把用户添加到角色
if (!result.Succeeded)
{
return BadRequest(result.Errors);
}
}
}
return Ok();
}
[HttpPost]
public async Task<ActionResult<string>> CheckUserPassword(string userName, string password)
{
//查看一下有没有对应用户
MyUser user = await userManager.FindByNameAsync(userName);
if (user == null)
{
//有些人会恶意的去黑网站,所以这里在正式环境里最好没有用户名不要给特别明确的信息
if (hostEn.IsDevelopment())
{
return BadRequest("用户名不存在");
}
else
{
return BadRequest();
}
}
//先判断下该用户有没有被封,被锁住
bool isLock = await userManager.IsLockedOutAsync(user);
if (isLock)
{
return BadRequest($"该用户已被锁住,锁住结束时间{user.LockoutEnd}");
}
//用户名存在就确定下密码对不对
bool isTrue = await userManager.CheckPasswordAsync(user, password);
if (isTrue)
{
//如果登录成功,将记录的登录失败数据删除
await userManager.ResetAccessFailedCountAsync(user);
return Ok("登录成功");
}
else
{
//记录一下输入错误数据
await userManager.AccessFailedAsync(user);
//获取登录失败次数
int failNum = await userManager.GetAccessFailedCountAsync(user);
//userManager.GetAccessFailedCountAsync(user);该方法记录错误次数,超过固定次数后会给他锁定该用户
//可以在前面定义的时候就将其设置,也可以使用期默认值,也可以灵活的使用里面的方法,来设置锁定时间
return BadRequest("用户名或密码错误错误");
}
}
[HttpPost]
public async Task<ActionResult<string>> SendPasswordToken(string userName)
{
//寻找下该用户
MyUser user = await userManager.FindByNameAsync(userName);
if (user == null)
{
if (hostEn.IsDevelopment())
{
return BadRequest("用户名不存在");
}
return BadRequest();
}
//获取到 Token
string token = await userManager.GeneratePasswordResetTokenAsync(user);
//这里就不将 Token 发送了,直接写在 控制台上
/*
注意:
这里在注册时候将 options.Tokens.PasswordResetTokenProvider 设置为 TokenOptions.DefaultEmailProvider, 返回的才是数值
options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider;
设置该默认值结果:403797
不设置该值的结果:CfDJ8LYtD6oWUe1GqPBiQxTKiPC1v0835ryQmJ8kuK86CKme6Ik6FDhmfY1uAIf01HpxshNC0/Rv4FWbwf3PNE9TYQqtYt1lX8JHwJ1ekAk6b0H6qi6gAViIMxOUFwhy4c5hMc8Fziw9r2plFzfnEwok3ps8TJaqyH+WVcNp9O4rsBvzBgg9g8dnFgC5qFi/Jvmfeg==
下面第二种情况一般是需要生成一个连接的时候使用的
*/
Console.WriteLine(token);
return Ok();
}
/// <summary>
/// 获取到生成的 Token,然后修改密码
/// 这个 Token 不知道保存在那个表里的,有可能保存在对应的表数据的字段里,然后给他加密了,防止有些人窃取
/// 或者其内部有个空间,专门给他保存下来了
/// </summary>
/// <param name="myUser"></param>
/// <param name="token"></param>
/// <param name="newPassword"></param>
/// <returns></returns>
[HttpPut]
public async Task<ActionResult<string>> ResetPasswordByToken(string myUser, string token, string newPassword)
{
MyUser user = await userManager.FindByNameAsync(myUser);
if (user == null)
{
if (hostEn.IsDevelopment())
{
return BadRequest("用户名不存在");
}
return BadRequest();
}
//重置用户名
IdentityResult result = await userManager.ResetPasswordAsync(user, token, newPassword);
if (result.Succeeded)
{
//清空之前的输错密码记录
await userManager.ResetAccessFailedCountAsync(user);
return Ok("密码修改成功");
}
else
{
//失败就记录下来
await userManager.AccessFailedAsync(user);
return BadRequest("密码修改失败");
}
}
}