JWT
大约 4 分钟
JWT
概念
- Json Web Token,JWT 把登录信息(也称作令牌)保存在客户端
- 优点:
- json 格式的通用性
- 可以利用 Payload 存储非敏感的信息
- 便于传输,JWT结构简单,字节占用小
- 不需要在服务端保存会话信息,易于应用的扩展
- 组成:
- header:令牌头部,记录了整个令牌的类型和签名算法
- payload:令牌负荷,记录了保存的主体信息
- signature:令牌签名,按照头部固定的签名算法对整个令牌进行签名
- 负载中的内容是明文形式保存的,不要把不能被客户端知道的信息放到JWT中
- 安装依赖包:
System.IdentityModel.Tokens.Jwt
原理示例
生成令牌
public ActionResult<string> GenerateJwt()
{
// 1. 添加字段信息
List<Claim> claims = new List<Claim>();
claims.Add(new(ClaimTypes.Name, "XiaoMing"));
claims.Add(new(ClaimTypes.Email, "123456@qq.com"));
claims.Add(new(ClaimTypes.Role, "admin"));
// 2. 设置过期时间
DateTime? expires = DateTime.Now.AddDays(1);
// 3. 设置密钥,与解密密钥一致
string key = "adfafdafd$dfadfea123";
SymmetricSecurityKey securityKey = new (Encoding.UTF8.GetBytes(key));
SigningCredentials credentials = new (securityKey, SecurityAlgorithms.HmacSha256Signature);
// 4. 生成 token
JwtSecurityToken token = new JwtSecurityToken(claims: claims, expires: expires, signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(token);
}
令牌解码
public ActionResult DecodeJwt(string jwt)
{
// 1. 设置密钥,与加密密钥一致
string key = "adfafdafd$dfadfea123";
JwtSecurityTokenHandler handler = new();
SymmetricSecurityKey securityKey = new(Encoding.UTF8.GetBytes(key));
// 2. 设置验证参数
TokenValidationParameters parameters = new();
parameters.IssuerSigningKey = securityKey;
parameters.ValidateIssuer = false;
parameters.ValidateAudience = false;
// 3. 解码 token ,如果 token 被篡改解码将不通过并抛出异常
ClaimsPrincipal claimsPrincipal = handler.ValidateToken(jwt, parameters, out SecurityToken validatedToken);
// 4. 获取 token 中携带的信息
foreach(var claim in claimsPrincipal.Claims)
Console.WriteLine($"{claim.Type}={claim.Value}");
return Ok();
}
Asp.Net Core 对 JWT 的封装
- 安装依赖包
Install-Package Microsoft.AspNetCore.Authentication.JwtBearer
配置文件
appsettings.json
添加 JWT 节点"JWT": { "SigningKey": "abcdefg123456", "ExpireSeconds": "60" }
添加配置类
JWTOptions
public class JWTOptions { public string SigningKey { get; set; } public int ExpireSeconds { get; set; } }
配置服务:在
Program.cs
中进行配置services
// 添加 JWT 配置 builder.Services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT")); // 添加身份验证 builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(opt => { // 设置密钥 JWTOptions jwtOpt = builder.Configuration.GetSection("JWT").Get<JWTOptions>()??new JWTOptions(); var keyBytes = Encoding.UTF8.GetBytes(jwtOpt.SigningKey); SymmetricSecurityKey securityKey = new(keyBytes); // 配置参数,用于中间件解码令牌 opt.TokenValidationParameters = new() { ValidateIssuer = false, ValidateAudience = false, ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKey = securityKey }; });
添加中间件:在
Program.cs
的app.UseAuthorization()
代码之前添加app.UseAuthentication()
调用登录
Controller
public async Task<ActionResult> Login(string username,string password,[FromServices] IOptions<JWTOptions> jwtOptions) { // 数据库检索并验证密码 User? user = await userManager.FindByNameAsync(username); if(user == null) return BadRequest("用户名或密码错误"); bool success = await userManager.CheckPasswordAsync(user, password); if (!success) return BadRequest("用户名或密码错误"); // 创建 token 信息 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)); // 生成 token string token = BuildToken(claims, jwtOptions.Value); return Ok(token); } // Token 生成函数 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 new JwtSecurityTokenHandler().WriteToken(tokenDescriptor); }
在需要登录才能访问的
Controller
或Action
上添加[Authorize]
[HttpPost] [Authorize] public ActionResult<string> 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($"id={id},userName={userName},roleNames ={roleNames}"); }
使用
Postman
测试接口- 访问 Login 方法获取 token
- Header 中添加
Authorization
键:Bearer[空格]token
做为值,访问 Hello 方法 - 如果 Postman 报错
unable to verify the first cerificate
,在 setting 中取消启用Enable SSL certificate verification
[Authorize]
- 控制器类上标注
[Authorize]
,则所有操作方法都会被进行身份验证和授权验证; - 如果其中某个操作方法不想被验证,可以在操作方法上添加
[AllowAnonymous]
- ASP.NET Core 会按照HTTP协议的规范,从 Authorization 取出来令牌,并且进行校验、解析,然后把解析结果填充到 User 属性
Swagger 中调试带 JWT 的请求
builder.Services.AddSwaggerGen(opt =>{
OpenApiSecurityScheme scheme = new();
scheme.Description = "Authorization header. \r\nExample: 'Bearer 12345abcdef'";
scheme.Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Authorization" };
scheme.Scheme = "oauth2";
scheme.Name = "Authorization";
scheme.In = ParameterLocation.Header;
scheme.Type = SecuritySchemeType.ApiKey;
OpenApiSecurityRequirement requirement = new();
requirement[scheme] = new List<string>();
opt.AddSecurityDefinition("Authorization", scheme);
opt.AddSecurityRequirement(requirement);
});
JWT 提前撤回
到期前,令牌无法被提前撤回
需要 JWT 撤回的场景用传统 Session 更合适
服务端维护 JWT 状态与使用 JWT 初衷相违背,应考虑其他方案应对需要提前撤回令牌的场景
实现令牌撤回的方案:
在数据库中增加 JWTVersion 用于控制令牌版本
每次登录、发放令牌的时候, JWTVersion 的值自增,同时将 JWTVersion 的值也放到令牌的负载中
当执行禁用用户、撤回用户的令牌等操作的时候, JWTVersion 的值自增
当服务器端收到客户端提交的令牌后,先把令牌中的 JWTVersion 值和数据库中 JWTVersion 的值做比较以判定令牌是否过期
编写过滤器,对所有请求验证令牌版本