JWT Session描述 在浏览器端用户登录后,服务器端生成唯一session id,并且把用户名和session id建立对应关系保存到服务器端,当下次浏览器端访问时携带着cookie和服务器保存session对应,就可以直接登录了。
Session缺点 1、对于分布式集群环境,session数据保存在服务器内存中就不合适了,应该放到一个中心状态服务器上。ASP.NET Core支持Session采用Redis、Memcached。 2、中心状态服务器有性能问题。
JWT描述 1、JWT把登陆信息保存在客户端。 2、为了防止客户端数据造假,保存在客户端的令牌经过了签名处理,而签名的密钥只有服务器端才知道,每次服务器端收到客户端提交过来的令牌的时候都要检查一下签名。 3、JWT是明文存储的,不要把不能被客户端知道的信息放到JWT中。
JWT优点 1、状态保存在客户端,天然适合分布式签名。 2、签名保证了客户端无法数据造假。 3、性能更高,不需要和中心状态服务器通讯,纯内存计算。
JWT基本使用 NuGet安装: System.IdentityModel.Token,生成和校验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 25 26 // See https://aka.ms/new-console-template for more information using Microsoft.IdentityModel.Tokens; using System.Data; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; List<Claim> claims = new List<Claim>();//一个claim代表一条用户信息,尽量使用ClaimTypes的值 claims.Add(new Claim("Passport", "123456")); claims.Add(new Claim("QQ", "8888888888")); claims.Add(new Claim("Id", "6666")); claims.Add(new Claim(ClaimTypes.NameIdentifier, "11111 ")); claims.Add(new Claim(ClaimTypes.Name, "LJH")); claims.Add(new Claim(ClaimTypes.HomePhone, "8364799655")); claims.Add(new Claim(ClaimTypes.Role, "admin")); claims.Add(new Claim(ClaimTypes.Role, "manager")); string key = "suibianshurudiandongxi,jinliangchangyidian";//签名的key,不能泄露 DateTime expire = DateTime.Now.AddHours(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: expire, signingCredentials: credentials); string jwt = new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);//JWT令牌 Console.WriteLine(jwt);
校验:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 //调用JwtSecurityTokenHandler类对JWT令牌进行解码 using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text; string jwt = Console.ReadLine()!; string secKey = "suibianshurudiandongxi,jinliangchangyidian"; JwtSecurityTokenHandler tokenHandler = new(); TokenValidationParameters valParam = new(); var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secKey)); valParam.IssuerSigningKey = securityKey; valParam.ValidateIssuer = false; valParam.ValidateAudience = false; //不合法的key生成的jwt或者篡改过的jwt就会报错 ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwt, valParam, out SecurityToken secToken); foreach (var claim in claimsPrincipal.Claims) { Console.WriteLine($"{claim.Type}={claim.Value}"); }
ASP.NETCore对JWT的封装 NuGet安装: 1、Microsoft.AspNetCore.Authentication.JwtBearer 2、配置JWT节点,在appsettings.json下创建SecKey(密钥)、ExpireSeconds(过期时间),再创建配置类JWTSettings,包含SecKey、ExpireSeconds属性。 3、对JWT进行配置
1 2 3 4 5 //appsetting.json "JWT": { "SecKey": "asjdofiwengwieh*audsh&sdy$", "ExpireSeconds": "3600" }
1 2 3 4 public class JWTSettings { public string SecKey { get;set; } public int ExpireSeconds { get; set; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 //Program.cs builder.Services.Configure<JWTSettings>(builder.Configuration.GetSection("JWT")); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(opt => { var jwtSettings = builder.Configuration.GetSection("JWT").Get<JWTSettings>(); byte[] keyBytes = Encoding.UTF8.GetBytes(jwtSettings.SecKey); var secKey = new SymmetricSecurityKey(keyBytes); opt.TokenValidationParameters = new() { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKey = secKey }; }); ............... app.UseAuthentication();//在app.UseAuthorization();前使用中间件 app.UseAuthorization();
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 //Demo1Controller [Route("[controller]/[action]")] [ApiController] public class Demo1Controller : ControllerBase { private readonly IOptionsSnapshot<JWTSettings> jwtSettings; public DemoController(IOptionsSnapshot<JWTSettings> jwtSettings) { this.jwtSettings = jwtSettings; } [HttpPost] //用户登录,返回jwt public ActionResult<string> Login(string userName,string password) { if(userName == "LJH" && password == "123456") { List<Claim> claims = new List<Claim>(); claims.Add(new Claim(ClaimTypes.NameIdentifier,"1")); claims.Add(new Claim(ClaimTypes.Name, userName)); claims.Add(new Claim(ClaimTypes.Role, "admin")); string key = jwtSettings.Value.SecKey; DateTime expires = DateTime.Now.AddSeconds(jwtSettings.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 { return BadRequest(); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 [Route("[controller]/[Action]")] [ApiController] [Authorize]//该控制下所有action都需要登录才能访问 public class Demo2Controller : ControllerBase { [HttpGet] public string Test1() { var claim = this.User.FindFirst(ClaimTypes.Name);//可通过这个方式从jwt字符串的payload中解析并获取当前登录用户的用户名 return "ok"; } [AllowAnonymous]//有这个特性,不登录也可调用该action [HttpGet] public string Test2() { return "666"; } [Authorize(Roles = "admin")]//角色为admin的才可以访问这个action [HttpGet] public string Test3() { return "888"; } }
在需要登录才能访问的控制器或者Action上添加[Authorize]。 客户端发送请求时在请求头中携带Authorization的值为”Bearer JWTToken”(JWTToken为生成的jwt)给服务器,就会自动判断是否登录,注意Bearer和jwtToken中间有空格,前后不能多出来额外的空格、换行等。
[Authorize]的注意事项 1、ASP.NET Core中身份验证和授权验证的功能由Authentication、Authorization中间件提供:app.UseAuthentication()、app.UseAuthorization()。 2、控制器类上标注了[Authorize],则所有操作方法都会被进行身份验证和授权验证;对于标注了[Authorize]的控制器中,如果其中某个操作方法不想被验证,可以在操作方法上添加[AllowAnonymous]。action上标注[Authorize],则该action需要被验证才可以。 3、除了简单的[Authorize]验证,我们也可以对jwt的内容比如身份进行进一步验证,需要客户端发送jwt时携带需要验证的内容,在action或者controller类加上[Authorize(Roles = “admin”)]特性即可。 4、ASP.NET Core会按照http协议的规范,从Authorization取出令牌,并且进行校验、解析,然后把解析结果填充到User属性中,这一切都由ASP.NET Core完成,不需要开发人员编写,但是一旦出现401,没有详细的报错信息,很难排查。
Swagger携带JWT报文头 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 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的缺点 1、到期前令牌无法被提前撤回。什么情况下需要撤回? 用户被删除了、禁用了;令牌被盗用了;单设备登录。 2、需要JWT撤回的场景用传统Session更合适。 3、思路:用Redis保存状态,或者用refresh_token+access_token机制等。 4、实现:服务器端将所有发出去的JWT保存下来,当客户端携带JWT,服务器端判断是否被修改,然后去查看有没有保存这个JWT。 在用户表中增加一个JWTVersion列,代表最后发放出去的令牌的版本号,每次登录、发放令牌的时候都让JWTVersion的值自增,同时将JWTVersion的值也放到JWT令牌的负载中;当执行禁用用户、撤回用户的令牌等操作时,把这个用户对应的JWTVersion列的值自增;当服务器收到客户端提交的JWT令牌后,先把JWT令牌中的JWTVersion值和数据库中JWTVersion的值做一下比较,如果JWT令牌中JWTVersion的值小于数据库中JWTVersion的值,就说明JWT令牌过期了