第二十八节:Asp.Net Core中JWT的几种写法和认证方式

2021-02-19 16:18

阅读:592

标签:strong   isnull   attribute   type   help   addm   toolbar   top   mat   

原文:第二十八节:Asp.Net Core中JWT的几种写法和认证方式

一. 前言

1.说明

  本章节重点介绍JWT的利用不同程序集的几种写法及认证方式,然后复习一下JWT的组成。

  其他概念参考:https://www.cnblogs.com/yaopengfei/p/10451189.html

  官网:https://jwt.io

2.JWT组成

  样式:"xxxxxxxxxxxx.xxxxxxxxxxxxx.xxxxxxxxxxxxxxxx"由三部分组成.

(1).Header头部:{\"alg\":\"HS256\",\"typ\":\"JWT\"}基本组成,也可以自己添加别的内容,然后对最后的内容进行Base64编码.

(2).Payload负载:iss、sub、aud、exp、nbf、iat、jti基本参数,也可以自己添加别的内容,然后对最后的内容进行Base64编码.

(3).Signature签名:将Base64后的Header和Payload通过.组合起来,然后利用Hmacsha256+密钥进行加密。

 

二. 加解密几种写法

1. 写法1

  说明:利用规则自己手动封装,安装【NETCore.Encrypt】程序集,利用里面的HMAC加密算法,声明一个expire用于存放过期时间。校验的时候先验证时间是否过期,再验证签名的准确性。

详细代码见:TestJwt1

技术图片技术图片
 1  #region Base64编码
 2         /// 
 3         /// Base64编码
 4         /// 
 5         /// 待编码的文本字符串
 6         /// 编码的文本字符串
 7         public string Base64UrlEncode(string text)
 8         {
 9             var plainTextBytes = Encoding.UTF8.GetBytes(text);
10             var base64 = Convert.ToBase64String(plainTextBytes).Replace(+, -).Replace(/, _).TrimEnd(=);
11             return base64;
12         }
13         #endregion
14 
15         #region Base64解码
16         /// 
17         /// Base64解码
18         /// 
19         /// 
20         /// 
21 
22         public string Base64UrlDecode(string base64UrlStr)
23         {
24             base64UrlStr = base64UrlStr.Replace(-, +).Replace(_, /);
25             switch (base64UrlStr.Length % 4)
26             {
27                 case 2:
28                     base64UrlStr += "==";
29                     break;
30                 case 3:
31                     base64UrlStr += "=";
32                     break;
33             }
34             var bytes = Convert.FromBase64String(base64UrlStr);
35             return Encoding.UTF8.GetString(bytes);
36         }
37         #endregion
Base64编码和解码
技术图片
 1         /// 
 2         /// 手写JWT算法
 3         /// 
 4         public bool TestJwt1()
 5         {
 6             string secretKey = Configuration["SecretKey"];
 7             //1.加密
 8             //1.1 表头的处理
 9             string headerBase64Url = this.Base64UrlEncode("{\"alg\":\"HS256\",\"typ\":\"JWT\"}");
10             //1.2 PayLoad的处理
11             var jwtPayLoad = new
12             {
13                 expire = DateTime.Now.AddMinutes(15),
14                 userId = "00000000001",
15                 userAccount = "admin"
16             };
17             string payloadBase64Url = this.Base64UrlEncode(JsonConvert.SerializeObject(jwtPayLoad));
18             //1.3 Sign的处理
19             string sign = $"{headerBase64Url}.{payloadBase64Url}".HMACSHA256(secretKey);
20             //1.4 最终的jwt字符串
21             string jwtStr = $"{headerBase64Url}.{payloadBase64Url}.{sign}";
22 
23             //2.校验token是否正确
24             bool result;   //True表示通过,False表示未通过
25             //2.1. 获取token中的PayLoad中的值,并做过期校验
26             JwtData myData = JsonConvert.DeserializeObject(this.Base64UrlDecode(jwtStr.Split(.)[1]));  //这一步已经获取到了payload中的值,并进行转换了
27             var nowTime = DateTime.Now;
28             if (nowTime > myData.expire)
29             {
30                 //表示token过期,校验未通过
31                 result = false;
32                 return result;
33             }
34             else
35             {
36                 //2.2 做准确性校验
37                 var items = jwtStr.Split(.);
38                 var oldSign = items[2];
39                 string newSign = $"{items[0]}.{items[1]}".HMACSHA256(secretKey);
40                 result = oldSign == newSign;  //true表示检验通过,false表示检验未通过 
41                 return result;
42             }
43         }
技术图片

2. 写法2(推荐!)

  说明:使用官方JWT程序集【JWT 5.3.1】,封装加密和解密算法,详见:JWTHelp帮助类.其中加密算法的封装将extraHeaders参数作为一个可空参数,可以根据自己的需要决定是否在Header头中添加额外的参数。

jwt实体类

技术图片技术图片
1    public class JwtData
2     {
3         public DateTime expire { get; set; }  //代表过期时间
4 
5         public string userId { get; set; }
6 
7         public string userAccount { get; set; }
8     }
JwtData

封装加密和解密方法

技术图片技术图片
 1    /// 
 2     /// Jwt的加密和解密
 3     /// 注:加密和加密用的是用一个密钥
 4     /// 依赖程序集:【JWT】
 5     /// 
 6     public class JWTHelp
 7     {
 8 
 9         /// 
10         /// JWT加密算法
11         /// 
12         /// 负荷部分,存储使用的信息
13         /// 密钥
14         /// 存放表头额外的信息,不需要的话可以不传
15         /// 
16         public static string JWTJiaM(IDictionarystring, object> payload, string secret, IDictionarystring, object> extraHeaders = null)
17         {
18             IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
19             IJsonSerializer serializer = new JsonNetSerializer();
20             IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
21             IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
22             var token = encoder.Encode(payload, secret);
23             return token;
24         }
25 
26         /// 
27         /// JWT解密算法
28         /// 
29         /// 需要解密的token串
30         /// 密钥
31         /// 
32         public static string JWTJieM(string token, string secret)
33         {
34             try
35             {
36                 IJsonSerializer serializer = new JsonNetSerializer();
37                 IDateTimeProvider provider = new UtcDateTimeProvider();
38                 IJwtValidator validator = new JwtValidator(serializer, provider);
39                 IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
40                 IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder);
41                 var json = decoder.Decode(token, secret, true);
42                 //校验通过,返回解密后的字符串
43                 return json;
44             }
45             catch (TokenExpiredException)
46             {
47                 //表示过期
48                 return "expired";
49             }
50             catch (SignatureVerificationException)
51             {
52                 //表示验证不通过
53                 return "invalid";
54             }
55             catch (Exception)
56             {
57                 return "error";
58             }
59         }
60 
61 
62     }
JWTHelp

测试

技术图片
 1         /// 
 2         /// 利用JWT官方程序集的写法
 3         /// 
 4         public void TestJwt2()
 5         {
 6             string secretKey = Configuration["SecretKey"];
 7 
 8             //1.加密
 9             //1.1 额外的header参数也可以不设置
10             var extraHeaders = new Dictionarystring, object>
11                     {
12                          {"myName", "limaru" },
13                     };
14             //过期时间(可以不设置,下面表示签名后 20分钟过期)
15             double exp = (DateTime.UtcNow.AddMinutes(20) - new DateTime(1970, 1, 1)).TotalSeconds;
16             //进行组装
17             var payload = new Dictionarystring, object>
18                     {
19                          {"userId", "00000000001" },
20                          {"userAccount", "admin" },
21                          {"exp",exp }
22                     };
23 
24             //1.2 进行JWT签名
25             var token = JWTHelp.JWTJiaM(payload, secretKey, extraHeaders);
26 
27             //2. 解密
28             var result = JWTHelp.JWTJieM(token, secretKey);
29             //然后在转换一下
30             JwtData myData = JsonConvert.DeserializeObject(result);
31 
32         }
技术图片

3. 写法3

  说明:使用Identity家的程序集【System.IdentityModel.Tokens.Jwt】,进行加密和解密,详见:TestJwt3。

注:下面代码,解密的时候不验证 aud 和 iss, ClockSkew = TimeSpan.Zero  代表校验过期时间的偏移量,即验证过期时间:(expires+该值),该值默认为5min,这里设置为0,表示生成token时的expries即为过期时间 。

技术图片
 1         public bool TestJwt3()
 2         {
 3             string secretKey = Configuration["SecretKey"];
 4             string token;
 5             //加密
 6             {
 7                 var tokenHandler = new JwtSecurityTokenHandler();
 8                 var key = Encoding.Default.GetBytes(secretKey);
 9                 var tokenDescriptor = new SecurityTokenDescriptor()
10                 {
11                     Subject = new ClaimsIdentity(new Claim[] {
12                          new Claim("userId","00000000001"),
13                          new Claim("userAccount","admin")
14                     }),
15                     Expires = DateTime.UtcNow.AddSeconds(10),
16                     SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha256Signature)
17                 };
18                 token = tokenHandler.WriteToken(tokenHandler.CreateToken(tokenDescriptor));           //将组装好的格式生成加密后的jwt字符串
19             }
20             //解密
21             bool result;
22             {
23                 var tokenHandler = new JwtSecurityTokenHandler();
24                 var key = Encoding.Default.GetBytes(secretKey);
25                 var validationParameters = new TokenValidationParameters
26                 {
27                     ValidateAudience = false, //表示不验证aud
28                     ValidateIssuer = false,   //表示不验证iss
29                     IssuerSigningKey = new SymmetricSecurityKey(key),
30                     ClockSkew = TimeSpan.Zero   //代表校验过期时间的偏移量,即验证过期时间:(expires+该值),该值默认为5min,这里设置为0,表示生成token时的expries即为过期时间
31                 };
32                 SecurityToken validatedToken;   //解密后的对象
33                 try
34                 {
35                     ClaimsPrincipal claimsPrincipal = tokenHandler.ValidateToken(token, validationParameters, out validatedToken);
36                     result = true;
37                     //获取payload中的数据
38                     var jwtPayload = ((JwtSecurityToken)validatedToken).Payload.SerializeToJson(); ;
39                 }
40                 catch (SecurityTokenExpiredException)
41                 {
42                     //表示过期
43                     result = false;
44                 }
45                 catch (SecurityTokenException)
46                 {
47                     //表示token错误
48                     result = false;
49                 }
50             }
51             return result;
52         } 
技术图片

 

三. 校验的几种方式

1. 利用过滤器校验

  这里使用override这个方法OnActionExecuting,在action执行之前过滤。

(1).涉及到几个技术点:

A.判断aciton是否有某个特性:

  context.ActionDescriptor.EndpointMetadata.Any(x => x.GetType() == typeof(SkipAttribute));

B.判断是否是ajax请求:

  判断Headers["X-Requested-With"]的值是不是"XMLHttpRequest".

C.获取传递过来的参数:

  a. ajax放在Header中传递的参数:context.HttpContext.Request.Headers["auth"].ToString();  判空方式:token == "null" || string.IsNullOrEmpty(token)

  b. 通过href=xxx?auth=xx传递的参数:context.HttpContext.Request.Query["auth"].ToString();   判空形式:string.IsNullOrEmpty(token)

PS:现在有一种Bearer认证, 是这样传递的("Authorization" "Bearer token"), 获取方式同上面的a,但是要拿到token的值,还需要截取一下,详见代码xxxx

D.过滤器中截断返回的几种形式:

  a.非ajax的截断:直接跳转到某个未授权的页面,如:context.Result = new RedirectResult("/Admin/ErrorIndex?isLogin=noPer");

  b.ajax的截断:

    ①.返回JsonResult,进入ajax中的success回调: context.Result = new JsonResult(new { status = "error", msg = "非法请求,参数已经过期" });

    ②.只返回状态码,进入ajax中的error回调:context.Result = new StatusCodeResult(401); 或 context.HttpContext.Response.StatusCode = 401;

    ③.返回ContentResult,进入ajax的error回调,同时回传错误内容:context.Result = new ContentResult { Content = "没有权限哦", StatusCode = 401 };

  注:推荐上述③,因为可以和正常的请求返回彻底区分开,特别注意过滤器中要加return; 光context.Result不能截断.。

E.过滤器中传值和action中取值:

  a.传值:context.RouteData.Values.Add("auth", xxxx);

  b.取值:ControllerContext.RouteData.Values["auth"].ToString()

(2).实战测试

  A:auth认证,指Header中传递参数的时候("auth", token),对于ajax返回JsonResult,校验不通过的时候,进入的是ajax中的success回调.

  B:Bearer认证,说白了就是在Header中传递参数的时候("Authorization", "Bearer " + token),在值的前面加了一个Bearer和空格,然后在解析的时候需要隔离拿出来token值.

PS:在JwtCheck2中,对于ajax请求验证未通过的时候返回ContextResult,状态码为401,进入ajax的error回调中,非ajax请求和A类似。

 服务端方法:获取token的代码、auth认证接口、Bearer认证接口、没有权限的页面

技术图片技术图片
 1        #region 获取Token
 2         /// 
 3         /// 获取Token
 4         /// 
 5         /// 
 6         public String GetToken()
 7         {
 8             string secretKey = Configuration["SecretKey"];
 9             //1.加密
10             //1.1 额外的header参数也可以不设置
11             var extraHeaders = new Dictionarystring, object>
12                     {
13                          {"myName", "limaru" },
14                     };
15             //过期时间(可以不设置,下面表示签名后 20分钟过期)
16             double exp = (DateTime.UtcNow.AddMinutes(20) - new DateTime(1970, 1, 1)).TotalSeconds;
17             //进行组装
18             var payload = new Dictionarystring, object>
19                     {
20                          {"userId", "00000000001" },
21                          {"userAccount", "admin" },
22                          {"exp",exp }
23                     };
24 
25             //1.2 进行JWT签名
26             var token = JWTHelp.JWTJiaM(payload, secretKey, extraHeaders);
27             return token;
28         }
29         #endregion
30 
31         #region JwtCheck1校验(auth认证)
32         [TypeFilter(typeof(JwtCheck1))]
33         public IActionResult GetMsg1()
34         {
35 
36             var jwtData = JsonConvert.DeserializeObject(ControllerContext.RouteData.Values["auth"].ToString());
37             return Json(new { status = "ok", msg = jwtData.userId });
38         }
39         #endregion
40 
41         #region JwtCheck2校验(Bearer认证)
42         [TypeFilter(typeof(JwtCheck2))]
43         public IActionResult GetMsg2()
44         {
45             var jwtData = JsonConvert.DeserializeObject(ControllerContext.RouteData.Values["auth"].ToString());
46             return Json(new { status = "ok", msg = jwtData.userId });
47         }
48 
49         #endregion
View Code

auth认证过滤器,对于ajax返回的是JsonResult,进入ajax的success

技术图片技术图片
  1     /// 
  2     /// auth认证过滤器,进入ajax的success
  3     /// 
  4     public class JwtCheck1 : ActionFilterAttribute
  5     {
  6 
  7         private IConfiguration _configuration;
  8         public JwtCheck1(IConfiguration configuration)
  9         {
 10             _configuration = configuration;
 11         }
 12 
 13         /// 
 14         /// action执行前执行
 15         /// 
 16         /// 
 17         public override void OnActionExecuting(ActionExecutingContext context)
 18         {
 19             //1.判断是否需要校验
 20             var isSkip = context.ActionDescriptor.EndpointMetadata.Any(x => x.GetType() == typeof(SkipAttribute));
 21             if (isSkip == false)
 22             {
 23                 //2. 判断是什么请求(ajax or 非ajax)
 24                 var actionContext = context.HttpContext;
 25                 if (IsAjaxRequest(actionContext.Request))
 26                 {
 27                     //表示是ajax
 28                     var token = context.HttpContext.Request.Headers["auth"].ToString();    //ajax请求传过来
 29                     if (token == "null" || string.IsNullOrEmpty(token))
 30                     {
 31                         context.Result = new JsonResult(new { status = "error", msg = "非法请求,参数为空" });
 32                         return;
 33                     }
 34                     //校验auth的正确性
 35                     var result = JWTHelp.JWTJieM(token, _configuration["SecretKey"]);
 36                     if (result == "expired")
 37                     {
 38                         context.Result = new JsonResult(new { status = "error", msg = "非法请求,参数已经过期" });
 39                         return;
 40                     }
 41                     else if (result == "invalid")
 42                     {
 43                         context.Result = new JsonResult(new { status = "error", msg = "非法请求,未通过校验" });
 44                         return;
 45                     }
 46                     else if (result == "error")
 47                     {
 48                         context.Result = new JsonResult(new { status = "error", msg = "非法请求,未通过校验" });
 49                         return;
 50                     }
 51                     else
 52                     {
 53                         //表示校验通过,用于向控制器中传值
 54                         context.RouteData.Values.Add("auth", result);
 55                     }
 56 
 57                 }
 58                 else
 59                 {
 60                     //表示是非ajax请求,则auth拼接在参数中传过来
 61                     var token = actionContext.Request.Query["auth"].ToString();
 62                     if (token == "null" || string.IsNullOrEmpty(token))
 63                     {
 64                         context.Result = new RedirectResult("/Home/NoPerIndex?reason=null");
 65                         return;
 66                     }
 67                     //校验auth的正确性
 68                     var result = JWTHelp.JWTJieM(token, _configuration["SecretKey"]);
 69                     if (result == "expired")
 70                     {
 71                         context.Result = new RedirectResult("/Home/NoPerIndex?reason=expired");
 72                         return;
 73                     }
 74                     else if (result == "invalid")
 75                     {
 76                         context.Result = new RedirectResult("/Home/NoPerIndex?reason=invalid");
 77                         return;
 78                     }
 79                     else if (result == "error")
 80                     {
 81                         context.Result = new RedirectResult("/Home/NoPerIndex?reason=error");
 82                         return;
 83                     }
 84                     else
 85                     {
 86                         //表示校验通过,用于向控制器中传值
 87                         context.RouteData.Values.Add("auth", result);
 88                     }
 89                 }
 90             }
 91 
 92         }
 93 
 94         /// 
 95         /// 判断该请求是否是ajax请求
 96         /// 
 97         /// 
 98         /// 
 99         private bool IsAjaxRequest(HttpRequest request)
100         {
101             string header = request.Headers["X-Requested-With"];
102             return "XMLHttpRequest".Equals(header);
103         }
104     }
View Code

Bearer认证过滤器,对于ajax返回的是ContentResult,进入的是ajax的error

技术图片技术图片
    /// 
    /// Bearer认证,返回ajax中的error
    /// 
    public class JwtCheck2 : ActionFilterAttribute
    {

        private IConfiguration _configuration;
        public JwtCheck2(IConfiguration configuration)
        {
            _configuration = configuration;
        }

        /// 
        /// action执行前执行
        /// 
        /// 
        public override void OnActionExecuting(ActionExecutingContext context)
        {
            //1.判断是否需要校验
            var isSkip = context.ActionDescriptor.EndpointMetadata.Any(x => x.GetType() == typeof(SkipAttribute));
            if (isSkip == false)
            {
                //2. 判断是什么请求(ajax or 非ajax)
                var actionContext = context.HttpContext;
                if (IsAjaxRequest(actionContext.Request))
                {
                    //表示是ajax
                    var token = context.HttpContext.Request.Headers["Authorization"].ToString();    //ajax请求传过来
                    string pattern = "^Bearer (.*?)$";
                    if (!Regex.IsMatch(token, pattern))
                    {
                        context.Result = new ContentResult { StatusCode = 401, Content = "token格式不对!格式为:Bearer {token}" };
                        return;
                    }
                    token = Regex.Match(token, pattern).Groups[1]?.ToString();
                    if (token == 


评论


亲,登录后才可以留言!