任何技术框架都有自身的局限性,不可能一劳永逸,JWT也不例外。本文将从JWT的概念、基本原理和适用范围来剖析JWT并不是银弹,需要谨慎处理。

众所周知,如果账户信息(用户名和密码)泄露,存储在服务器上的隐私数据将受到毁灭性的打击,如果是管理员的账户信息泄露,系统还有被攻击的危险。那么,JWT的信息发生泄露,会带来什么样的影响?该如何防范?

1、什么是Token


JWT入坑爬坑指南【收藏篇】_java

 

Token的主要作用是验证身份的合法性,以允许计算机系统的用户可以操作系统资源。Token(令牌)通常是指Security Token(安全令牌),可分为Hardware Token(硬件令牌),Authentication Token(授权令牌),USB Token(USB令牌),Cryptographic Token(加密令牌),Virtual Token(虚拟令牌)和 Key Fob(钥匙卡)。

生活中常见的令牌如:登录密码,指纹,声纹,门禁卡,银行电子卡等。

这里所讲的Token,主要目的是为计算机系统提供一个可以识别用户的任意数值,像“token123”的明文字符串,或像“41ea873f-3a4d-57c8-1e38-ef74f31015af”之类的加密字符。

2、什么是JSON Web Token

JWT入坑爬坑指南【收藏篇】_java_02

 

JSON Web Token(JWT)是一个基于RFC7519的开放数据标准,它定义了一种宽松且紧凑的数据组合方式,使用JSON对象在各应用之间传输加密信息。

该JSON对象可以通过数字签名进行鉴签和校验,一般可采用HMAC算法、RSA或者ECDSA的公钥/私钥对数据进行签名操作。

JWT通常有HEADER (头),PAYLOAD (有效载荷)和SIGNATURE (签名)三个部分组成,三者之间使用“.”链接,格式如下:


JWT入坑爬坑指南【收藏篇】_java_03

下面字符串是一个JWT的实际案例:

JWT入坑爬坑指南【收藏篇】_java_04
为了更直观的了解JWT的创建过程和使用方式,通过一个简单的例子来演示这两个过程。注意三者之间有一个点号(“.”)相连

3、如何创建JWT?

JWT通常由“标头.有效载荷.签名”的格式组成。其中,标头用于存储有关如何计算JWT签名的信息,如对象类型,签名算法等。下面是JWT中Header部分的JSON对象实例:

 

JWT入坑爬坑指南【收藏篇】_java_05
其中,type表示该对象为JWT,alg表示创建JWT时使用HMAC-SHA256散列算法计算签名。有效载荷主要用于存储用户信息,如用户id,email,角色和权限信息等。下面是有效载荷的一个简单示例:

JWT入坑爬坑指南【收藏篇】_java_06

签名需要使用Base64URL编码技术对标头和有效载荷进行编码,并作为参数和秘钥一同传递给签名算法,生成最终的签名 (Signature)。以HMAC-SHA256算法为例,下面是生成签名的一个伪代码:


JWT入坑爬坑指南【收藏篇】_java_07

4-1、依赖
JWT入坑爬坑指南【收藏篇】_java_08

4、基于 Java 实现的 JWT至此,已经了解了JWT的基本原理,接下来将使用Java来演示生成JWT的完整过程。

以Maven工程为例,需要在pom.xml文件中添加如下的配置信息:


JWT入坑爬坑指南【收藏篇】_java_09

如果是非Maven工程,可以到Maven中央仓库搜索jjwt,选择相应的版本(0.9.0)下载到本地,并将jar包添加到工程的类路径(classpath)中。

JWT入坑爬坑指南【收藏篇】_java_10

在工程中新建JJWTUitls.java工具类,使用jjwt提供的方法实现JWT的生成,实现细节如下:4-2、生成JWT


JWT入坑爬坑指南【收藏篇】_java_11此方法中JJWT已经处理好JWT标头(Header)的信息,我们只需要提供签名所使用的算法(如SignatureAlgorithm.HS256),有效载荷,主题(包含了用户信息),过期时间(exp-time)和秘钥即可,最后使用jjwt的builder()方法组装JWT。下面是生成秘钥方法key()的源代码:


JWT入坑爬坑指南【收藏篇】_java_12

用JJWT解析JWT信息相对简单,首先获取秘钥,然后通过Jwts.parse()方法设置秘钥并JWT进行解析,实现细节如下:4-3、解析JWT


JWT入坑爬坑指南【收藏篇】_java_13

最后,在工程中新建一个JavaJWT.java 类,并在main方法中检验JJWTUtils工具类中生成和解析JWT两个方法是否有效。实现细节如下:4-4、测试JJWT


JWT入坑爬坑指南【收藏篇】_java_14

如上图所示,“jwt”将作为JWT标头(Header) “type” 的值,有效载荷(payload)中的主题信息如下:

JWT入坑爬坑指南【收藏篇】_java_15

且JWT签名的有效时间为60,000毫秒。执行main方法,输出信息如下所示:

JWT入坑爬坑指南【收藏篇】_java_165、 JWT 工作流程从测试结果可以看出,成功的使用JJWT创建并解析了JWT。接下来,我们将了解到在实际的应用中,JWT对用户信息进行验证的基本流程。

在身份验证中,当用户成功登录系统时,授权服务器将会把JWT返回给客户端,用户需要将此凭证信息存储在本地(cookie或浏览器缓存)。

当用户发起新的请求时,需要在请求头中附带此凭证信息,当服务器接收到用户请求时,会先检查请求头中有无凭证,是否过期,是否有效。

如果凭证有效,将放行请求;若凭证非法或者过期,服务器将回跳到认证中心,重新对用户身份进行验证,直至用户身份验证成功。以访问API资源为例,下图显示了获取并使用JWT的基本流程:

 


如果身份验证服务器和应用服务器完全独立,则应用服务器的JWT校验工作也可以交由认证服务器完成。在上述的案例中,我们使用HS256算法对JWT进行签名,在这个过程中,只有身份验证服务器和应用服务器知道秘钥是什么。

当客户端对应用服务器发起调用时,应用服务器会使用秘钥对签名进行校验,如果签名有效且未过期,则允许客户端的请求,反之则拒绝请求。

6、使用 JWT 的利弊

优势与劣势是相对而言的,这里主要以传统的Session模式作为参考,总结使用JWT可以获得优势以及带来的弊端。

6-1、 使用 JWT 的优势

使用JWT保护应用安全,至少可以获得以下优势:

更少的数据库连接:因其基于算法来实现身份认证,在使用JWT时查询数据的次数更少(更少的数据连接不等于不连接数据库),可以获得更快的系统响应时间。

构建更简单:如果应用程序本身是无状态的,那么选择JWT可以加快系统构建过程。

跨服务调用:可以构建一个认证中心来处理用户身份认证和发放签名的工作,其他应用服务在后续的用户请求中不需要(理论上)在询问认证中心,可使用自有的公钥对用户签名进行验证。

无状态:不需要向传统的Web应用那样将用户状态保存于Session中。

6-2、使用 JWT 的弊端

JWT不是万能的,使用JWT时可能会面临以下麻烦:

严重依赖于秘钥:JWT的生成与解析过程都需要依赖于秘钥(Secret),且都以硬编码的方式存在于系统中(也有放在外部配置文件中的)。如果秘钥不小心泄露,系统的安全性将受到威胁。

 

服务端无法管理客户端的信息:如果用户身份发生异常(信息泄露,或者被攻击),服务端很难向操作Session那样主动将异常用户进行隔离。

 

服务端无法主动推送消息:服务端由于是无状态的,将无法使用像Session那样的方式推送消息到客户端,例如过期时间将至,服务端无法主动为用户续约,需要客户端向服务端发起续约请求。

 

冗余的数据开销:一个JWT签名的大小要远比一个Session ID长很多,如果对有效载荷(payload)中的数据不做有效控制,其长度会成几何倍数增长,且在每一次请求时都需要负担额外的网络开销。

 

JWT相比于Session,OIDC(OpenId Connect)等技术还比较新,支持的库还比较少。而且JWT也并非比传统Session更安全,它们都没有解决CSRF和XSS的问题。因此,在决定使用JWT前,需要仔细考虑其利弊。

 

7、JWT 并非银弹

考虑这样一个问题:如果客户端的JWT令牌泄露或者被盗取,会发生什么严重的后果?有什么补救措施?

 

如果单纯依靠JWT解决用户认证的所有问题,那么系统的安全性将是脆弱的。

 

由于JWT令牌存储于客户端中,一旦客户端存储的令牌发生泄露事件或者被攻击,攻击者就可以轻而易举的伪造用户身份去修改/删除系统资源。

 

虽然JWT自带过期时间,但在过期之前,攻击者可以肆无忌惮的操作系统数据。通过算法来校验用户身份合法性是JWT的优势,也是最大的弊端——太过于依赖算法。

 

反观传统的用户认证措施,通常会包含多种组合,如手机验证码,人脸识别,语音识别,指纹锁等。

 

用户名和密码只做用户身份识别使用,当用户名和密码泄露后,在遇到敏感操作时(如新增,修改,删除,下载,上传),都会采用其他方式对用户的合法性进行验证(发送验证码,邮箱验证码,指纹信息等)以确保数据安全。

 

与传统的身份验证方式相比,JWT过多的依赖于算法,缺乏灵活性,而且服务端往往是被动执行用户身份验证操作,无法及时对异常用户进行隔离。

 

那是否有补救措施呢?答案是肯定的。接下来,将介绍在发生令牌泄露事件后,如何保证系统的安全。

8、JWT 爬坑指南

不管是基于Sessions还是基于JWT,一旦密令被盗取,都是一件棘手的事情。下面介绍JWT发生令牌泄露是该采取什么样的措施(包含但不局限于此)。

 


JWT入坑爬坑指南【收藏篇】_java_17为了防止用户JWT令牌泄露而威胁系统安全,可以在以下方面完善系统功能:

清除已泄露的令牌:最直接也容易实现。将JWT令牌在服务端也存储一份,若发现有异常的令牌存在,则从服务端将此异常令牌清除。当用户发起请求时,强制用户重新进行身份验证,直至验证成功。服务端令牌的存储,可以借助Redis等缓存服务器进行管理,也可使用Ehcache将令牌信息存储在内存中。

 

敏感操作保护:在涉及到诸如新增,修改,删除,上传,下载等敏感性操作时,定期(30分钟,15分钟甚至更短)检查用户身份,如手机验证码,扫描二维码等手段,确认操作者是用户本人。如果身份验证不通过,则终止请求,并要求重新验证用户身份信息。

 

地域检查:通常用户会在一个相对固定的地理范围内访问应用程序,可以将地理位置信息作为辅助来甄别。如果发现用户A由经常所在的地区1变到了相对较远的地区2,或者频繁在多个地区间切换,不管用户有没有可能在短时间内在多个地域活动(一般不可能),都应当终止当前请求,强制用户重新进行验证身份,颁发新的JWT令牌,并提醒(或要求)用户重置密码。

 

监控请求频率:如果JWT密令被盗取,攻击者或通过某些工具伪造用户身份,高频次的对系统发送请求,以套取用户数据。针对这种情况,可以监控用户在单位时间内的请求次数,当单位时间内的请求次数超出预定阈值值,则判定该用户密令是有问题的。例如1秒内连续超过5次请求,则视为用户身份非法,服务端终止请求并强制将该用户的JWT密令清除,然后回跳到认证中心对用户身份进行验证。

 

客户端环境检查:对于一些移动端应用来说,可以将用户信息与设备(手机,平板)的机器码进行绑定,并存储于服务端中,当客户端发起请求时,可以先校验客户端的机器码与服务端的是否匹配,如果不匹配,则视为非法请求,并终止用户的后续请求。

总结

JWT的出现,为解决Web应用安全性问题提供了一种新思路。但JWT并不是银弹,仍然需要做很多复杂的工作才能提升系统的安全性。

当然,世上没有完美的解决方案,系统的安全性需要开发者积极主动地去提升,其过程是漫长且复杂的。