一. 简介

1. 背景

  传统的基于Session的校验存在诸多问题,比如:Session过期、服务器开销过大、不能分布式部署、不适合前后端分离的项目。 传统的基于Token的校验需要存储Key-Value信息,存在Session或数据库中都有弊端,如果按照一定规律采用对称加密算法生成token,虽然能解决上面问题,但是一旦对称加密算法泄露,很容被反编译;所以在此基础上继续升级,利用userId生成Token,只要保存好秘钥即可,从而引出JWT。

2. 什么是JWT

  Json web token 简称:JWT, 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

下面就是一段JWT字符串(后面详细分析)

1 eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJVc2VySWQiOjEyMywiVXNlck5hbWUiOiJhZG1pbiJ9.Qjw1epD5P6p4Yy2yju3-fkq28PddznqRj3ESfALQy_U

3. JWT的优点

(1). JWT是无状态的,不需要服务器端保存会话信息,减轻服务器端的读取压力(存储在客户端上),同时易于扩展、易于分布式部署。

(2). JWT可以跨语言支持。

(3). 便于传输,jwt的构成很简单,字节占用空间少,所以是非常便于传输的。

(4). 自身构成有payload部分,可以存储一下业务逻辑相关的非敏感信息。

  特别声明:JWT最大的优势是无状态的,相对传统的Session验证能减轻服务器端的存储压力,安全性更高,但也不是绝对的,比如针对同一个接口,JWT字符串被截取后,且在有效期内,在不篡改JWT字符串的情况下,也是可以模拟请求进行访问的。(随着下面的内容深入体会JWT的核心)

二. JWT深度剖析

1. JWT的长相

  下面的一段字符串就是JWT加密后的显示格式,我们仔细看,中间通过两个 “点” 将这段字符串分割成三部分了。

eyJ0eXAxIjoiMTIzNCIsImFsZzIiOiJhZG1pbiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0.eyJVc2VySWQiOjEyMywiVXNlck5hbWUiOiJhZG1pbiIsImV4cCI6MTU1MjI4Njc0Ni44Nzc0MDE4fQ.pEgdmFAy73walFonEm2zbxg46Oth3dlT02HR9iVzXa8

  上面一段很长的字符串到底是怎么来的呢?就需要了解JWT的构成原理。

2. JWT的构成

 JWT由三部分组成,如下图,分别是:Header头部、Payload负载、Signature签名。

  

(1). 头部(Header)

  通常包括两部分,类型(如 “typ”:“JWT”)和加密算法(如“alg”:"HS256"),当然你也可以添加其它自定义的一些参数,然后对这个对象机型base64编码,生成一段字符串,

如“eyJ0eXAxIjoiMTIzNCIsImFsZzIiOiJhZG1pbiIsInR5cCI6IkpXVCIsImFsZyI6IkhTMjU2In0”,我们可以对其进行反编码一下,看一下其庐山真面目。

 注:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

(2). 负载(Payload)

  通常用来存放一些业务需要但不敏感的信息,比如:用户编号(userId)、用户账号(userAccount)、权限等等,该部分也有一些默认的声明,如下图,很多不常用。

  • iss: jwt签发者
  • sub: jwt所面向的用户
  • aud: 接收jwt的一方
  • exp: jwt的过期时间,这个过期时间必须要大于签发时间
  • nbf: 定义在什么时间之前,该jwt都是不可用的.
  • iat: jwt的签发时间
  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

其中最常用的就是exp过期时间,要和1970年1月1日那个点进行比对,用法如下,下面表示生成jwt字符串后20分钟过期。

最后对该部分组装成的对象进行base64编码,如:“eyJVc2VySWQiOjEyMywiVXNlck5hbWUiOiJhZG1pbiIsImV4cCI6MTU1MjI4Njc0Ni44Nzc0MDE4fQ”,我们可以对其进行反编码看一下庐山真面目,如下图:

注:该部分也是可以解码的,所以不要存储敏感信息。

(3). 签名(Signature)

  这个部分需要base64加密后的headerbase64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

伪代码如下:

1 var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
2 var signature = HMACSHA256(encodedString, 'sercret密钥')

  说明:密钥存在服务器端,不要泄露,在不知道密钥的情况下,是不能进行解密的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。

  特别说明:即使payload中的信息被篡改,服务器端通过signature就可以判断出来是非法请求,即校验不能通过。

3. 代码尝鲜

  需要通过Nuget装JWT包,新版本的jwt建议.Net 版本4.6起。

 1         [HttpGet]2         public string JiaM()3         {4             //设置过期时间(可以不设置,下面表示签名后 20分钟过期)5             double exp = (DateTime.UtcNow.AddMinutes(20) - new DateTime(1970, 1, 1)).TotalSeconds;6             var payload = new Dictionary<string, object>7             {8                 { "UserId", 123 },9                 { "UserName", "admin" },
10                 {"exp",exp }   //该参数也可以不写
11             };
12             var secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";//不要泄露
13             IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
14
15             //注意这个是额外的参数,默认参数是 typ 和alg
16             var headers = new Dictionary<string, object>
17             {
18                 { "typ1", "1234" },
19                 { "alg2", "admin" }
20             };
21
22             IJsonSerializer serializer = new JsonNetSerializer();
23             IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
24             IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
25             var token = encoder.Encode(headers, payload, secret);
26             return token;
27         }
28
29        [HttpGet]
30         public string JieM(string token)
31         {
32             var secret = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";
33             try
34             {
35                 IJsonSerializer serializer = new JsonNetSerializer();
36                 IDateTimeProvider provider = new UtcDateTimeProvider();
37                 IJwtValidator validator = new JwtValidator(serializer, provider);
38                 IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
39                 IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder);
40                 var json = decoder.Decode(token, secret, true);
41                 return json;
42             }
43             catch (TokenExpiredException)
44             {
45                 //过期了自动进入这里
46                 return "Token has expired";
47             }
48             catch (SignatureVerificationException)
49             {
50                 //校验未通过自动进入这里
51                 return "Token has invalid signature";
52             }
53             catch (Exception)
54             {
55                 //其它错误,自动进入到这里
56                 return "other error";
57             }

上述代码方便通过PostMan进行快速测试,注意解密的方法中的三个catch,token过期,会自动进入到TokenExpiredException异常中,token验证不通过,会自动进入SignatureVerificationException中。

三. JWT的使用流程

   整体流程,大致如下图:

1. 客户端(前端或App端)通过一个Http请求把用户名和密码传到登录接口,建议采用Https的模式,避免信息被嗅探。

2. 服务器端校验登录的接口验证用户名和密码通过后,把一些业务逻辑需要的信息如:userId、userAccount放到Payload中,进而生成一个xxx.yyy.zzz形式的JWT字符串返回给客户端。

3. 客户端获取到JWT的字符串,可以存放到LocalStorage中,注意退出的登录的时候删除该值。

4. 登录成功,每次请求其它接口的时候都在表头带着该jwt字符串,建议放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)  或者自己命名比如:“auth”,进行该字符串的传递。

5. 服务器端要写一个过滤器,在该过滤器中进行校验jwt的有效性(签名是否正确、是否过期),验证通过进行接口的业务逻辑,验证不通过,返回给客户端。

这里要解决两个问题?

(1). 在WebApi的过滤器中,如果校验通过了,如何将解密后的值传递到action中。(解密两次就有点坑了)

(2). 在WebApi的过滤器中,如果校验不通过,如何返回给客户端,然后客户端针对这种情况,又该如何接受呢。

(实战中揭晓)。

四. 项目实战

一. 整体目标:

  通过一个登陆接口和一个获取信息的接口模拟JWT的整套验证逻辑。

二. 详细步骤

1. 封装JWT加密和解密的方法。

 需要通过Nuget安装JWT的程序集,JWT的最新版本建议使用.Net 4.6 起。

 JWTHelp

2. 模拟登陆接口

 在登录接口中,模拟数据库校验,即账号和密码为admin和12345,即校验通过,然后把账号和userId(实际应该到数据库中查),这里也可以设置一下过期时间,比如20分钟,一同存放到PayLoad中,然后生成JWT字符串,返回给客户端。

        /// <summary>/// 模拟登陆/// </summary>/// <param name="userAccount"></param>/// <param name="pwd"></param>/// <returns></returns>[HttpGet]public string Login1(string userAccount, string pwd){try{//这里模拟数据操作,只要是admin和123456就验证通过if (userAccount == "admin" && pwd == "123456"){//1. 进行业务处理(这里模拟获取userId)string userId = "0806";//过期时间(可以不设置,下面表示签名后 20分钟过期)double exp = (DateTime.UtcNow.AddMinutes(20) - new DateTime(1970, 1, 1)).TotalSeconds;//进行组装var payload = new Dictionary<string, object>{{"userId", userId },{"userAccount", userAccount },{"exp",exp }};//2. 进行JWT签名var token = JWTHelp.JWTJiaM(payload);var result = new { result = "ok", token = token };return JsonConvert.SerializeObject(result);}else{var result = new { result = "error", token = "" };return JsonConvert.SerializeObject(result);}}catch (Exception){var result = new { result = "error", token = "" };return JsonConvert.SerializeObject(result);}}

3. 客户端调用登录接口

 这里只是单纯为了测试,使用的get请求,实际项目中建议post请求,且配置Https,请求成功后,把jwt字符串存放到localStorage中。

 1            //1.登录2             $('#j_jwtLogin').on('click', function () {3                 $.get("/api/Seventh/Login1", { userAccount: "admin", pwd: "123456" }, function (data) {4                     var jsonData = JSON.parse(data);5                     if (jsonData.result == "ok") {6                         console.log(jsonData.token);7                         //存放到本地缓存中8                         window.localStorage.setItem("token", jsonData.token);9                         alert("登录成功,ticket=" + jsonData.token);
10                     } else {
11                         alert("登录失败");
12                     }
13                 });
14             });

运行结果:

4. 服务器端过滤器

  代码中分享了两种获取header中信息的方式,获取到“auth”后,进行校验,校验不通过的话,通过状态码401返回给客户端,校验通过的话,则使用 actionContext.RequestContext.RouteData.Values.Add("auth", result); 进行解密值的存储,方便后续action的直接获取。

 1  /// <summary>2     /// 验证JWT算法的过滤器3     /// </summary>4     public class JWTCheck : AuthorizeAttribute5     {6         public override void OnAuthorization(HttpActionContext actionContext)7         {8             //获取表头Header中值的几种方式9             //方式一:
10             //{
11             //    var authHeader2 = from t in actionContext.Request.Headers
12             //                      where t.Key == "auth"
13             //                      select t.Value.FirstOrDefault();
14             //    var token2 = authHeader2.FirstOrDefault();
15             //}
16
17             //方式二:
18             IEnumerable<string> auths;
19             if (!actionContext.Request.Headers.TryGetValues("auth", out auths))
20             {
21                 //HttpContext.Current.Response.Write("报文头中的auth为空");
22                 //返回状态码验证未通过,并返回原因(前端进行401状态码的捕获),注意:这句话并不能阶段该过滤器,还会继续往下走,要借助if-else
23                 actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("报文头中的auth为空"));
24             }
25             else
26             {
27                 var token = auths.FirstOrDefault();
28                 if (token != null)
29                 {
30                     if (!string.IsNullOrEmpty(token))
31                     {
32                         var result = JWTHelp.JWTJieM(token);
33                         if (result == "expired")
34                         {
35                             //返回状态码验证未通过,并返回原因(前端进行401状态码的捕获)
36                             actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("expired"));
37                         }
38                         else if (result == "invalid")
39                         {
40                             //返回状态码验证未通过,并返回原因(前端进行401状态码的捕获)
41                             actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("invalid"));
42                         }
43                         else if (result == "error")
44                         {
45                             //返回状态码验证未通过,并返回原因(前端进行401状态码的捕获)
46                             actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("error"));
47                         }
48                         else
49                         {
50                             //表示校验通过,用于向控制器中传值
51                             actionContext.RequestContext.RouteData.Values.Add("auth", result);
52                         }
53                     }
54                 }
55                 else
56                 {
57                     //返回状态码验证未通过,并返回原因(前端进行401状态码的捕获)
58                     actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, new HttpError("token 空"));
59                 }
60             }
61
62         }
63     }

5.服务器端获取信息的的方法

  将上说过滤器以特性的形式作用在该方法中,然后通过 RequestContext.RouteData.Values["auth"] 获取到解密后的值,进而进行其它业务处理。

 1         /// <summary>2         /// 加密后的获取信息3         /// </summary>4         /// <returns></returns>5         [JWTCheck]6         [HttpGet]7         public string GetInfor()8         {9             var userData = JsonConvert.DeserializeObject<userData>(RequestContext.RouteData.Values["auth"].ToString()); ;
10             if (userData == null)
11             {
12                 var result = new { Message = "error", data = "" };
13                 return JsonConvert.SerializeObject(result);
14             }
15             else
16             {
17                 var data = new { userId = userData.userId, userAccount = userData.userAccount };
18                 var result = new { Message = "ok", data =data };
19                 return JsonConvert.SerializeObject(result);
20             }
21         }

6. 客户端调用获取信息的方法 

  前端获取到localStorage中token值,采用自定义header的方式以“auth”进行传递调用服务器端的方法,由于服务器的验证token不正确的时候,是以状态码的形式返回,所以这里要采用error方法,通过xhr.status==401进行判断,凡是进入到这个401中,均是token验证没有通过,具体是什么原因,可以通过xhr.responseText获取详细的值进行判断。

 1           //2.获取信息2             $('#j_jwtGetInfor').on('click', function () {3                 //从本地缓存中读取token值4                 var token = window.localStorage.getItem("token");5                 $.ajax({6                     url: "/api/Seventh/GetInfor",7                     type: "Get",8                     data: {},9                     datatype: "json",
10                     //设置header的方式1
11                     headers: { "auth": token},
12                     //设置header的方式2
13                     //beforeSend: function (xhr) {
14                     //    xhr.setRequestHeader("auth", token)
15                     //},
16                     success: function (data) {
17                         console.log(data);
18                         var jsonData = JSON.parse(data);
19                         if (jsonData.Message == "ok") {
20                             var myData = jsonData.data;
21                             console.log("获取成功");
22                             console.log(myData.userId);
23                             console.log(myData.userAccount);
24                         } else {
25                             console.log("获取失败");
26                         }
27                     },
28                     //当安全校验未通过的时候进入这里
29                     error: function (xhr) {
30                         if (xhr.status == 401) {
31                             console.log(xhr.responseText);
32                             var jsonData = JSON.parse(xhr.responseText);
33                             console.log("授权失败,原因为:" + jsonData.Message);
34                         }
35                     }
36                 });
37             });

运行结果:

其他的如token过期只需要改一下电脑时间即可以测试,token不正确改一下获取到的jwt字符串可以测试,这里不再进行 了。

第九节:JWT简介和以JS+WebApi为例基于JWT的安全校验相关推荐

  1. js如何获取jwt信息_学习后端鉴权系列: 基于JWT的会话管理

    内容回顾 上一节讲了基于Cookie+Session的认证方案. Even:学习后端鉴权系列: 基于Cookie, Session认证​zhuanlan.zhihu.com 由于基于Session方案 ...

  2. 第九节:点云PointCloud(第2部分,CloudCompare 处理点云)【Three.js整理】

     点云相关文章: 第八节:点云Point Cloud(第1部分,点云介绍)[three.js理论篇]_web搅拌机的博客-CSDN博客 第九节:点云Point Cloud(第2部分,CloudComp ...

  3. 第 1 节 JavaScript简介、JS的HelloWorld、JS编写位置

    第1节 JavaScript简介 JavaScript简介 实现 特点 JS的HelloWorld JS编写位置 JavaScript简介 实现 ECMAScript 是一个标准,一般情况下这两个词被 ...

  4. .NET Core + JWT令牌认证 + Vue.js 通用动态权限(RBAC)管理系统框架[DncZeus]开源啦!!!...

    DncZeus 前言 关于 DncZeus DncZeus = Dnc + Zeus "Dnc"--.Net Core 的缩写: "Zeus"--中文译为宙斯, ...

  5. 开源干货!.NET Core + JWT令牌认证 + Vue.js通用动态权限(RBAC)管理系统框架[DncZeus]开源

    DncZeus 前言 关于 DncZeus DncZeus = Dnc + Zeus "Dnc"–.Net Core 的缩写: "Zeus"–中文译为宙斯,是古 ...

  6. JavaWeb学习笔记(狂神版)--- 第九节 Cookie与Session

    第九节 Cookie与Session 目录 第九节 Cookie与Session 9.1 会话 9.2 Cookie 9.3 Session 9.1 会话 会话:用户打开一个浏览器,点了很多超链接,访 ...

  7. web安全通信之JWT简介

    jwt简介 JWT是JSON Web Token的简称,它是一个非常轻巧的规范.这个规范允许我们使用JWT在用户和服务器之间传递安全可靠的信息. 和Cookie-Session的模式不同,JWT使用T ...

  8. 【java】兴唐课程第五节到第九节知识点总结

    第九节 1. 代码:void readBook(String- bookNames) 表示不确定参数的个数,此时变量为一个数组. 2.当方法中的参数名称(如stuname)和属性名称相同时. this ...

  9. 《STM32库开发实战指南:基于STM32F103(第2版)》——2.1节仿真器简介

    本节书摘来自华章社区<STM32库开发实战指南:基于STM32F103(第2版)>一书中的第2章,第2.1节仿真器简介,作者刘火良 杨森,更多章节内容可以访问云栖社区"华章社区& ...

最新文章

  1. BZOJ 1050: [HAOI2006]旅行comf(枚举+并查集)
  2. 解决读写Excel的第三方类库as3xls无法读取中文和写入中文的问题
  3. 课后作业-阅读任务-阅读提问-3 。
  4. Android初学:联系创建Activity
  5. 【Qt】QWidget类详解(函数篇)
  6. 前端小知识点(8):作者为什么设计原型
  7. 名片识别信息分类python_python体验名片识别OCR
  8. VC实现在ListBox中选择背景位图
  9. RabbitMQ(四) Work模式下的消息产生以及消费代码实现示例
  10. bomb android实战,android 仿qq app源码下载(bmob)
  11. Nodejs的url模块方法
  12. 敏捷个人新体系学习 - 1.实践的艺术
  13. linux分布式安装hadoop1.2
  14. 002-一般处理程序(HttpHandler)
  15. python强制转型,python2--python3如何转型
  16. 文件管理器 Root Explorer v2.9.4 零售完全版
  17. ps钢笔路径打不出字怎么解决?
  18. ANE for Android Setp by Step
  19. API接口开发其实特简单,Python Flask Web 框架教程来了
  20. java 时间类 joda_Java Joda-Time 处理时间工具类(JDK1.7以上)

热门文章

  1. Exp3 免杀原理与实践
  2. android旋转动画的两种实现方式
  3. 深入理解C# 静态类与非静态类、静态成员的区别 [转载]
  4. iOS开发——高级技术本地化与国际化详解
  5. coco2dx c++ HTTP实现
  6. Struts2从一个action转到另一个action的两种方法
  7. 一些有用的javascript实例分析(二)
  8. 关于php的字符串编码
  9. linux命令 sed 有的功能有,Linux命令:sed简介
  10. docker安装mysql数据库映射_Docker安装mysql数据库