jwt 撤回方案之令牌版本号
1、为用户实体MyUser类增加一个long类型的属性JWTVersion
public long JWTVersion { get; set; }
2、在登录并发放令牌的代码中,把用户的JWTVersion属性的值自增,并且把JWTVersion的值写入JWT令牌
if(await userManager.CheckPasswordAsync(user, password)) {
...
user.JWTVersion++;//JWTVersion值自增
await userManager.UpdateAsync(user);//JWTVersion值保存到数据库
...
claims.Add(new Claim("JWTVersion",user.JWTVersion.ToString()));//JWTVersion值写入令牌
3、新建一个NotCheckJWTVersion
的Attribute派生类
[AttributeUsage(AttributeTargets.Method)]
public class NotCheckJWTVersionAttribute:Attribute {
}
4、编写一个ActionFilter,统一实现对所有的控制器的操作方法中JWT令牌的检查操作
1)继承IAsyncActionFilter
接口
2)注入UserManager服务
private readonly UserManager<MyUser> userManager;
public JWTVersionCheckFilter(UserManager<MyUser> userManager) {
this.userManager = userManager;
}
3)实现接口方法
//代码尚不明白
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next) {
ControllerActionDescriptor ctrlActionDesc= context.ActionDescriptor as ControllerActionDescriptor;
if (ctrlActionDesc == null) {
await next();//不next则截断,不执行
return;
}
//有标准NotCheckJWTVersionAttribute则返回,不检查
if (ctrlActionDesc.MethodInfo.GetCustomAttributes(typeof(NotCheckJWTVersionAttribute), true).Any()) {
await next();
return;
}
var claimJWTVer= context.HttpContext.User.FindFirst("JWTVersion");
if (claimJWTVer == null) {
context.Result = new ObjectResult("payload负载中没有JWTVersion") {
StatusCode = 400
};
return;
}
var claimUserId= context.HttpContext.User.FindFirst(ClaimTypes.NameIdentifier);
//long userId=Convert.ToInt64(claimUserId.Value);
long jwtVerFromClient = Convert.ToInt64(claimJWTVer.Value);
var user =await userManager.FindByIdAsync(claimUserId.Value);
if (user == null) {
context.Result = new ObjectResult("user找不到") {
StatusCode = 400
};
return;
}
if (user.JWTVersion > jwtVerFromClient) {
context.Result = new ObjectResult("客户端的JWT过时") {
StatusCode = 400
};
return;
}
await next();
}
5、把JWTVersionCheckFilter
注册到Program.cs中MVC的全局筛选器中
//注入JWT的ActionFilter
builder.Services.Configure<MvcOptions>(opt => {
opt.Filters.Add<JWTVersionCheckFilter>();
});
jwt的用法
jwt原生用法
System.IdentityModel.Tokens.Jwt
生成JWT令牌
注意:权限等级高的用户,可以给他添加多个角色。仅凭角色名无法判断权限高低
var claims = new List<Claim>();//每一个Claim代表payload中的一条信息
claims.Add(new Claim(ClaimTypes.NameIdentifier, "6"));
claims.Add(new Claim(ClaimTypes.Name, "yzk"));
claims.Add(new Claim(ClaimTypes.Role, "User"));//尽量使用ClaimTypes,不要自定义
claims.Add(new Claim(ClaimTypes.Role, "Admin"));//可以有多个role
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);//生成jwt
Console.WriteLine(jwt);
解码JWT令牌
1、不校验签名
解码算法是公开的算法,所以负载中的信息相当于“明文存储”
不校验签名是指,服务端不管签名是否正确,直接读取负载中的值。这样可能用户篡改了负载,而服务端却没有发现。而校验签名的话,用户篡改负载后,签名会变化,校验不通过,服务端知道用户篡改了信息
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);
}
string jwt=Console.ReadLine();
string[] segments = jwt.Split('.');
string head = JwtDecode(segments[0]);
string payload = JwtDecode(segments[1]);
string signature = JwtDecode(segments[2]);
Console.WriteLine("---head---");
Console.WriteLine(head);
Console.WriteLine("---payload---");
Console.WriteLine(payload);
Console.WriteLine("---signature---");
Console.WriteLine(signature);
2、校验签名
string jwt = Console.ReadLine();
string secKey = "fasdfad&9045dafz222#fadpio@02321";
//关键:采用JwtSecurityTokenHandler
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
Microsoft.AspNetCore.Authentication.JwtBearer
1、在appsettings.json中添加JWT节点,创建SecKey、ExpireSeconds两个配置项,分别代表JWT的密钥和过期时间(单位:秒)
"JWT": {
"SecKey": "fasdfad&9045dafz222#fadpio@02321",
"ExpireSeconds": 3600
}
2、创建JWT配置类JWTSettings,包含SecKey(签名)、ExpireSeconds(过期时间)两个属性
public class JWTSettings {
public string SecKey { get; set; }
public int ExpireSeconds { get; set; }
}
3、配置读取json文件的配置系统
读取JSON文件的配置
1)在Program.cs中注册读取json文件的服务
builder.Services.Configure<JWTSettings>(builder.Configuration.GetSection("JWT"));
2)在控制器中通过构造函数注入读取json文件的服务
private readonly IOptionsSnapshot<JWTSettings> jwtSettingsOpt;
public DemoController(IOptionsSnapshot<JWTSettings> jwtSettingsOpt) {
this.jwtSettingsOpt = jwtSettingsOpt;
}
3)在控制器中读取配置项的值
jwtSettingsOpt.Value.SecKey;
jwtSettingsOpt.Value.ExpireSeconds;
4、在Program.cs中注册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
};
});
//注:UseAuthentication一定要写在UseAuthorization之前
app.UseAuthentication();
app.UseAuthorization();
5、在控制器的登录方法中生成JWT字符串
[HttpPost]
public ActionResult<string> Login(string userName,string password) {
if (userName == "yzk" && password == "123456") {
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, "1"));
claims.Add(new Claim(ClaimTypes.Name, "yzk"));
claims.Add(new Claim(ClaimTypes.Role, "admin"));
string key = jwtSettingsOpt.Value.SecKey;
DateTime expires = DateTime.Now.AddSeconds
(jwtSettingsOpt.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("用户名或密码错误");
}
}
6、在需要登录才能访问的控制器类或者Action方法上添加[Authorize],进行权限或角色控制
[HttpGet]
//[Authorize] 代表登录才能访问
[Authorize(Roles ="admin")] //代表登录且角色是admin才能访问
public string Test3() {
return "666";
}
第三方库用法
Zack.JWT
1、Program.cs中注册JWT服务,并启用鉴权授权
对token进行配置,添加swagger的Authorize按钮
//JWTOptions是从数据库读取的配置,如下所示
//{"Issuer":"my","Audience":"my","Key":"yanwenlong@fdbatt.com","ExpireSeconds":3156000}
JWTOptions jwtOpt = configuration.GetSection("JWT").Get<JWTOptions>();
//AddJWTAuthentication是封装的方法,主要是根据jwtOption对token进行一些配置
builder.Services.AddJWTAuthentication(jwtOpt);
//启用Swagger中的【Authorize】按钮。这样就不用每个项目的AddSwaggerGen中单独配置了
builder.Services.Configure<SwaggerGenOptions>(c => {
c.AddAuthenticationHeader();
});
app.UseAuthentication();
app.UseAuthorization();
2、生成token令牌
注入ITokenService(zack.jwt封装)、IOptions
private readonly ITokenService tokenService;
private readonly IOptions<JWTOptions> optJWT;
生成token的方法
private async Task<string> BuildTokenAsync(User user) {
List<Claim> claims = new List<Claim>();
//添加token中payload内容...
//每一个Claim代表payload中的一条信息
claims.Add(new Claim(ClaimTypes.NameIdentifier, user.Id.ToString()));
//调用封装的BuildToken方法
return tokenService.BuildToken(claims, optJWT.Value);
}
第三方库源码
Zack.JWT
token的配置
public static class AuthenticationExtensions {
public static AuthenticationBuilder AddJWTAuthentication(this IServiceCollection services, JWTOptions jwtOpt) {
JWTOptions jwtOpt2 = jwtOpt;
return services.AddAuthentication("Bearer").AddJwtBearer(delegate (JwtBearerOptions x) {
x.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = jwtOpt2.Issuer,
ValidAudience = jwtOpt2.Audience,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOpt2.Key))
};
});
}
}
生成token
//定义--ITokenService.cs
public interface ITokenService {
string BuildToken(IEnumerable<Claim> claims, JWTOptions options);
}
//实现--TokenService.cs
public class TokenService : ITokenService
{
//根据jwt配置和需要存放的内容生成token
public string BuildToken(IEnumerable<Claim> claims, JWTOptions options)
{
TimeSpan ExpiryDuration = TimeSpan.FromSeconds(options.ExpireSeconds);
var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(options.Key));
var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256Signature);
var tokenDescriptor = new JwtSecurityToken(options.Issuer, options.Audience, claims,
expires: DateTime.Now.Add(ExpiryDuration), signingCredentials: credentials);
return new JwtSecurityTokenHandler().WriteToken(tokenDescriptor);
}
}
JWTOptions实体类
public class JWTOptions {
public string Issuer { get; set; }
public string Audience { get; set; }
public string Key { get; set; }
public int ExpireSeconds { get; set; }
}
服务自注册
class ModuleInitializer : IModuleInitializer {
public void Initialize(IServiceCollection services) {
services.AddScoped<ITokenService, TokenService>();
}
}