Authentication与Authorization 1.Authentication对访问者的用户身份进行验证,“用户是否登陆成功”。 2.Authorization验证访问者用户身份是否又对资源访问的访问权限,“用户是否有权限访问这个地址”。
标识(Identity)框架 1.标识(Identity)框架:采用基于角色的访问控制(Role-Based Access Control,简称RBAC)策略,内置了对用户、角色等表的管理以及相关的接口,支持外部登录、2FA等。 2.标识框架使用EF Core对数据进行操作,因此标识框架支持几乎所有数据库。
Identity框架使用 1.IdentityUser<TKey>、IdentityRole<TKey>,TKey代表主键的类型。我们一般编写继承自IdentityUser<TKey>、IdentityRole<TKey>等的自定义类,可以增加自定义属性。 2.NuGet安装 Microsoft.AspNetCore.Identity.EntityFrameworkCore 3.创建继承自IdentityDbContext的类。 4.可以通过IdDbContext类来操作数据库,不过框架中提供了RoleManager、UserManager等类来简化对数据库的操作。 5.部分方法的返回值为Task<IdentityResult>类型,查看、讲解IdentityResult类型定义。
创建用户实体类User和角色实体类Role. 分别继承自IdentityUser<long>、IdentityRole<long>的User类和Role类。
1 2 3 4 5 public class User:IdentityUser<long> { public DateTime CreationTime { get; set; } public string? NickName { get; set; } }
1 2 3 public class Role :IdentityRole <long >{ }
IdnetityUser中定义类了很多属性。
除了IdentityUser和IdentityRole之外,表示框架中还有很多其他实体类。
创建继承自IdentityDbContext的类,这是一个EF Core中的上下文类,我们可以通过这个类操作数据库。IdentityDbContext是一个泛型类,有3个泛型参数,分别代表用户类型、角色类型和主键类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 public class IdDbContext :IdentityDbContext <User ,Role ,long >{ public IdDbContext (DbContextOptions<IdDbContext> options ) : base (options ) { } protected override void OnModelCreating (ModelBuilder builder ) { base .OnModelCreating(builder); new UserConfig().Configure(builder.Entity<User>()); new RoleConfig().Configure(builder.Entity<Role>()); } }
标识框架中的方法有执行失败的可能,比如重置密码可能由于密码太简单而失败,因此标识框架中部分方法的返回值为Task类型。IdentityResult类型中有bool类型的Succeeded属性表示操作是否成功;如果操作失败,我们可以从Errors属性中获取错误的详细信息,由于有可能有多条错误信息,因此Errors是一个IEnumerable类型的属性。IdentityError类包含Code(错误码)和Description(错误的详细信息)这两个属性。
向依赖注入容器中注册与标识框架相关的服务。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 var builder = WebApplication.CreateBuilder(args);builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); IServiceCollection services = builder.Services; 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>>(); var app = builder.Build();if (app.Environment.IsDevelopment()){ app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthorization(); app.MapControllers(); app.Run();
然后执行数据库迁移Add-Migration createIdentity、Update-database等命令执行EF Core的数据库迁移,然后程序就会在数据库中生成多张数据库表。这些数据库表都由标识框架负责管理,开发人员一般不需要直接访问这些表。
编写控制器的代码。我们在控制器中需要对角色、用户进行操作,也需要输出日志,因此通过控制器的构造方法注入相关的服务。编写创建角色和用户的方法CreateUserRole。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 public class TestController : ControllerBase { private readonly ILogger<TestController> _logger; private readonly UserManager<User> _userManager; private readonly RoleManager<Role> _roleManager; public TestController ( ILogger<TestController> logger, UserManager<User> userManager, RoleManager<Role> roleManager ) { _logger = logger; _userManager = userManager; _roleManager = roleManager; } [HttpPost ] public async Task<IActionResult> CreateUserRole () { bool roleExists = await _roleManager.RoleExistsAsync("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 _userManager.FindByNameAsync("if" ); if (user == null ) { user = new User { UserName = "YOUXIANYU" , Email = "123456@QQ.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("注册成功" ); } }
6.编写处理登录请求的操作方法Login。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 [HttpPost ] public async Task<IActionResult> Login (LoginRequest loginRequest ){ string userName = loginRequest.UserName; string password = loginRequest.Password; try { var user = await _userManager.FindByNameAsync(userName); if (user == null ) { return NotFound($"用户名或密码错误" ); } var islocked = await _userManager.IsLockedOutAsync(user); if (islocked) { var lockoutEnd = await _userManager.GetLockoutEndDateAsync(user); return BadRequest($"用户已锁定,解锁时间:{lockoutEnd.Value.LocalDateTime} " ); } var success = await _userManager.CheckPasswordAsync(user, password); if (success) { return Ok("登录成功" ); } else { var r = await _userManager.AccessFailedAsync(user); if (!r.Succeeded) { return BadRequest("访问失败信息写入错误!" ); } else { return BadRequest("失败!" ); } } } catch (Exception ex) { _logger.LogError(ex, "登录过程发生异常" ); return StatusCode(500 , "登录失败,请稍后重试" ); } }
检查登录用户信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 [HttpPost ] public async Task<ActionResult<string >> CheckPwd(CheckPwdRequest req) { string userName = req.userName; string password = req.password; var user = await userConfig.FindByNameAsync(userName); if (user == null ) return NotFound($"用户{userName} 不存在" ); if (await userConfig.IsLockedOutAsync(user)) return BadRequest("用户已锁定" ); var success = await userConfig.CheckPasswordAsync(user, password); if (success) { await userConfig.ResetAccessFailedCountAsync(user); return Ok("密码正确" ); } else { await userConfig.AccessFailedAsync(user); var count = await userConfig.GetAccessFailedCountAsync(user); if (count >= 5 ) { await userConfig.SetLockoutEndDateAsync(user, DateTimeOffset.UtcNow.AddMinutes(5 )); return BadRequest("用户已锁定" ); } return BadRequest($"密码错误,还有{5 - count} 次机会" ); } }
实现密码的重置 1.生成重置Token 2.Token发给客户(邮件、短信等),形式:连接、验证码等。 3.根据Token完成密码的重置。
编写一个发送重置密码请求的操作方法SendResetPasswordToken.
1 2 3 4 5 6 7 8 9 10 [HttpPost ] public async Task<ActionResult> SendResetPasswordToken (string userName ) { var user = await userConfig.FindByNameAsync(userName); if (user == null ) return NotFound($"用户{userName} 不存在" ); string token = await userConfig.GeneratePasswordResetTokenAsync(user); Console.WriteLine($"验证码是{token} " ); return Ok("ok" ); }
调用GeneratePasswordResetTokenAsyncc方法来生成一个密码重置令牌,这个令牌会保存到数据库中,然后我们把这个令牌发送到用户邮箱。
编写重置密码的操作方法VerifyResetPasswordToken。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 [HttpPut ] public async Task<ActionResult> ResetPassword (string userName,string token,string newPassword ) { var user = await userConfig.FindByNameAsync(userName); if (user == null ) return NotFound($"用户{userName} 不存在" ); var r = await userConfig.ResetPasswordAsync(user, token, newPassword); if (r.Succeeded) { await userConfig.ResetAccessFailedCountAsync(user); return Ok("重置密码成功" ); } else { await userConfig.AccessFailedAsync(user); return BadRequest("重置密码失败" ); } }
JWT(Json Web Token) 1.JWT把登录信息(也称作令牌)保存在客户端。 2.为了防止客户端的数据造假,保存在客户端的令牌经过了签名处理,而签名的密钥只有服务器端才知道,每次服务器段收到客户端提交过来的令牌的时候都要检查一下签名。 3.基于JWT如何实现“登录”。
JWT实现登陆的流程如下。
客户端向服务端发送用户名、密码等请求登录。
服务器端校验用户名、密码,如果校验成功,则从数据库中取出这个用户的ID、角色等用户相关信息。
服务器段采用只有服务器端才知道的密钥来对用户信息的JSON字符串进行签名,形成签名数据。
服务器端把用户信息的JSON字符串和签名拼接到一起形成JWT,然后发送给客户端。
客户端保存服务器返回的JWT,并且在客户端每次向服务器端发送请求的时候都带上这个JWT。
每次服务器端收到浏览器请求中携带的JWT后,服务器端用密钥对JWT的签名进行校验,如果校验成功,服务器端则从JWT中的JSON字符串中读取出用户的信息。这样服务器端就知道这个请求对应的用户了,也就实现了登录的功能。
JWT的基本使用 先创建一个控制台程序用来创建JWT,在创建一个程序读取JWT。 .NET中进行JWT读取的NuGet包是:System.IdentityModel.Tokens.Jwt
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 var claims = new List<Claim>();claims.Add(new Claim(ClaimTypes.NameIdentifier, "6" )); claims.Add(new Claim(ClaimTypes.Name, "路飞" )); claims.Add(new Claim(ClaimTypes.Role, "User" )); claims.Add(new Claim(ClaimTypes.Role, "Admin" )); claims.Add(new Claim("jz" , "112233" )); string key = "hweorgnfhsoifhjafvbsfjawoighnbsdhfvklawhng" ; DateTime expires = DateTime.Now.AddDays(1 ); 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);Console.WriteLine(jwt);
运行程序
JWT被句点分隔成了3部分,分别是头部、负载和签名。JWT都是明文存储的,JWT中使用Base64URL算法对字符串进行编码,这个算法跟Base64算法基本相同,考虑到JWT可能会被放到URL中,而Base64有3个特殊字符+、/和=,它们在URL里面有特殊含义,因此我们需要从Base64中删除=,并且把+替换成-、把/替换成_。
把JWT字符串的头部和负载解码为可读的字符串。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 string jwt = Console.ReadLine()!;string [] segments = jwt.Split('.' );string head = JwtDecode(segments[0 ]); ;string payload = JwtDecode(segments[1 ]);Console.WriteLine("--------head--------" ); Console.WriteLine(head); Console.WriteLine("--------payload--------" ); Console.WriteLine(payload); string JwtDecode (string s ){ s=s.Replace('-' ,'+' ).Replace("/" ,"-" ); switch (s.Length % 4 ) { case 2 : s += "===" ; break ; case 3 : s += "=" ; break ; } var bytes = Convert.FromBase64String(s); return Encoding.UTF8.GetString(bytes); }
JWT的编码和解码规则都是公开的,而且负部分的Claim信息也是明文的,因此恶意攻击者可以对负载部分中的用户ID等信息进行修改,从而冒充其他用户的身份来访问服务器上的资源。因此,服务器端需要对部分进行校验,从而检查JWT是否被篡改了。 我们可以调用JwtSecurityTokenHandler类对JWT进行解码,因为它会在对JWT解码前对签名进行校验。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 string jwt =Console.ReadLine()!;string secKey = "hweorgnfhsoifhjafvbsfjawoighnbsdhfvklawhng" ;JwtSecurityTokenHandler tokenHandler = new (); TokenValidationParameters valParam = new (); var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secKey));valParam.IssuerSigningKey = securityKey; valParam.ValidateIssuer = false ; valParam.ValidateAudience = false ; ClaimsPrincipal claimsPrincipl = tokenHandler.ValidateToken(jwt, valParam, out SecurityToken sectToken); foreach (var claim in claimsPrincipl.Claims){ Console.WriteLine($"{claim.Type} ={claim.Value} " ); }
JWT机制让我们可以把用户的信息保存到客户端,每次客户端向服务器发送请求的时候,客户端只要把JWT发送到服务器端,服务器端就可以得知当前请求用户的信息,而通过签名的机制则可以避免JWT内存被篡改。
ASP.NET Core对于JWT的封装
配置JWT节点,节点下创建SigningKey、ExpireSeconds两个配置项,分别代表JWT的密钥和过期时间(单位:秒)。在创建配置类JWTOptions,包含SigningKey、ExpireSeconds两个属性。
NuGet:Microsoft.AspNetCore.Authentication.JwtBearer
第一步,在配置系统中配置一个名字为JWT的节点,并在节点下创建SigningKey、ExpireSeconds两个配置项,分别代表JWT的密钥和过期时间(单位为秒)。我们在创建一个对应JWT节点的配置类JWTOptions,类中包含SigningKey、ExpireSwconds这两个属性。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 { "JWT" : { "SigningKey" : "jlkerujoigsnvjlkasjfwehiojkgnsgss" , "ExpireSeconds" : "3600" } , "ConnectionStrings" : { "DefaultConnection" : "Server=localhost;Database=YourDatabaseName;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True;Encrypt=True;" } , "Logging" : { "LogLevel" : { "Default" : "Information" , "Microsoft.AspNetCore" : "Warning" } } , "AllowedHosts" : "*" }
1 2 3 4 5 public class JWTOptions { public string SigningKey { get ; set ; } public int ExpirSeconds { get ; set ; } }
第三步,编写代码对JWT进行配置,内容添加到Program.cs的builder.Build之前。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT" )); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(x => { var jwtOpt = builder.Configuration.GetSection("JWT" ).Get<JWTOptions>(); byte [] keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SigningKey); var secKey = new SymmetricSecurityKey(keyBytes); x.TokenValidationParameters = new TokenValidationParameters() { ValidateIssuer = false , ValidateAudience = false , ValidateLifetime = false , ValidateIssuerSigningKey = true , IssuerSigningKey = secKey }; }); var app = builder.Build();if (app.Environment.IsDevelopment()){ app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();
第4步,在Program.cs的app.UseAuthorization之前添加app.UseAuthentication。
第五步,在TestController类中增加登录并且创建JWT的操作方法Login。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 [HttpPost ] public async Task<IActionResult> Login (LoginRequest loginRequest, [FromServices] IOptions<JWTOptions> jwtOptions ){ string userName = loginRequest.UserName; string password = loginRequest.Password; var user = await _userManager.FindByNameAsync(userName); var success = await _userManager.CheckPasswordAsync(user,password); if (!success) { return BadRequest("失败" ); } var claims = new List<Claim>(); claims.Add(new Claim(ClaimTypes.NameIdentifier,user.Id.ToString())); claims.Add(new Claim(ClaimTypes.Name,user.UserName)); var roles = await _userManager.GetRolesAsync(user); foreach (var role in roles) { claims.Add(new Claim(ClaimTypes.Role, role)); } string jwtToken = BuildToken(claims, jwtOptions.Value); return Ok(jwtToken); } private static string BuildToken (IEnumerable<Claim> claims,JWTOptions options ){ DateTime expires = DateTime.Now.AddSeconds(options.ExpirSeconds); byte [] keyBytes = Encoding.UTF8.GetBytes(options.SigningKey); var secKey = new SymmetricSecurityKey(keyBytes); var credentials = new SigningCredentials(secKey, SecurityAlgorithms.HmacSha256Signature); var tokenDescriptor = new JwtSecurityToken(expires:expires,signingCredentials:credentials,claims:claims); return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor); }
第六步,在需要登录才能访问的控制器类或者Action方法上添加[Authorize]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [Authorize ] public class Demo2Controller : ControllerBase { [HttpGet ] public IActionResult Hello () { string id= this .User.FindFirst(ClaimTypes.NameIdentifier)!.Value; string userName = this .User.FindFirst(ClaimTypes.Name)!.Value; IEnumerable<Claim> roleClaims = this .User.FindAll(ClaimTypes.Role); string roleNames = string .Join("," , roleClaims.Select(c => c.Value)); return Ok($"Hello {userName} , your ID is {id} , and your roles are {roleNames} ." ); } }
第七步,测试登录和访问。用PostMan自定义报文头:Authorization的值为“Bearer JWTToken”,Authorization的值中的“Bearer”和JWT令牌之间一定要通过空格分隔。前后不能多出来额外的空格、换行等。
[Authorize]的注意事项
ASP.NET Core中身份验证和授权验证的功能由Authentication、Authorization中间件提供:app.UseAuthentication()、app.UseAuthorization()。
控制器类上标注[Authorize],则所有操作方法都会被进行身份验证和授权验证;对于标注了[Authorzie]的控制器中,如果其中某个操作方法不想被验证,可以在操作方法上添加[AllowAnonymous]。如果没有在控制器类上标注[Authorize],那么这个控制器中的所有操作方法都允许被自由地访问;对于没有标注[Authorzie]的控制器中,如果其中某个操作方法需要被验证,文买也可以在操作方法上添加[Authorize].
ASP.NET Core会按照HTTP协议的规范,从Authorization取出来令牌,并且进行校验、解析,然后把解析结果填充到User属性中,这一切都是ASP.NET Core完成的,不需要开发人员自己编写代码。但是一旦出现401,没有详细的报错信息,很难排查。
让Swagger中调试待验证的请求跟简单 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 builder.Services.AddSwaggerGen(c => { var scheme = new OpenApiSecurityScheme() { Description = "Authorization header.\r\nExample:'Bearer 12345abcdef'" , Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Authorization" }, Scheme = "oauth2" , Name = "Authorization" , In = ParameterLocation.Header, Type = SecuritySchemeType.ApiKey, }; c.AddSecurityDefinition("Authorization" , scheme); var requirement = new OpenApiSecurityRequirement(); requirement[scheme] = new List<string >(); c.AddSecurityRequirement(requirement); });
解决JWT无法提前撤回的难题 JWT的缺点 1.到期前,令牌无法撤销
遇到这种情况用Session更好解决这个问题。
解决方法 在用户表中增加一个整数类型的列JWTVersion,代表最后一次发放出去的令牌的版本号;每次登录、发放令牌的时候,都让JWTVersion的值自增,同时将JWTVersion的值也放到JWT令牌的负载中;当执行禁用用户、撤回用户的令牌等操作的时候,把这个用户对应的JWTVersion列的值自增;当服务器段收到客户端提交的JWT令牌后,先把JWT令牌中的JWTVersion值和数据库中JWTVersion的值做一下比较,如果JWT令牌中JWTVersion的值小于数据库中JWTVsersion的值,就说明这个JWT令牌过期了。
先配置所需程序:
配置CheckAsync扩展方法:如果用户、密码正确则空,失败则自增
1 2 3 4 5 6 7 8 9 10 11 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)); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 public class DemoController : ControllerBase { private readonly IOptionsSnapshot<JWTSettings> jwtSettings; private readonly UserManager<MyUser> userManager; public DemoController (IOptionsSnapshot<JWTSettings> jwtSettings, UserManager<MyUser> userManager ) { this .jwtSettings = jwtSettings; this .userManager = userManager; } [HttpPost ] public async Task<ActionResult<string >> Login(string userName,string password) { var user = await userManager.FindByNameAsync(userName); if (user == null ) { return BadRequest("用户名或密码错误" ); } if (await userManager.CheckPasswordAsync(user,password)) { await userManager.AccessFailedAsync(user).CheckAsync(); List<Claim> claims = new List<Claim>(); claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())); claims.Add(new Claim(ClaimTypes.Name, user.UserName)); var roles = await userManager.GetRolesAsync(user); foreach (var role in roles) { claims.Add(new Claim(ClaimTypes.Role, role)); } string key = jwtSettings.Value.SecKey; DateTime expires = DateTime.Now.AddSeconds(jwtSettings.Value.ExpirSeconds); 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("用户名或密码错误" ); } } }
为用户实体User类增加一个long类型的属性JWTVersion.
1 2 3 4 5 public class MyUser :IdentityUser <long > { public string ? WeiXinAccount { get ; set ; } public long JWTVersion { get ; set ; } }
修改登录并发放令牌的代码,把用户的JWTVersion属性的值自增,并且把JWTVersion的值写入JWT令牌。
1 2 3 4 5 6 7 await userManager.AccessFailedAsync(user).CheckAsync(); user.JWTVersion++; await userManager.UpdateAsync(user); List<Claim> claims = new List<Claim>(); claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString())); claims.Add(new Claim(ClaimTypes.Name, user.UserName)); claims.Add(new Claim("JWTVersion" , user.JWTVersion.ToString()));
编写一个操作筛选器,统一实现对所有的控制器的操作方法中JWT令牌的检查操作。把JWTValidationFilter注册到Program.cs中MVC的全局筛选器中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 public class JWTValidationFilter : IAsyncActionFilter { private IMemoryCache memCache; private UserManager<MyUser> userManager; public JWTValidationFilter (IMemoryCache memCache, UserManager<MyUser> userManager ) { this .memCache = memCache; this .userManager = userManager; } public async Task OnActionExecutionAsync (ActionExecutingContext context, ActionExecutionDelegate next ) { var claimUserId = context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier); if (claimUserId == null ) { await next(); return ; } long userId = long .Parse(claimUserId!.Value); string cacheKey = $"JWTValidationFilter.UserInfo.{userId} " ; MyUser user = await memCache.GetOrCreateAsync(cacheKey, async e => { e.AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(5 ); return await userManager.FindByIdAsync(userId.ToString()); }); if (user == null ) { var result = new ObjectResult($"UserId({userId} ) not found" ); result.StatusCode = (int )HttpStatusCode.Unauthorized; context.Result = result; return ; } var claimJWTVersion = context.HttpContext.User.FindFirst(ClaimTypes.Version); long jwtVersion = long .Parse(claimJWTVersion!.Value); if (jwtVersion != user.JWTVersion) { next(); } else { var result = new ObjectResult($"UserId({userId} ) JWTVersion({jwtVersion} ) not match" ); result.StatusCode = (int )HttpStatusCode.Unauthorized; context.Result = result; return ; } } }
把JWTValidationFilter注册到Program.cs中MVC的全局筛选器中。
1 2 3 4 builder.Services.Configure<MvcOptions>(opt => { opt.Filters.Add<JWTValidationFilter>(); });