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