8.2 JWT(代替Session)
经典的保持登陆状态的办法是Session,也就是用户登陆后,服务器产生唯一标识SessionId,并把SessionId和登陆的用户信息保存在服务器内存中,通时将SessionId发送给浏览器,当浏览器再次访问的时候,http请求中便携带了SessionId,服务器根据该Id在内存中取到用户信息,实现了登陆功能。
Session的缺点:
- 登陆用户多的时候,会占用服务器大量内存
- 每次客户端请求都要向服务器获取一次Session,导致请求速度响应慢
现在多采用JWT代替Session,JWT使用JSON来保存令牌信息,并不把信息保存在服务端,而是保存在客户端。JWT的结构包括头部、负载和签名:

头部中保存的是加密算法说明,负载中保存用户信息,签名是根据头部和负责经过算法算出来的值
JWT的登陆流程:
- 客户端向服务单发送登陆请求
- 服务端检验用户名密码,如果成功将从数据库中提取出这个用户的Id、角色等信息
- 服务端采用自定义的秘钥对用户信息(JSON)进行签名,形成签名数据
- 将用户信息(JSON)和上一步形成的签名拼接形成JWT,发送给客户端
- 客户端每次请求都带上这个JWT
- 每次服务器收到带JWT的请求后,使用自定义的秘钥对JWT的签名进行校验,如果成功则从JWT中取出用户信息
JWT基本使用
NuGet安装System.IdentityModel.Tokens.Jwt
生成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
| using Microsoft.IdentityModel.Tokens; using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using System.Text;
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, "6")); claims.Add(new Claim(ClaimTypes.Name, "yzk")); claims.Add(new Claim(ClaimTypes.Role, "User")); claims.Add(new Claim(ClaimTypes.Role, "Admin")); claims.Add(new Claim("PassPort", "E90000082"));
string key = "fasdfad&9045dafz222#fadpio@0232"; 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其实是明文,所以不要将重要的信息放到JWT中,为了防止篡改JWT,服务器需要对JWT签名进行验证
- JWT验证
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| string jwt = Console.ReadLine()!; string secKey = "fasdfad&9045dafz222#fadpio@0232"; JwtSecurityTokenHandler tokenHandler = new(); TokenValidationParameters valParam = new(); var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secKey)); valParam.IssuerSigningKey = securityKey; valParam.ValidateIssuer = false; valParam.ValidateAudience = false;
ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(jwt, valParam, out SecurityToken secToken); foreach (var claim in claimsPrincipal.Claims) { Console.WriteLine($"{claim.Type}={claim.Value}"); }
|
ASP.NET Core对JWT的封装
- 在配置系统中增加JWT节点,节点下设置秘钥和过期时间,再创建一个对应的配置类
1 2 3 4
| "JWT": { "SigningKey": "fasdfad&9045dafz222#fadpio@0232", "ExpireSeconds": "86400" }
|
1 2 3 4 5
| public class JWTOptions { public string SigningKey { get; set; } public int ExpireSeconds { get; set; } }
|
NuGet安装Microsoft.AspNetCore.Authentication.JwtBearer
对JWT进行配置在builder.Build之前添加
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT")); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(x => { JWTOptions? jwtOpt = builder.Configuration.GetSection("JWT").Get<JWTOptions>(); byte[] keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SigningKey); var secKey = new SymmetricSecurityKey(keyBytes); x.TokenValidationParameters = new() { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKey = secKey }; });
|
在Program.cs中的app.UseAuthorization()前面加上app.UseAuthentication();
在控制类中增加登陆并且创建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 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| public record LoginRequest (string UserName,string Password)
[HttpPost] public async Task<IActionResult> Login2(LoginRequest req, [FromServices] IOptions<JWTOptions> jwtOptions) { string userName = req.UserName; string password = req.Password; var user = await userManager.FindByNameAsync(userName); if (user == null) { return NotFound($"用户名不存在{userName}"); } var success = await userManager.CheckPasswordAsync(user, password); if (!success) { return BadRequest("Failed"); } 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 (string 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.ExpireSeconds); 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 "Bearer "+new JwtSecurityTokenHandler().WriteToken(tokenDescriptor); }
|
- 在需要登陆才能访问的控制器类上添加
[Authorize]
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| [Route("[controller]/[action]")] [ApiController] [Authorize] public class Test2Controller : ControllerBase { [HttpGet] public IActionResult Hello() { string id = this.User.FindFirst(ClaimTypes.NameIdentifier)!.Value; string userName = this.User.FindFirst(ClaimTypes.Name)!.Value; return Ok($"id={id},userName={userName}"); } }
|
在请求的时候,ASP.Net Core要求,JWT要放到请求报文头中,其key为Authorization ,值为Bearer JWT值。
对于客户端获得的JWT,在前端项目中可以将其保存在Cookie或者LocalStorage等位置,在退出的时候进行删除。
[Authorize]注意事项
[Authorize]
可以加到控制器上也可以加到操作方法中,在控制器上标注表示该控制器下所有的操作方法都需要身份验证,如果某个操作方法不需要身份验证则在该操作方法上标注[AllowAnonymous]
在Swagger带有Authorize按钮
修改Program.cs
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); });
|
界面如图:


在对话框中输入Bearer JWT