当前位置: 首页 > article >正文

如何用.NET Core Identity实现定制化的用户身份验证系统

目录

初识标识框架(Identity)

重置密码操作

JWT基本使用

Swagger添加报文头

初识标识框架(Identity)

.net core Identity是一个完整的身份验证和授权框架,它帮助开发人员处理用户的登录、注册、角色管理、权限控制以及其他与用户身份相关的操作,标识框架采用基于角色访问控制(Role-Based Access Control,简称RBAC)策略,内置了对用户、角色等表的管理以及相关的接口,支持外部登录、2FA等。标识框架使用EF Core对数据库进行操作,因此标识框架支持几乎所有数据库。

在日常开发中要区分以下以下两种控制策略:

1)Authentication:对访问者的用户身份进行验证,“用户是否登录成功”。

2)Authorization:验证访问者的用户身份是否有对资源访问的访问权限,“用户是否有权限访问这个地址”。

接下来在包管理器下载这个标识框架即可,如果.net core版本过低的话,该包也降低版本即可:

然后这里我们创建一个用户和角色的实体类,分别去继承IdentityUser和IdentityRole,可以增加自定义属性,如下所示:

然后创建继承自IdentityDbContext的类,使用泛型参数来指定自定义的用户类和角色类:

namespace webapi_study
{
    public class MyDbContext: IdentityDbContext<MyUser, MyRole,long>
    {
        public MyDbContext(DbContextOptions<MyDbContext> options) : base(options)
        {
        }
    }
}

然后我们把我们这个dbcontext连接到数据库之后,配置用户和角色相关的设置,然后迁移数据库里即可,如下所示:

builder.Services.AddDbContext<MyDbContext>(options =>
{
    string connStr = builder.Configuration.GetSection("ConStr").Value;
    options.UseMySQL(connStr);
});
builder.Services.AddDataProtection(); // 添加数据保护服务
builder.Services.AddIdentityCore<MyUser>(options =>
{
    options.Password.RequireDigit = true; // 密码至少包含一个数字
    options.Password.RequiredLength = 6; // 密码至少包含6个字符
    options.Password.RequireNonAlphanumeric = false; // 密码可以不包含非字母数字字符
    options.Password.RequireUppercase = false; // 密码可以不包含大写字母
    options.Password.RequireLowercase = false; // 密码可以不包含小写字母
    options.Tokens.PasswordResetTokenProvider = TokenOptions.DefaultEmailProvider; // 密码重置令牌提供程序
    options.Tokens.EmailConfirmationTokenProvider = TokenOptions.DefaultProvider; // 邮箱确认令牌提供程序
    options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); // 默认锁定时间
    options.Lockout.MaxFailedAccessAttempts = 5; // 最大失败尝试次数
    options.Lockout.AllowedForNewUsers = true; // 新用户允许锁定
});
IdentityBuilder identityBuilder = new IdentityBuilder(typeof(MyUser), typeof(MyRole), builder.Services);
identityBuilder.AddEntityFrameworkStores<MyDbContext>().AddDefaultTokenProviders()
    .AddUserManager<UserManager<MyUser>>().AddRoleManager<RoleManager<MyRole>>(); // 添加EF Core存储和默认令牌提供程序

执行Add-migration init和update-database命令将继承的实体类迁移到数据库之后,可以看到数据库已经生成了一堆用户和角色权限设置的表了:

接下来初始化系统中的角色和用户权限配置:检查角色是否存在、检查并创建用户、将用户添加到角色、返回成功,如下所示:

[HttpPost]
public async Task<ActionResult<string>> Test()
{
    if (await roleManager.RoleExistsAsync("admin") == false)
    {
        var result = await roleManager.CreateAsync(new MyRole { Name = "admin" });
        if (!result.Succeeded) return BadRequest("roleManager CreateAsync Failed");

    }
    MyUser user = await userManager.FindByNameAsync("zhangsan");
    if (user == null) {
        user = new MyUser { UserName = "zhangsan" };  // 确保邮箱不为空
        var result = await userManager.CreateAsync(user, "123456");
        if (!result.Succeeded) {
            var errors = string.Join(", ", result.Errors.Select(e => e.Description));
            return BadRequest($"userManager CreateAsync Failed: {errors}");
        }
    }
    if (!await userManager.IsInRoleAsync(user, "admin"))
    {
        var result = await userManager.AddToRoleAsync(user, "admin");
        if (!result.Succeeded) return BadRequest("userManager AddToRoleAsync Failed");
    }
    return "success";
}

接下来我们通过调用下面的接口来校验,输入的用户和密码是否正确,如下所示,如果是在开发环境下就正常提示用户不存在即可,当尝试输入的用户密码过多,超出我们的限制之后,账户就会被锁定:

[HttpPost]
public async Task<ActionResult> CheckPwd(CheckPwdRequest request)
{
    string userName = request.UserName;
    string password = request.Password;
    var user = await userManager.FindByNameAsync(userName);
    if (user == null)
    {
        if (env.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); // 登录失败次数+1
        return BadRequest("用户名或密码错误");
    }
}

重置密码操作

在标识框架这如果想进行重置密码的操作,需要参考以下流程:

1)生成重置Token

2)Token发给客户(邮件、短信等),形式:链接、验证码等

3)根据Token完成密码的重置

如下我们模拟手机发送验证码的方式,获取数据库当中的重置密码的token:

[HttpPost]
public async Task<ActionResult> SendResetPasswordToken(string userName)
{
    var user = await userManager.FindByNameAsync(userName);
    if (user == null) return BadRequest("用户不存在");
    string token = await userManager.GeneratePasswordResetTokenAsync(user);
    return Ok(token);
}

如下我们输入我们的用户名,然后点击接口触发重置密码指令,返回我们重置的令牌:

接下来我们编写重置密码的接口:

[HttpPost]
public async Task<ActionResult> ResetPassword(string userName, string token, string newPwd)
{
    var user = await userManager.FindByNameAsync(userName);
    if (user == null) return BadRequest("用户不存在");
    IdentityResult result = await userManager.ResetPasswordAsync(user, token, newPwd);
    if (result.Succeeded) { 
        await userManager.ResetAccessFailedCountAsync(user);
        return Ok("重置密码成功");
    } else {
        await userManager.AccessFailedAsync(user);
        return Ok("重置密码失败");
    }
}

输入账号之后,点击获取重置token的接口,输入token,然后再输入重置密码即可进行密码重置:

JWT基本使用

JWT:是一种开放标准用于在网络应用环境中传递声明,它是一种轻量级的、安全的认证和授权机制,广泛用于现代web应用程序中,JWT把登录信息(也称作令牌)保存再客户端,为了防止客户端的数据造假,保存再客户端的令牌经过了签名处理,而签名的密钥只有服务器才知道,每次服务器端收到客户端提交过来的令牌的时候都要检查一下签名,以下是JWT的使用流程:

1)用户通过用户名和密码登录系统,服务器验证身份后生成一个JWT

2)服务器将JWT返回给用户

3)用户在后续的请求中,将该JWT放入HTTP请求的Authorization头中(通常使用Bearer作为前缀)

4)服务器验证JWT的有效性(比如检查签名和过期时间),如果有效则允许访问受保护的资源

接下来我们NuGet安装如下操作JWT的包:

接下来我们创建一个JWT的类,并且把类中相关的属性设置在配置文件当中:

接下来我们在入口文件当中进行如下配置,读取JWT文件并设置对应的JWT配置:

builder.Services.Configure<JWTSettings>(builder.Configuration.GetSection("JWT")); // 配置JWT设置
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>
{
    var jwtSettings = builder.Configuration.GetSection("JWT").Get<JWTSettings>();
    byte[] keyBytes = Encoding.UTF8.GetBytes(jwtSettings.SecKey);
    var secKey = new SymmetricSecurityKey(keyBytes);
    options.TokenValidationParameters = new()
    {
        ValidateIssuer = false, // 验证发行者
        ValidateAudience = false, // 验证受众
        ValidateLifetime = true, // 验证过期时间
        ValidateIssuerSigningKey = true, // 验证签名密钥
        IssuerSigningKey = secKey // 签名密钥
    };
});

然后在app.UseAuthorization();这行代码之前添加app.UseAuthentication(); 开启身份验证:

app.UseAuthentication(); // 开启身份验证
app.UseAuthorization();

接下来我们就在控制器当中执行开始登录的代码,如下所示:

[HttpPost]
public ActionResult<string> Login(string userName, string password)
{
    if (userName == "zhangsan" && password == "123456")
    {
        List<Claim> claims = new List<Claim>();
        claims.Add(new Claim(ClaimTypes.NameIdentifier, "1"));
        claims.Add(new Claim(ClaimTypes.Name, userName));

        string key = jwtSettings.Value.SecKey;
        DateTime expiore = DateTime.Now.AddSeconds(jwtSettings.Value.ExpireSeconds);
        byte[] secBytes = Encoding.UTF8.GetBytes(key);
        var secKey = new SymmetricSecurityKey(secBytes);
        var creds = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature);
        var token = new JwtSecurityToken(claims: claims, expires: expiore, signingCredentials: creds);
        string jwt = new JwtSecurityTokenHandler().WriteToken(token);
        return jwt;
    }
    else
    {
        return BadRequest("用户名或密码错误");
    }
}

执行登录之后,直接返回一个jwt加密后的字符串,如下所示:

接下来就需要在服务器解析客户端提交过来的这个jwt字符串,这里我们可以在需要登录才能访问的控制器类或者Action方法上添加:[Authorize],如下所示:

然后测试登录和访问的时候,需要通过如下格式自定义报文头:Authorization的值为"Bearer JWTToken", Authorization的值中的"Bearer"和JWT令牌之间一定要通过空格分隔。前后不能多出来额外的空格、换行等。

注意:制器类上标法[Authorize],则所有操作方法都会被进行身份验证和授权验证;对于标注了[Authorize]控制器中,如果其中某个操作方法不想被验证,可以在操作方法上添如[AllowAnonymous]。如果没有在控制器类上标注[Authorize],那么这个控制器中的所有操作方法都允许被自由地访问;对于没有标注[Authorize]的控制器中,如果其中某个操作方法需要被验证,我们也可以在操作方法上添加[Authorize]。

ASP.NET Core会按照HTTP协议的规范,从Authorization取出来令牌,并且进行校验、解析,然后把解析结果填充到User属性中,这一切都是ASP.NETCore完成的,不需要开发人员自己编写代码。但是一旦出现401,没有详细的报错信息,很难排查,这是初学者遇到的难题。

解决JWT无法提前撤回的问题:由于JWT本质上是一种自包含的认证机制,它用于在客户端和服务器之间安全地传递信息,由于JWT是由服务器签发并且可以在客户端进行存储,因此它本身没有撤回(即失效)机制,这意味着一旦JWT被签发,它通常会一直有效直到过期时间为止,为了解决这个情况可以采用以下思路:

在用户表中增加一个整数类型的列JWTVersion,代表最后一次发放出去的令牌的版本号,每次登录、发放令牌的时候都让JWTVersion的值自增,同时将JWTVersion的值也放到JWT令牌的负载中;

1)当执行禁用用户、撤回用户的令牌等操作的时候,把这个用户对应的JWTVersion列的值自增;

2)当服务器端收到客户端提交的WT令后,先把JWT令牌中的JWTVersion值和数据库中JWTVersion的值做一下比较,如果JWT令牌中JWTVersion的值小于数据库中JWTVersion的值,就说明这个JWT令牌过期了。

实现步骤如下所示:

1)为用户实体User类增加一个long类型的属性JWTVersion.

2)修改登录并发放令牌的代码,把用户的JWTVersion属性的值自增,并且把JWTVersion的值写入JWT令牌。

3)编写一个操作筛选器,统一实现对所有的控制器的操作方法中JWT令牌的检查操作。把JWTValidationFilter注册到Program.cs中MVC的全局筛选器中

Swagger添加报文头

接下来我们可以给Swagger添加对应的身份校验的设置,首先我们先在入口文件中对Swagger进行相应的配置,如下所示:

// 设置Swagger添加JWT认证信息报文头
builder.Services.AddSwaggerGen(options =>
{
    var scheme = new OpenApiSecurityScheme()
    {
        Description = "JWT认证信息报文头",
        Reference = new OpenApiReference()
        {
            Id = "Authorization",
            Type = ReferenceType.SecurityScheme
        },
        Scheme = "oauth2",
        Name = "Authorization",
        In = ParameterLocation.Header,
        Type = SecuritySchemeType.ApiKey,
    };
    options.AddSecurityDefinition("Authorization", scheme);
    var requirement = new OpenApiSecurityRequirement();
    requirement[scheme] = new List<string>();
    options.AddSecurityRequirement(requirement);
});

配置完成之后,运行项目然后打开的Swagger文档中就会有Authorize进行输入JWT的按钮:

然后我们运行一个需要身份校验的接口,输入对应的JWT校验的字符串,可以看到我们请求的接口当中也会带上我们的身份校验的Authorization: 

FluentValidation使用

.net core中内置了对数据校验支持,在System.ComponentModel.DataAnnotations这个命名空间下, 比如[Required]、[EmailAddress] 、[RegularExpression],CustomValidationAttribute、IValidatableObject。不过其内置的校验机制,校验规则都是和模型类耦合在一起,违反“单一职责原则";很多常用的校验都需要编写自定义校验规则,而且写起来麻烦。FluentValidation用类似于EF Core中Fluent API的方式进行校验规则的配置,也就是我们可以把对模型类的校验放到单独的校验类中进行。在NuGet中安装如下包:

然后我们在入口文件当中开启自动校验:

builder.Services.AddFluentValidation(options => // 添加Fluent验证自动验证
{
    options.RegisterValidatorsFromAssembly(Assembly.GetEntryAssembly());
}); 

然后我们声明一个实体类,然后可以在声明一个对该实体类字段进行校验的请求类:

请求接口的时候可以看到外面输入的参数会自动进行校验,类似如下所示,报错的提示可自定义:


http://www.kler.cn/a/539512.html

相关文章:

  • C语言基础第04天:数据的输出和输出
  • 软件模拟I2C案例(寄存器实现)
  • C++ Primer sizeof运算符
  • ubuntu文件同步
  • 使用LLaMA Factory踩坑记录
  • C++ 中信号转异常机制:在磁盘 I/O 内存映射场景下的应用与解析
  • Vue 响应式渲染 - 条件渲染
  • PHP-综合3
  • PrimeFaces Poll组件实现周期性Ajax调用
  • S4 HANA金税接口
  • STM32的HAL库开发---高级定时器---互补输出带死区实验
  • 集成开发环境GoLand安装配置结合内网穿透实现ssh远程访问服务器
  • Stable Diffusion室内设计文生图实操
  • 5.【BUUCTF】[RoarCTF 2019]Easy Calc1
  • C# OpenCV机器视觉:多尺度细节提升
  • MFC 的 CListCtrl 控件,使用SetItemState 方法来设置选中某个 item,如何达到效果和鼠标点击一致
  • qml前后端数据交互
  • 第436场周赛:按对角线进行矩阵排序、将元素分配给有约束条件的组、统计可以被最后一个数位整除的子字符串数目、最大化游戏分数的最小值
  • 【C++篇】智能指针
  • Objective-C语言的云计算
  • openssl使用
  • 【HeadFirst系列之HeadFirstJava】第2天之类与对象-拜访对象村
  • 使用golang wails写了一个桌面端小工具:WoWEB, 管理本地多前端项目
  • YOLOV8 OpenCV + usb 相机 实时识别
  • JMeter常用函数整理
  • 高并发读多写少场景下的高效键查询与顺序统计的方案思路