讲真,别再使用JWT了!

作者: ThoughtWorks | 来源:发表于2017-08-16 08:51 被阅读44723次

    摘要:

    • 在Web应用中,使用JWT替代session并不是个好主意
    • 适合JWT的使用场景

    抱歉,当了回标题党。我并不否认JWT的价值,只是它经常被误用。

    什么是JWT

    根据维基百科的定义,JSON WEB TokenJWT,读作 [/dʒɒt/]),是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。JWT通常由三部分组成: 头信息(header), 消息体(payload)和签名(signature)。

    头信息指定了该JWT使用的签名算法:

    header = '{"alg":"HS256","typ":"JWT"}'
    

    HS256 表示使用了 HMAC-SHA256 来生成签名。

    消息体包含了JWT的意图:

    payload = '{"loggedInAs":"admin","iat":1422779638}'//iat表示令牌生成的时间
    

    未签名的令牌由base64url编码的头信息和消息体拼接而成(使用"."分隔),签名则通过私有的key计算而成:

    key = 'secretkey'  
    unsignedToken = encodeBase64(header) + '.' + encodeBase64(payload)  
    signature = HMAC-SHA256(key, unsignedToken) 
    

    最后在未签名的令牌尾部拼接上base64url编码的签名(同样使用"."分隔)就是JWT了:

    token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature) 
    
    # token看起来像这样: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJsb2dnZWRJbkFzIjoiYWRtaW4iLCJpYXQiOjE0MjI3Nzk2Mzh9.gzSraSYS8EXBxLN_oWnFSRgCzcmJmMjLiuyu5CSpyHI 
    

    JWT常常被用作保护服务端的资源(resource),客户端通常将JWT通过HTTP的Authorization header发送给服务端,服务端使用自己保存的key计算、验证签名以判断该JWT是否可信:

    Authorization: Bearer eyJhbGci*...<snip>...*yu5CSpyHI
    

    那怎么就误用了呢

    近年来RESTful API开始风靡,使用HTTP header来传递认证令牌似乎变得理所应当,而单页应用(SPA)、前后端分离架构似乎正在促成越来越多的WEB应用放弃历史悠久的cookie-session认证机制,转而使用JWT来管理用户session。支持该方案的人认为:

    1.该方案更易于水平扩展

    在cookie-session方案中,cookie内仅包含一个session标识符,而诸如用户信息、授权列表等都保存在服务端的session中。如果把session中的认证信息都保存在JWT中,在服务端就没有session存在的必要了。当服务端水平扩展的时候,就不用处理session复制(session replication)/ session黏连(sticky session)或是引入外部session存储了。

    从这个角度来说,这个优点确实存在,但实际上外部session存储方案已经非常成熟了(比如Redis),在一些Framework的帮助下(比如spring-sessionhazelcast),session复制也并没有想象中的麻烦。所以除非你的应用访问量非常非常非常(此处省略N个非常)大,使用cookie-session配合外部session存储完全够用了。

    2.该方案可防护CSRF攻击

    跨站请求伪造Cross-site request forgery(简称CSRF, 读作 [sea-surf])是一种典型的利用cookie-session漏洞的攻击,这里借用spring-security的一个例子来解释CSRF:

    假设你经常使用bank.example.com进行网上转账,在你提交转账请求时bank.example.com的前端代码会提交一个HTTP请求:

    POST /transfer HTTP/1.1
    Host: bank.example.com
    cookie: JsessionID=randomid; Domain=bank.example.com; Secure; HttpOnly
    Content-Type: application/x-www-form-urlencoded
    
    amount=100.00&routingNumber=1234&account=9876
    

    你图方便没有登出bank.example.com,随后又访问了一个恶意网站,该网站的HTML页面包含了这样一个表单:

    <form action="https://bank.example.com/transfer" method="post">
        <input type="hidden" name="amount" value="100.00"/>
        <input type="hidden" name="routingNumber" value="evilsRoutingNumber"/>
        <input type="hidden" name="account" value="evilsAccountNumber"/>
        <input type="submit" value="点击就送!"/>
    </form>
    

    你被“点击就送”吸引了,当你点了提交按钮时你已经向攻击者的账号转了100元。现实中的攻击可能更隐蔽,恶意网站的页面可能使用Javascript自动完成提交。尽管恶意网站没有办法盗取你的session cookie(从而假冒你的身份),但恶意网站向bank.example.com发起请求时,你的cookie会被自动发送过去。

    因此,有些人认为前端代码将JWT通过HTTP header发送给服务端(而不是通过cookie自动发送)可以有效防护CSRF。在这种方案中,服务端代码在完成认证后,会在HTTP response的header中返回JWT,前端代码将该JWT存放到Local Storage里待用,或是服务端直接在cookie中保存HttpOnly=false的JWT。

    在向服务端发起请求时,用Javascript取出JWT(否则前端Javascript代码无权从cookie中获取数据),再通过header发送回服务端通过认证。由于恶意网站的代码无法获取bank.example.com的cookie/Local Storage中的JWT,这种方式确实能防护CSRF,但将JWT保存在cookie/Local Storage中可能会给另一种攻击可乘之机,我们一会详细讨论它:跨站脚本攻击——XSS。

    3.该方案更安全

    由于JWT要求有一个秘钥,还有一个算法,生成的令牌看上去不可读,不少人误认为该令牌是被加密的。但实际上秘钥和算法是用来生成签名的,令牌本身不可读仅是因为base64url编码,可以直接解码,所以如果JWT中如果保存了敏感的信息,相对cookie-session将数据放在服务端来说,更不安全。

    除了以上这些误解外,使用JWT管理session还有如下缺点:

    1. 更多的空间占用。如果将原存在服务端session中的各类信息都放在JWT中保存在客户端,可能造成JWT占用的空间变大,需要考虑cookie的空间限制等因素,如果放在Local Storage,则可能受到XSS攻击。

    2. 更不安全。这里是特指将JWT保存在Local Storage中,然后使用Javascript取出后作为HTTP header发送给服务端的方案。在Local Storage中保存敏感信息并不安全,容易受到跨站脚本攻击,跨站脚本(Cross site script,简称xss)是一种“HTML注入”,由于攻击的脚本多数时候是跨域的,所以称之为“跨域脚本”,这些脚本代码可以盗取cookie或是Local Storage中的数据。可以从这篇文章查看XSS攻击的原理解释。

    3. 无法作废已颁布的令牌。所有的认证信息都在JWT中,由于在服务端没有状态,即使你知道了某个JWT被盗取了,你也没有办法将其作废。在JWT过期之前(你绝对应该设置过期时间),你无能为力。

    4. 不易应对数据过期。与上一条类似,JWT有点类似缓存,由于无法作废已颁布的令牌,在其过期前,你只能忍受“过期”的数据。

    看到这里后,你可能发现,将JWT保存在Local Storage中,并使用JWT来管理session并不是一个好主意,那有没有可能“正确”地使用JWT来管理session呢?比如:

    • 不再使用Local Storage存储JWT,使用cookie,并且设置HttpOnly=true,这意味着只能由服务端保存以及通过自动回传的cookie取得JWT,以便防御XSS攻击
    • 在JWT的内容中加入一个随机值作为CSRF令牌,由服务端将该CSRF令牌也保存在cookie中,但设置HttpOnly=false,这样前端Javascript代码就可以取得该CSRF令牌,并在请求API时作为HTTP header传回。服务端在认证时,从JWT中取出CSRF令牌与header中获得CSRF令牌比较,从而实现对CSRF攻击的防护
    • 考虑到cookie的空间限制(大约4k左右),在JWT中尽可能只放“够用”的认证信息,其他信息放在数据库,需要时再获取,同时也解决之前提到的数据过期问题

    这个方案看上去是挺不错的,恭喜你,你重新发明了cookie-session,可能实现还不一定有现有的好。

    那究竟JWT可以用来做什么

    我的同事做过一个形象的解释:

    JWT(其实还有SAML)最适合的应用场景就是“开票”,或者“签字”。

    在有纸化办公时代,多部门、多组织之间的协同工作往往会需要拿着A部门领导的“签字”或者“盖章”去B部门“使用”或者“访问”对应的资源,其实这种“领导签字/盖章”就是JWT,都是一种由具有一定权力的实体“签发”并“授权”的“票据”。一般的,这种票据具有可验证性(领导签名/盖章可以被验证,且难于模仿),不可篡改性(涂改过的文件不被接受,除非在涂改处再次签字确认);并且这种票据一般都是“一次性”使用的,在访问到对应的资源后,该票据一般会被资源持有方收回留底,用于后续的审计、追溯等用途。

    举两个例子:

    1. 员工李雷需要请假一天,于是填写请假申请单,李雷在获得其主管部门领导签字后,将请假单交给HR部门韩梅梅,韩梅梅确认领导签字无误后,将请假单收回,并在公司考勤表中做相应记录。
    2. 员工李雷和韩梅梅因工外出需要使用公司汽车一天,于是填写用车申请单,签字后李雷将申请单交给车队司机老王,乘坐老王驾驶的车辆外出办事,同时老王将用车申请单收回并存档。

    在以上的两个例子中,“请假申请单”和“用车申请单”就是JWT中的payload,领导签字就是base64后的数字签名,领导是issuer,“HR部门的韩梅梅”和“司机老王”即为JWT的audience,audience需要验证领导签名是否合法,验证合法后根据payload中请求的资源给予相应的权限,同时将JWT收回。

    放到系统集成的场景中,JWT更适合一次性操作的认证:

    服务B你好, 服务A告诉我,我可以操作<JWT内容>, 这是我的凭证(即JWT)

    在这里,服务A负责认证用户身份(相当于上例中领导批准请假),并颁布一个很短过期时间的JWT给浏览器(相当于上例中的请假单),浏览器(相当于上例中的请假员工)在向服务B的请求中带上该JWT,则服务B(相当于上例中的HR员工)可以通过验证该JWT来判断用户是否有权执行该操作。这样,服务B就成为一个安全的无状态的服务了。

    总结

    1. 在Web应用中,别再把JWT当做session使用,绝大多数情况下,传统的cookie-session机制工作得更好
    2. JWT适合一次性的命令认证,颁发一个有效期极短的JWT,即使暴露了危险也很小,由于每次操作都会生成新的JWT,因此也没必要保存JWT,真正实现无状态。

    文/ThoughtWorks 周宇刚

    相关文章

      网友评论

      • Y先生_ce2c:rsa给你加个密。你给我破解试试?
      • 5d99d2317383:好久之前的文章了,有翻出来了
      • HolloWord:jwt如果不使用base64生成,还有更安全的使用rsa加密生成,这个绝对破解不了
        Daemonw:header和payload不是一直是base64吗?签名肯定是使用的安全算法啦
      • a9e3b7379cc4:标题党举报了
      • MrTT:这里其实应该分开讨论,B/S和C/S两种模式,浏览器原本的方案为cookie session,如果拿token替换,反而得不偿失,一般在service端签发token,访问其他服务。而c/s则应当考虑如何安全的存储token。另外现在的b越来越像c了,所以。。。。😏
      • kopshome:jwt只是一种方式而已,关键在于复杂系统下,服务的有状态,实在是束缚手脚,作者说的不要滥用还是有道理的,毕竟要结合自己的实际情况,但是鉴权的方式来说,这已经是大势所趋,作者反驳的有些苍白,当然了,对于小应用或者初期不复杂的情况,其实都无所谓
      • 18c3d1880952:还一次性认证呢,所有操作的server都能签发新的jwt?
        这不是引入更多的安全问题了么。
        楼主真的试过这样设计系统吗?
      • 35668136d67a:无状态实现本身就是谬论, 最终的绕不过去 服务器状态查询, 最终结果都是实现session, 只是一个词而已, jwt 只能说觉得更简单了, 就用了, 其他就别说了, 没意义
        Daemonw:jwt只是实现的服务端无状态,并没说客户端也无状态啊,jwt本身记录的就是状态,只是保存在客户端而已
      • 4001509114:反正也不一定要用jwt,jwt给大家的感觉就是一种规范,相信很多人现在的做法都是对所有传参以及时间戳通过secret进行不可逆加密,后台再通过相同加密方式去校验sign是否一致就好了。和jwt也没什么不同
      • 5fa9fe2af073:那请问要做到无状态使用什么来解决问题呢 ?
      • 追梦3000:jwt只是一种认证方式而已,和session没有本质区别 ,大部分系统大概不会照本宣科的用jwt。针对作者最后的结论,还有几点疑问:1、分布式系统怎么解决session问题? 2、jwt如何做到无状态的“一次”操作,既然无状态,系统是怎样判断jwt被用过了?
      • Platanuses:我想知道作者有没有给百度钱?
      • RobinJiang:标题党,看完标题,内容很水,至少作者看起来并没有在项目中很好的处理oauth的问题,想来本文最大的价值只有这个标题了,
      • 勤奋的大鱼:说一点我的见解,首先,如果网站有xss漏洞,也太low了吧,就算设置了httpOnly也还有其他很多办法能攻击啊。然后JWT的签名不是用的Base64加密的啊,肯定是用加盐的消息摘要算法。不过文章中对JWT的缺点还是值得一看的,但确实是标题党
      • 逍遥子_:看了作者被喷,忍不住登陆来支持下
      • 吐太阳的小葵花:看完了,觉得楼主的文章还是对我有挺大启发的。不过还是有个疑问,在现在前后端分离的这种模式下,多端(android/ios/h5)公用一个后端api,如果采用cookie-session该怎样解决跨域问题?
        成功的失败者:@吐太阳的小葵花 分别生成三种token分别管理认证
      • 447b87487fb6:其实文章内容说的很对,不要勿用滥用 jwt。一帮装最懂的人在底下评论。
        xiangtan:@吴所畏啊 他估计不是个写代码的
        18c3d1880952:文章的总结您也觉得对吗?
      • 倘若花落:你如果对输入验证xss还有机会吗,那么此时的jwt能扩大适用范围吗?
      • ksfkf:JWT 原则上也是一种 Session
        蝶遗尘梦:应该算是一种别样的cookie,没有人会将敏感信息存储到前台
      • loyal_6693:首先这个文章说的是对的,不同意这个文章观点的人还没有理解jwt.jwt确实有很多问题.首先无状态,决定了你无法主动销毁一个jwt token,导致用户的禁用/修改密码等操作无法主动让之前的jwt token失效.很多人就在db里做个关联什么的,那不就变成了cookies/session了吗?看了评论发现幼稚的人太多.根本没有理解无状态和jwt就出来乱喷.类似的文章国外多了去了.而且国外还有人更改了jwt的定义.让某部分不参与签名等方式.达到主动过期或失效等.
        Daemonw:请教一下,我的token签名的key采用密码的话,那么修改密码不就让之前的token失效了吗?为什么您说“导致用户的禁用/修改密码等操作无法主动让之前的jwt token失效”
        loyal_6693:但不参与签名带来的问题就是无法验明真身.也不是很合理.
      • 咖啡爷爷:有点瞎说了,看你怎么用
      • 秋月明:tw....
      • marlondu:所以HttpOnly 到底是设置成true还是false
      • 0bb4c43cbb13:楼主的文章确实有很多问题
      • RonnieChen888:对作者的更不安全,我并不赞同,xss安全问题在session机制上一样存在,session被盗取也一样能被伪造请求,原生请求甚至可以伪装Referer。
      • Xanthuim:只能说不同场景不同技术,就好比数据库用mybatis还是用hibernate一样,谁好?博主是在说不要误用,并没说不好。
      • ac0092a0f975:唉,为什么作者会被喷。。。看了评论区忍不住登陆来支持下
        ThoughtWorks:@海岸_2018 感人。。。
      • efafb47a48df:这样胡说八道都能排到百度前排???
      • f3aeee11043d:jwt那么不安全?那么不好吗?你明白其中原理吗?你确定你自己实现过jwt吗?题主是完全乱写,都不知道自己写的什么,最后也没给一个他认为最好的方案。
      • 8bb39b711c96:胡说八道
      • 美途2016:百度搜索jwt这篇文字排在第五位,首先标题党就不说了,点进来看内容发现作者完全不知所云。根本没理解jwt的使用场景。在当下前后端分离的情况下,客户端无状态访问怎么来确定session呢?这是最大的问题。所以希望作者在发表这类文章的时候多查阅资料,谨慎用词,不然真的是误导群众。
        cc6a95b3fcb1:@孤潭云影 话说cookie-session不也是走header传输的么。。。。。浏览器禁用session才走url参数
        孤潭云影:@轮子妈妈 说实话,无状态的含义如果非要深究的话,他说的是对的。你以为把SESSION换个名字,从COOKIE换成HEADER,换个地方存储,就是实现无状态了吗?这些说白了还是SESSION的变种,最多是替换了浏览器的默认实现。你不应该张嘴就是TM的。
        8feae25c446d:客户端无状态不能确定session?你确定你懂session吗?你不会tm以为cookie-session是完全依赖浏览器的吧
      • 6744a9f85ceb:已举报。
        kevinYY:确实得举报
      • 像牛嗷嗷:标题党呀
      • cc0da8ca8dc9:..ThoughtWorks已经沦落到这个水平了
      • youyinnn:signature部分并不是简单的Base64加密而成,你用Base64是解不开的.
      • 10d76bcee7b0:jwt只是一个token规范而已,用不用都无所谓,我用uuid照样可以实现token,作者估计没理解oauth2吧,在强调移动端的今天,session那套越来越显得落后了,而且现在的不同的前端,同一套后台,很多前端现在连cookie都没有了,现在行业内的老大基本上都是token鉴权了。
        Daemonw:但是uuid无法实现服务器的无状态吧?JWT之所以能无状态,因为它在payload部分存储了状态信息啊,服务端只用鉴权就行了。我觉得你用uuid来实现,本质上来说是一个session_id
      • ccc7b37c9552:内容没细看,但我对这标题确实无比认同。

        jwt最致命的问题,就是将用户标识明文(base64等同明文)的放在客户端,而且单一依赖同一个secret。一旦secret被泄露,那攻击者就可以毫不费力的冒充所有用户。

        而不幸的是,secret的保护并不足够:
        1. 一般作为配置,总有人容易接触到。(在安全领域,人是最不可靠的)
        2. 要更换secret的话,已签发的所有token都将失效(比如知道了secret的人要离职)
        3.算法是明文的,所以,攻击者通过暴力破解,有较大可能碰撞出secret,这是因为:
        a. 很多人用一些常见或简单的词语作为secret
        b. 碰撞时只需要本机运算,不会访问服务器(如果有足够的利益驱动,算力不是问题),也就是说服务端根本不会知道有人得知了secret。
        同样,如果有人碰撞出了secret,那他可以任意的构造不同的用户身份而且不会被发觉
        你也想起舞吗彡:@文章圣手 这和 qq密码一样么?,qq密码又不需要解密 ,不需要知道明文,
        223e299f463e:jwt本来就是为了将session放到客户端而设计的,是为了解决特定情况下session带来的BUG,不应该将jwt想的那么差。与jwt相比,session的缺点更为致命。你说的知道secret的人离职就token会失效?token过期也会失效的,失效怎么办?再生成一个咯。其实换secret其实根本没什么影响。还有用暴力破解的方法解出secret其实很难,类似你暴力破解一个足够长,字符足够丰富的QQ密码一样,花费也巨大,没有超级计算机,单凭市场上的渣渣电脑,几台一起解都要你解几年了。我也没有细看,楼主的主题主要讲不要误用而已,意思就是不要杀鸡用牛刀罢了
        ThoughtWorks:@Tim1020 :+1:
      • 狂奔的虾米:不知道该说什么了
      • 一粒吗:“不易应对数据过期” 为什么不易 是博主自身的技术问题吗?
      • 阿炎:在微服务架构中jwt还是很好用的,都是无状态的服务,session就显得不合适了。
      • a36c0317e2f6:不错,写的很好。
        bin_x:楼上说的对
        一粒吗:好个蛋蛋呐 一看就是菜鸡 误导你呢
      • 屠龙刀张无忌:无状态和有状态在安全和效率上面的考量 看业务场景了
        cc6a95b3fcb1:@屠龙刀张无忌 看了一些微服务设计都在网关上做jwt 我在想网关上做session也能达到目的啊,token放内存和放数据库有什么不一样
      • 梨花梧桐:想问你个问题,代码部分是如何带着格式粘贴到文章里的,我复制粘贴代码时,到文章里就没有格式了。
        梨花梧桐: @高尔呼司机 嗯嗯,我试试
        高尔呼司机:@梨花梧桐 用反引号包在代码段外面 就有格式了
      • 8f61c447c7c3:使用这个是为了系统微服务化,如果用session就是个大尾巴,干点啥都要考虑它,麻烦得很

      本文标题:讲真,别再使用JWT了!

      本文链接:https://www.haomeiwen.com/subject/btwbrxtx.html