当前位置: 首页 > 科技观察

你管这狗屎叫Token?

时间:2023-03-19 22:42:40 科技观察

上周,我们在团队中第一次使用jwt(JsonWebToken)token的no-session方式来验证用户的账号,发现网上很多文章介绍token都存在错误,所以我对cookie、session、token做了一个比较。相信大家看完后都会有所收获!Cookie1991HTTP0.9诞生。当时只是为了满足大家浏览网页文档的要求,所以只有GET请求,浏览完之后就可以了,两个连接之间是没有连接的,这就是为什么HTTP是无状态的,因为它没有这个需求在它诞生之初。但随着交互式Web的兴起(所谓交互式是指不仅可以浏览,还可以进行登录、发表评论、购物等用户操作),单纯的浏览Web已经不能满足人们的要求了。为了记录用户的购物车记录,需要有一种机制来记录每次连接的关系,这样我们就知道加入购物车的商品是属于谁的,于是Cookie就诞生了。Cookies,有时也以复数形式称为Cookies。类型为“小文本文件”,是一些网站为了识别用户身份和进行Session跟踪而存储在用户本地终端的数据(通常是加密的),由用户客户端临时或永久保存的信息电脑。工作机制如下。以加入购物车为例。每次浏览器请求后,服务器都会将商品id保存在cookie中,返回给客户端。客户端会将cookie保存在本地,下次保存上一次的本地。把cookie传给服务器就可以了,这样每一个cookie都存储了用户的产品id,购买记录就不会丢失。仔细观察上图,相信不难发现,随着购物车中的商品越来越多,每个请求的cookies也越来越大,这对每个请求来说都是一个不小的负担。我只想将产品添加到购物车。为什么要将历史产品记录返回给服务器?购物车信息其实已经在服务器中记录了。操作这样的浏览器岂不是多此一举?如何改进?Session仔细考虑,由于用户的购物车信息会保存在服务器中,所以只要在cookie中保存可以识别用户身份的信息,就可以知道是谁发起加入购物车的操作,这样在每次请求之后,cookie中只需要包含用户的身份信息,请求体中只需要包含本次加入购物车的商品id即可,大大减小了cookie的大小。我们把这种可以识别哪个请求是由哪个用户发起的机制称为Session(会话机制),生成的可以识别用户身份信息的字符串称为sessionId。其工作机制如下。首先,用户登录,服务器会为用户生成。一个session,给它分配一个唯一的sessionId,这个sessionId是和某个用户绑定的,也就是说根据这个sessionid(假设是abc)可以查询到是哪个用户,然后把这个sessionid传过去通过cookie给浏览器浏览器向浏览器添加购物车请求后,只需要在cookie中包含sessionId=abc键值对即可。服务器根据sessionId找到自己对应的用户后,将传入的产品id保存到服务器中对应的用户。可以看到,这样一来,就不用把购物车的所有商品id都传到cookie里了,大大减轻了请求的负担!另外,从上面不难看出,cookie保存在client,session保存在server,sessionId需要借助cookie传递才有意义。session的痛点看似通过cookie+session解决了问题,但是我们忽略了一个问题。上面的情况可以正常工作是因为我们假设服务器工作在单机上,但是在实际生产中,为了保证高可用,一般服务器至少需要两台机器,通过负载均衡的方式来确定哪台机器该请求应该命中。均衡如图:客户端请求后,由负载均衡器(如Nginx)决定命中哪台机器。假设登录请求命中了机器A,机器A生成session并将sessionId添加到cookie中返回给浏览器,那么问题来了:如果下次添加购物车时请求命中了B或者C,由于session是在A机器上生成的,此时B和C找不到session,那么就会出现购物车无法添加错误,必须重新登录,这时候怎么办。主要有三种方法:1.session复制A生成session复制到B,C,这样每台机器都有一个session,无论添加购物车的请求命中哪台机器,既然session都可以找到,不会有balance的问题(1)这种方法虽然可行,但是缺点也很明显:同一个session保存多份,节点少的话数据冗余还好,但是节点多的话,尤其像阿里,微信,由于上亿的DAU,可能需要部署几万台机器,所以额外节点的复制带来的性能消耗也会非常大。2、sessionsticking的方法是让每个client请求只命中固定的机器。比如浏览器登录请求命中机器A后,之后所有添加购物车的请求也会命中机器A。Nginx的sticky模块可以支持这种方式,比如通过ip或者cookie等进行粘贴。例如,sticking通过ip如下:upstreamtomcats{  ip_hash;  server10.1.1.107:88;  server10.1.1。132:80;}这样每次client请求到达Nginx后,只要其ip不变,根据iphash计算出的值就会发送到固定的机器上,就不会出现session不能的问题被发现。当然不难看出,这种方式的弊端也很明显。对应机器挂了怎么办?3、Session共享也是目前各大公司常用的一种方案。session保存在redis、memcached等中间件中,请求来的时候,每台机器都可以到这些中间件去获取session。缺点其实不难发现,就是每次请求都需要去redis中获取session,对性能有一点消耗。另外,为了保证redis的高可用,必须要集群化。当然对于大公司来说,基本都是部署Redis集群,所以这个方案可以说是大公司的首选。令牌:没有会话!通过上面的分析,我们知道可以通过在服务器端共享session来完成用户的身份识别,但是不难发现,这里也有一个小瑕疵:我要搭建一个redis集群给一个验证机制?确实redis被大厂广泛使用,但是对于小厂来说,他们的业务量可能达不到使用redis的水平,那么有没有其他不使用server来存储session的用户身份验证机制呢?这就是我们今天要介绍的主角:令牌。首先,请求者输入自己的用户名和密码,然后服务器据此生成令牌。客户端拿到token后,会保存在本地,然后向服务端请求时,可以将token附加到请求头中。相信看完上图你会发现两个问题。1、token只保存在浏览器中,不保存在服务器端。在这种情况下,我可以制作一个随机令牌并将其传递给服务器吗?答:服务器会有验证机制,检查token是否合法。2、为什么不像session那样根据sessionId查找userid,那怎么知道是哪个用户呢?答:token本身携带uid信息。第一个问题,如何验证token?我们可以使用HTTPS的签名机制来验证。首先看一下jwttoken的组成部分。可以看到token主要由三部分组成。Header:指定签名算法payload:可以指定用户id,过期时间等非敏感数据head+payload根据签名算法生成token。服务器收到浏览器发来的token时,会先取出token中的header+payload,根据key生成签名,然后与token中的签名进行比对。如果成功,则说明签名合法,即token合法。而且你会发现我们的userId是保存在payload中的,所以可以在拿到token后直接在payload中获取userid,避免了像session一样从redis中获取的开销画外音:header,payload其实就是这种形式base64存在,文中为了描述方便省略了这一步。你会发现这个方法真的很妙。只要服务端保证密钥不泄露,生成的token就是安全的,因为如果token是伪造的,就无法通过签名验证环节,可以判定token是非法的。可见,该方法有效避免了token必须存放在服务器的弊端,实现了分布式存储。但是需要注意的是,token一旦被服务器生成,就一直有效直到过期,token不能作废。除非在服务器上为令牌设置了黑名单,否则在验证令牌之前先通过黑名单。如果在黑名单里,token就失效了,但是一旦做了,就意味着黑名单必须保存在服务器上。这就回到session模式了,直接用session不是很好吗?所以一般的做法是,当客户端注销让token失效时,只需要在本地移除token,下次登录重新生成token即可。另外需要注意的是,token一般是放在header的Authorizationcustomheader中,而不是放在cookie中。这个主要是为了解决cookie不能跨域共享的问题(下面有详细介绍)。cookie和令牌的简要概述有哪些限制?1.Cookies不能跨站共享。在这种情况下,如果你想实现多应用(多系统)单点登录(SSO),那么使用cookie来做你需要的事情将是非常困难的(你需要使用更复杂的trick,如果你是有兴趣的可以看文末参考链接)画外音:所谓单点登录,就是在多个应用系统中,用户只需要登录一次,就可以访问所有相互信任的应用系统。但是如果使用token来实现SSO,就非常简单了。如下,只需要在header中的authorize字段(或其他自定义)添加token即可完成所有跨域站点的认证。2、移动端原生请求中没有cookie,sessionid依赖于cookie,所以sessionid不能通过cookie传递。如果使用了令牌,则它不存在,因为它与标头的授权一起传递。这个问题,换句话说,token天然支持移动平台,具有很好的扩展性。综上所述,token具有存储实现简单、扩展性好等特点。令牌的缺点是什么?有人问,既然代币这么好,为什么几乎所有的大公司都采用分享会的方式?可能很多人都是第一次听说token。token不好吗?Token有以下两点缺点:1.token太长。token是header和payload的编码样式,所以一般比sessionId长很多,很有可能超过cookie的大小限制(cookie一般都有大小限制,比如4kb)。中存储的信息越长,令牌本身就越长。这样的话,由于你每次请求都会带上token,所以对请求来说是一个很大的负担。2、不太安全网上很多文章都说token更安全,其实不然,细心的你可能注意到了,我们说token是保存在浏览器中的,然后问,它保存在浏览器的什么地方?由于在cookie中太长,可能会导致cookie超过限制,所以只好放到本地存储。这样会造成安全隐患,因为localstorage等本地存储是可以被JS直接读取的。另外,如上所述,token一旦生成,就不能作废,必须等到过期。在这种情况下,如果服务端检测到安全威胁,则无法使关联的令牌失效。因此token更适合一次性的命令认证,设置比较短的有效期。误解:Cookie的安全性低于令牌,例如CSRF攻击。首先,我们需要解释一下CSRF攻击是怎么回事。攻击者通过一些技术手段,诱使用户浏览服务器可以访问一个经过自身认证的网站,并进行一些操作(比如发送邮件、发送消息,甚至是转账、购买商品等财产操作)。由于浏览器已经通过身份验证(cookie中带有sessionId等身份验证信息),被访问的网站会认为是真实的用户操作而运行。例如,用户登录银行网站(假设为http://www.examplebank.com/,转账地址为http://www.examplebank.com/withdraw?amount=1000&transferTo=PayeeName),cookie中会包含登录用户的sessionid,攻击者可以在另一个网站放置如下代码那么如果正常用户点击上面由于对同一个域名的请求会自动带上cookie,而cookie中包含正常登录用户的sessionid,所以在服务器上进行类似上面的转账操作会成功,会造成很大的安全隐患.CSRF攻击的根本原因是对于同一个域名的每一次请求,都会自动带上它的cookie。这是浏览器的机制决定的,所以很多人认为cookies不安全。使用token确实避免了CSRF的问题,但是如前所述,由于token存储在本地存储,会被JS读取,从存储的角度来说是不安全的(其实正确的保护方式是对抗CSRF攻击就是使用CSRFtoken)所以不管是cookie还是token,从存储的角度来说都是不安全的,有暴露的风险。我们所说的安全,更多的是传输上的安全,可以使用HTTPS协议进行传输。在这种情况下,可以对请求头进行加密,从而保证传输中的安全性。事实上,我们将cookies与token进行比较是不合理的。一种是存储方式,另一种是认证方式。正确的比较应该是会话与令牌。综上所述,session和token本质上没有区别。它们都是对用户身份的认证机制,只是它们实现的验证机制不同(一个是保存在服务端,通过获取redis等中间件进行验证,一个是保存在客户端,通过签名验证),它是大多数场景使用session比较合理,但是单点登录和一次性命令认证使用token更合适,不同业务场景最好合理选择类型,以达到事半功倍的效果努力。本文转载自微信公众号“码海”,可通过以下二维码关注。转载本文请联系马海公众号。