目录
原理;
根据原理使用 C# 语言, 生成 jwt;
自定义验证 jwt;
使用 aspnetcore 中自带的类生成 jwt;
学有所得
了解 jwt 原理;
使用 C# 轻松实现 jwt 生成和验证
原理
jwt 对所有语言都是通用的, 只要知道秘钥, 另一一种语言有可以对 jwt 的有效性进行判断;
jwt 的组成; Header 部分 Base64 转化. Payload 部分 Base64 转化. 使用 HS256 方式根据秘钥对前面两部分进行加密后再 Base64 转化, 其中使用的 hs256 加密是 header 部分指定的, 也可以通过官网的查看, 如下图:
原理就这么简单, 那究竟用怎样使用 C# 来实现呢, 又怎么确定它的正确性呢?, 请继续
使用 C# 实现
我们定义一个今天方法, 其中需要使用到 Microsoft.IdentityModel.Tokens.dll,ASP.NET core 2.1 再带, 如果其他版本, 没有自带, 需要 nuget 一下这个类库
- /// <summary>
- /// 创建 jwttoken, 源码自定义
- /// </summary>
- /// <param name="payLoad"></param>
- /// <param name="header"></param>
- /// <returns></returns>
- public static string CreateToken(Dictionary<string, object> payLoad,int expiresMinute, Dictionary<string, object> header = null)
- {
- if (header == null)
- {
- header = new Dictionary<string, object>(new List<KeyValuePair<string, object>>() {
- new KeyValuePair<string, object>("alg", "HS256"),
- new KeyValuePair<string, object>("typ", "JWT")
- });
- }
- // 添加 jwt 可用时间 (应该必须要的)
- var now = DateTime.UtcNow;
- payLoad["nbf"] = ToUnixEpochDate( now);// 可用时间起始
- payLoad["exp"] = ToUnixEpochDate(now.Add(TimeSpan.FromMinutes(expiresMinute)));// 可用时间结束
- var encodedHeader = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(header));
- var encodedPayload = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(payLoad));
- var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(securityKey));
- var encodedSignature = Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(encodedHeader, ".", encodedPayload))));
- var encodedJwt = string.Concat(encodedHeader, ".", encodedPayload, ".", encodedSignature);
- return encodedJwt;
- }
- public static long ToUnixEpochDate(DateTime date) =>
- (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);
该方法很简单, 只需要传入 header 键值对和 payLoad 键值对, 然后根据原理进行 Base64 转换和 hs256 加密, 接下来我们来使用一个测试类对其进行测试, 代码如下:
- [TestMethod]
- public void TokenValidateTest()
- {
- Dictionary<string, object> payLoad = new Dictionary<string, object>();
- payLoad.Add("sub", "rober");
- payLoad.Add("jti", "09e572c7-62d0-4198-9cce-0915d7493806");
- payLoad.Add("nbf", null);
- payLoad.Add("exp", null);
- payLoad.Add("iss", "roberIssuer");
- payLoad.Add("aud", "roberAudience");
- payLoad.Add("age", 30);
- var encodeJwt = TokenContext.CreateToken(payLoad, 30);
- var result = TokenContext.Validate(encodeJwt, (load) => { return true; });
- Assert.IsTrue(result);
- }
先不管后面的验证, 我们先看看其中生成的 encodeJwt 的值: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJyb2JlciIsImp0aSI6IjY0OWMyYjUxLTE4ZGQtNDEzYy05Yzc5LTI4NWNhMDAxODU2NSIsIm5iZiI6MTU0MDYxMDY2NSwiZXhwIjoxNTQwNjEyNDY1LCJpc3MiOiJyb2Jlcklzc3VlciIsImF1ZCI6InJvYmVyQXVkaWVuY2UiLCJhZ2UiOjMwfQ.7Is2KYHAtSr5fW2gPU1jGeHPzz2ULCZJGcWb40LSYyw
第一部分和第二部分, 并不是加密, 只是 Base64 转换, 我们可以通过其他语言轻松转换回来, 如下使用 JavaScript 进行转, Windows.atob(base64 加密) Windows.btoa(base64 解密)
var header=JSON.parse(Windows.atob('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9'))
如下图:
我再对 payLoa 进行转换回来, var payLoad=JSON.parse(Windows.atob('eyJzdWIiOiJyb2JlciIsImp0aSI6IjY0OWMyYjUxLTE4ZGQtNDEzYy05Yzc5LTI4NWNhMDAxODU2NSIsIm5iZiI6MTU0MDYxMDY2NSwiZXhwIjoxNTQwNjEyNDY1LCJpc3MiOiJyb2Jlcklzc3VlciIsImF1ZCI6InJvYmVyQXVkaWVuY2UiLCJhZ2UiOjMwfQ')) , 如下图:
所以, 从这里可以看出来, Base64 并不是属于加密, 只是简单转换, 因此, 不能在 payLoad 中存放重要内容, 比如密码等
使用 aspnetcore 中自带的类生成 jwt
aspnet core 中自带了一个 jwt 帮助类, 其实原理一样, 对上面做了封装, 丰富了一个内容, 我们继续使用一个静态方法, 如下
- /// <summary>
- /// 创建 jwtToken, 采用微软内部方法, 默认使用 HS256 加密, 如果需要其他加密方式, 请更改源码
- /// 返回的结果和 CreateToken 一样
- /// </summary>
- /// <param name="payLoad"></param>
- /// <param name="expiresMinute"> 有效分钟 </param>
- /// <returns></returns>
- public static string CreateTokenByHandler(Dictionary<string, object> payLoad, int expiresMinute)
- {
- var now = DateTime.UtcNow;
- // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims.
- // You can add other claims here, if you want:
- var claims = new List<Claim>();
- foreach (var key in payLoad.Keys)
- {
- var tempClaim = new Claim(key, payLoad[key]?.ToString());
- claims.Add(tempClaim);
- }
- // Create the JWT and write it to a string
- var jwt = new JwtSecurityToken(
- issuer: null,
- audience: null,
- claims: claims,
- notBefore: now,
- expires: now.Add(TimeSpan.FromMinutes(expiresMinute)),
- signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(securityKey)), SecurityAlgorithms.HmacSha256));
- var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
- return encodedJwt;
- }
- View Code
它效果和上面一模一样, 如果使用同样的 header ,payload, 秘钥, 生成的 jwt 肯定一样, 这里就不演示了, 感兴趣的可以自行尝试;
aspnetcore 中如何使用自定义 jwt 验证
上面讲了那么多, 只是为了大家更好的理解如何使用 jwt 进行验证, 那是 jwt 是如何进行验证的呢?, 如果一个 http 请求过来, 一般 jwt 携带在 http 请求头部的 Authorization 中; 先不看如何获取, 先看看他是如何验证的, 我们再定义个静态方法, 如下:
- /// <summary>
- /// 验证身份 验证签名的有效性,
- /// </summary>
- /// <param name="encodeJwt"></param>
- /// <param name="validatePayLoad"> 自定义各类验证; 是否包含那种申明, 或者申明的值, </param>
- /// 例如: payLoad["aud"]?.ToString() == "roberAuddience";
- /// 例如: 验证是否过期 等
- /// <returns></returns>
- public static bool Validate(string encodeJwt,Func<Dictionary<string,object>,bool> validatePayLoad)
- {
- var success = true;
- var jwtArr = encodeJwt.Split('.');
- var header = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[0]));
- var payLoad = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[1]));
- var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(securityKey));
- // 首先验证签名是否正确 (必须的)
- success = success && string.Equals(jwtArr[2], Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(jwtArr[0], ".", jwtArr[1])))));
- if (!success)
- {
- return success;// 签名不正确直接返回
- }
- // 其次验证是否在有效期内 (也应该必须)
- var now = ToUnixEpochDate(DateTime.UtcNow);
- success = success && (now>= long.Parse(payLoad["nbf"].ToString()) && now <long.Parse(payLoad["exp"].ToString()));
- // 再其次 进行自定义的验证
- success = success && validatePayLoad(payLoad);
- return success;
- }
其中 validatePayLoad 参数是一个自定义的验证的 Fun, 执行该 Fun 方法时会把解密后的 payload 作为参数传入进去
我们验证通过分为两部分,
第一, 必须的 (自认为的)
jwt 签名是否正确, 请看以上代码实现
jwt 是否在可以时间内, 请看以上代码实现
第二, 自定义的 (各复杂的, 原理就是获取 payLoad 的某个值, 然后对这个值进行各种判读 -- 等于, 大于, 包含,)
该 jwt 是不是进入黑名单
aud=='roberAudience'
我们来通过一个测试类验证
- [TestMethod]
- public void TokenCustomerValidateTest()
- {
- Dictionary<string, object> payLoad = new Dictionary<string, object>();
- payLoad.Add("sub", "rober");
- payLoad.Add("jti", Guid.NewGuid().ToString());
- payLoad.Add("nbf", null);
- payLoad.Add("exp", null);
- payLoad.Add("iss", "roberIssuer");
- payLoad.Add("aud", "roberAudience");
- payLoad.Add("age", 30);
- var encodeJwt = TokenContext.CreateToken(payLoad, 30);
- var result = TokenContext.Validate(encodeJwt, (load) => {
- var success = true;
- // 验证是否包含 aud 并等于 roberAudience
- success = success&& load["aud"]?.ToString() == "roberAudience";
- // 验证 age>20 等
- int.TryParse(load["age"].ToString(), out int age);
- Assert.IsTrue(age> 30);
- // 其他验证 jwt 的标识 jti 是否加入黑名单等
- return success;
- });
- Assert.IsTrue(result);
- }
如上面, 我们可以把 jwt 中的 payload 解析出来, 然后进行各种复杂的想要的验证;
其实, aspnet core 中的基于角色, 用户, 策略, 自定义策略的验证就相当这里的自定义验证, 一下章将详细说明和对比, 这里暂时不讲解
看完上面, 是不是觉得 jwt 很简单就, 主要就两部
创建 jwt;
验证 jwt;
完整代码如下:
- /// <summary>
- /// Token 上下文, 负责 token 的创建和验证
- /// </summary>
- public class TokenContext
- {
- /// <summary>
- /// 秘钥, 可以从配置文件中获取
- /// </summary>
- public static string securityKey = "GQDstclechengroberbojPOXOYg5MbeJ1XT0uFiwDVvVBrk";
- /// <summary>
- /// 创建 jwttoken, 源码自定义
- /// </summary>
- /// <param name="payLoad"></param>
- /// <param name="header"></param>
- /// <returns></returns>
- public static string CreateToken(Dictionary<string, object> payLoad,int expiresMinute, Dictionary<string, object> header = null)
- {
- if (header == null)
- {
- header = new Dictionary<string, object>(new List<KeyValuePair<string, object>>() {
- new KeyValuePair<string, object>("alg", "HS256"),
- new KeyValuePair<string, object>("typ", "JWT")
- });
- }
- // 添加 jwt 可用时间 (应该必须要的)
- var now = DateTime.UtcNow;
- payLoad["nbf"] = ToUnixEpochDate( now);// 可用时间起始
- payLoad["exp"] = ToUnixEpochDate(now.Add(TimeSpan.FromMinutes(expiresMinute)));// 可用时间结束
- var encodedHeader = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(header));
- var encodedPayload = Base64UrlEncoder.Encode(JsonConvert.SerializeObject(payLoad));
- var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(securityKey));
- var encodedSignature = Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(encodedHeader, ".", encodedPayload))));
- var encodedJwt = string.Concat(encodedHeader, ".", encodedPayload, ".", encodedSignature);
- return encodedJwt;
- }
- /// <summary>
- /// 创建 jwtToken, 采用微软内部方法, 默认使用 HS256 加密, 如果需要其他加密方式, 请更改源码
- /// 返回的结果和 CreateToken 一样
- /// </summary>
- /// <param name="payLoad"></param>
- /// <param name="expiresMinute"> 有效分钟 </param>
- /// <returns></returns>
- public static string CreateTokenByHandler(Dictionary<string, object> payLoad, int expiresMinute)
- {
- var now = DateTime.UtcNow;
- // Specifically add the jti (random nonce), iat (issued timestamp), and sub (subject/user) claims.
- // You can add other claims here, if you want:
- var claims = new List<Claim>();
- foreach (var key in payLoad.Keys)
- {
- var tempClaim = new Claim(key, payLoad[key]?.ToString());
- claims.Add(tempClaim);
- }
- // Create the JWT and write it to a string
- var jwt = new JwtSecurityToken(
- issuer: null,
- audience: null,
- claims: claims,
- notBefore: now,
- expires: now.Add(TimeSpan.FromMinutes(expiresMinute)),
- signingCredentials: new SigningCredentials(new SymmetricSecurityKey(Encoding.ASCII.GetBytes(securityKey)), SecurityAlgorithms.HmacSha256));
- var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);
- return encodedJwt;
- }
- /// <summary>
- /// 验证身份 验证签名的有效性,
- /// </summary>
- /// <param name="encodeJwt"></param>
- /// <param name="validatePayLoad"> 自定义各类验证; 是否包含那种申明, 或者申明的值, </param>
- /// 例如: payLoad["aud"]?.ToString() == "roberAuddience";
- /// 例如: 验证是否过期 等
- /// <returns></returns>
- public static bool Validate(string encodeJwt,Func<Dictionary<string,object>,bool> validatePayLoad)
- {
- var success = true;
- var jwtArr = encodeJwt.Split('.');
- var header = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[0]));
- var payLoad = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[1]));
- var hs256 = new HMACSHA256(Encoding.ASCII.GetBytes(securityKey));
- // 首先验证签名是否正确 (必须的)
- success = success && string.Equals(jwtArr[2], Base64UrlEncoder.Encode(hs256.ComputeHash(Encoding.UTF8.GetBytes(string.Concat(jwtArr[0], ".", jwtArr[1])))));
- if (!success)
- {
- return success;// 签名不正确直接返回
- }
- // 其次验证是否在有效期内 (也应该必须)
- var now = ToUnixEpochDate(DateTime.UtcNow);
- success = success && (now>= long.Parse(payLoad["nbf"].ToString()) && now <long.Parse(payLoad["exp"].ToString()));
- // 再其次 进行自定义的验证
- success = success && validatePayLoad(payLoad);
- return success;
- }
- /// <summary>
- /// 获取 jwt 中的 payLoad
- /// </summary>
- /// <param name="encodeJwt"></param>
- /// <returns></returns>
- public static Dictionary<string ,object> GetPayLoad(string encodeJwt)
- {
- var jwtArr = encodeJwt.Split('.');
- var payLoad = JsonConvert.DeserializeObject<Dictionary<string, object>>(Base64UrlEncoder.Decode(jwtArr[1]));
- return payLoad;
- }
- public static long ToUnixEpochDate(DateTime date) =>
- (long)Math.Round((date.ToUniversalTime() - new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero)).TotalSeconds);
- }
- View Code
以上就是 jwt 的基本内容, 它确实很简单, 不要被 aspnet core 中的各种写法给搞晕了, 只要是 jwt 相关的验证都是基于上面这些东西
下一章节将讲述:
在 aspnet core 中, 自定义 jwt 管道验证;
在 aspnet core 中, 自定义策略验证 CommonAuthorizeHandler : AuthorizationHandler<CommonAuthorize>
自定义 jwt 逻辑验证和原生的角色, 用户, 策略, 等进行对比
来源: https://www.cnblogs.com/lechengbo/p/9860711.html