.net jwt用法

110 阅读5分钟

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>();
    }
}