使用步骤
Microsoft.AspNetCore.Identity.EntityFrameworkCore
1、编写继承自IdentityUser<TKey>、IdentityRole<TKey>等的自定义类
//TKey代表主键的类型,如下面类型为long
public class MyRole: IdentityRole<long> {
}
public class MyUser: IdentityUser<long> {
//可以增加自定义属性
public string? WeiXinAccount { get; set; }
}
2、创建IdentityDbContext的派生类
注意:MyDbContext直接继承自IdentityDbContext,而不是DbContext。IdentityDbContext内置了基本的数据库增删改查,和对用户权限、角色的处理
//不要忘了<MyUser,MyRole,long>
public class MyDbContext: IdentityDbContext<MyUser,MyRole,long> {
public MyDbContext(DbContextOptions<MyDbContext> options) : base(options) {
//...
}
}
3、注册Identity框架相关的服务
1)注册DbContext服务
string connStr = @"Data Source=N30001084B002\SQL;AttachDbFilename=
D:\My Visual Studio\MySQLServer\MyInstance\DepartmentMIS.mdf;Integrated
Security=False;Connect Timeout=30;User Id =yanwenlong;Password =nrec1234.";
builder.Services.AddDbContext<MyDbContext>(opt => {
opt.UseSqlServer(connStr);
});
2)对密码强度做相关配置
注意,在注册DbContext服务后面注册Identity框架的服务
builder.Services.AddDataProtection();//密码加密
//是AddIdentityCore不是AddIdentity
builder.Services.AddIdentityCore<MyUser>(options => {
//跳过对用户名的验证,否则不允许中文
options.User.AllowedUserNameCharacters = null;
options.Lockout.MaxFailedAccessAttempts= 10;//输入错误密码10次锁定
options.Lockout.DefaultLockoutTimeSpan=TimeSpan.FromDays(1);//锁定一天
options.Password.RequireDigit = false;//密码不是必须得有数字
options.Password.RequireLowercase = false;//密码不是必须得有小写字母
options.Password.RequireNonAlphanumeric = false;//密码不是必须得有非数字、非字母,即特殊符号
options.Password.RequireUppercase = false;//密码不是必须得大写字母
options.Password.RequiredLength = 6;//密码最小长度6
//如果不配置此项,验证码特别长。如果是把重置链接发送到用户邮箱,那么就不用配置
//如果需要用户输入验证码,则需要配置
options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider;
//发邮件重置密码的规则
options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultEmailProvider;
});
3)将Identity框架和实体建立关系
var idBuilder = new IdentityBuilder(typeof(MyUser), typeof(MyRole), builder.Services);
idBuilder.AddEntityFrameworkStores<MyDbContext>()
.AddDefaultTokenProviders().AddRoleManager<RoleManager<MyRole>>()
.AddUserManager<UserManager<MyUser>>();
4、为Identity添加扩展方法,检查某操作是否执行成功
只要某步操作是返回<IdentityResult> 类型,就可以链式调用此方法来保证操作已经正确执行
namespace Microsoft.AspNetCore.Identity {
public static class IdentityHelper {
public static async Task CheckAsync(this Task<IdentityResult> task) {
var r = await task;
if (!r.Succeeded) {
throw new Exception(JsonSerializer.Serialize(r.Errors));
}
}
}
}
5、在控制器中通过RoleManager、UserManager等来进行数据操作
注意:可以通过MyDbContext类来操作数据库,不过框架中提供了RoleManager、UserManager等类来简化对数据库的操作
1)通过构造方法注入UserManager、RoleManager、IWebHostEnvironment服务
IWebHostEnvironment可用来判断是开发环境还是生产环境,来给出不同的响应信息
2)编写创建角色等业务代码
await roleManager.RoleExistsAsync("admin");
await roleManager.CreateAsync(role);
6、执行Add-Migration、Update-Database等命令执行EF Core的数据库迁移
API
IdentityUser类:含有Id、姓名、邮箱、手机号、密码、锁定机制这些属性
| 属性名 | 含义 |
|---|---|
| Id | |
| UserName | |
| NormalizedUserName | 数据库中英文全大写 |
| NormalizedEmail | 数据库中英文全大写 |
| EmailConfirmed | 用户是否确认邮箱地址 |
| PasswordHash | 密码哈希 |
| SecurityStamp | 安全戳,用户凭证(密码等)变化时会随之变化 |
| ConcurrencyStamp | 每当用户被持久化到存储时,必须更改的随机值 |
| PhoneNumber | 手机号 |
| PhoneNumberConfirmed | 用户是否确认手机号,默认false |
| TwoFactorEnabled | 是否启用双重身份验证,默认false |
| LockoutEnd | 锁定结束时间,是DateTimeOffset类型 |
| LockoutEnabled | 是否对此用户启用锁定机制,默认true |
| AccessFailedCount | 登录失败计数 |
IdentityRole类
| 属性名 | 含义 |
|---|---|
| Id | |
| Name、NormalizedName | 角色名 |
| ConcurrencyStamp | 每当角色被持久化到存储时,必须更改的随机值 |
IdentityResult类
Identity框架的很多方法都返回IdentityResult类型
| 属性名 | 含义 |
|---|---|
| Errors | 这次调用失败,Errors中会包含错误信息,可能不止一个,是一个集合 |
SignInResult类
登录失败:SignInResult.Failed
IdentityError类:错误信息
示例:用户登录
[HttpPost]
public async Task<ActionResult> CheckPwd(CheckPwdRequest req) {
string userName = req.UserName;
string password = req.Password;
var user = await userManager.FindByNameAsync(userName);
if (user == null) {
//IWebHostEnvironment服务判断生产或开发环境
if (hostEnvironment.IsDevelopment()) {
return BadRequest("用户名不存在");
}
else {
return BadRequest("生产环境用户名不存在");//更安全
}
}
if(await userManager.IsLockedOutAsync(user)) {
return BadRequest($"用户已经被锁定,锁定结束时间{user.LockoutEnd}");
}
if(await userManager.CheckPasswordAsync(user, password)) {
await userManager.ResetAccessFailedCountAsync(user);//重置登录失败次数
return Ok("登录成功");
}
else {
await userManager.AccessFailedAsync(user);//记录一次登录失败
return BadRequest("用户名或密码错误");
}
}
示例:重置密码
步骤
- 生成重置Token(即验证码)
- 通过邮件、短信等向用户发送Token,形式:链接、验证码等
- 根据Token完成密码的重置
1)编写发送重置密码Token的方法
[HttpPost]
public async Task<ActionResult> SendResetPasswordToken(string userName) {
var user = await userManager.FindByNameAsync(userName);
if (user == null) {
return BadRequest("用户名不存在");
}
//生成重置密码的Token
string token=await userManager.GeneratePasswordResetTokenAsync(user);
Console.WriteLine($"验证码是{token}");
return Ok();
}
2)编写重置密码的方法
[HttpPut]
public async Task<ActionResult> ResetPasssword(string userName,string token,string newPassword) {
var user=await userManager.FindByNameAsync(userName);
if (user == null) {
return BadRequest("用户名不存在");
}
//真正完成重置密码
var result=await userManager.ResetPasswordAsync(user,token,newPassword);
if (result.Succeeded) {
await userManager.ResetAccessFailedCountAsync(user);
return Ok("密码重置成功");
}
else {
await userManager.AccessFailedAsync(user);//记录登录失败一次
return BadRequest("密码重置失败");
}
}
示例:Identity+JWT实现用户登录
后端
1、创建登录请求实体
public record LoginRequest(string username, string password);
2、在登录方法中检查用户是否存在,存在则生成一个jwt令牌(字符串)并返回给客户端
[HttpPost]
public async Task<ActionResult<string>> Login(LoginRequest req) {
var user = await userManager.FindByNameAsync(req.username);
if (user == null) {
return BadRequest("用户名或密码错误");
}
if (await userManager.CheckPasswordAsync(user, req.password)) {
await userManager.ResetAccessFailedCountAsync(user).CheckAsync();
//user.JWTVersion++;//!!
//await userManager.UpdateAsync(user);//保存JWTVersion
//生成jwt令牌
var claims = new List();
claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
claims.Add(new Claim(ClaimTypes.Name, user.UserName));
//将JWTVersion版本写入jwt令牌
//claims.Add(new Claim("JWTVersion", user.JWTVersion.ToString()));
var roles = await userManager.GetRolesAsync(user);//获取用户角色
foreach (var role in roles) {
claims.Add(new Claim(ClaimTypes.Role, role));
}
string key = jwtSettingsOpt.Value.SecKey;
DateTime expires = DateTime.Now.AddSeconds(jwtSettingsOpt.Value.ExpireSeconds);//过期时间
byte[] secBytes = Encoding.UTF8.GetBytes(key);
var secKey = new SymmetricSecurityKey(secBytes);
var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature);
var tokenDescriptor = new JwtSecurityToken(claims: claims,expires: expires, signingCredentials: credentials);
string jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
return jwt;
}
else {
await userManager.AccessFailedAsync(user).CheckAsync();
return BadRequest("用户名或密码错误");
}
}
前端
注意:正式项目中在登录组件中生成token,应该是全局的,别的组件发送请求时要用到这个token
登录成功,则取出token、建立websocket连接(将token作为配置项)、监听服务端推送的消息
const loginWithJWT = function () {
//loginData是存储用户名、密码的对象
const payload = state.loginData;
//向登录方法发送请求
axios
.post("https://localhost:7142/api/Demo/Login", payload)
.then(async (res) => {
//取出token
const token = res.data;
const options = {
skipNegotiation: true,
transport: signalR.HttpTransportType.WebSockets,
};
//将token作为配置项,正式项目要用登录组件中的token值
options.accessTokenFactory = () => token;
//建立websocket连接、监听服务端消息...
})
.catch((err) => {
console.log(err);
});
};