.net core Identity框架使用

286 阅读5分钟

使用步骤

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数据库中英文全大写
Email
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("用户名或密码错误");
    }
}

示例:重置密码

步骤

  1. 生成重置Token(即验证码)
  2. 通过邮件、短信等向用户发送Token,形式:链接、验证码等
  3. 根据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);
        });
};