标签: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 ==