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

两种给Http添加状态的方式都不是完美的

时间:2023-03-20 12:50:23 科技观察

我们知道http是无状态的,也就是说,上一次请求和下一次请求没有关系。但是,如果我们要实现应用程序的功能,很多时候需要有状态。例如,在登录并添加购物车后,应该认识到这是登录用户完成的。如何为http请求添加状态?这个问题有两种解决方案:session+cookie的方案存储在server端,token的方案存储在client端。但实际上,这两种方案都不是很好,也不完美。你为什么这么说?我们分别来看一下:服务端保存的session+cookie给http加上status,然后对每个请求进行标记,然后将这个标记对应的数据保存在服务端。这样每一个被标记的请求都能找到对应的数据,自然就可以存储登录、权限等状态。这个标记应该是自动带上的,所以http设计了cookie机制,每次请求都会带上保存在里面的数据。然后根据cookie中的标记,服务器端对应的数据称为session,这个标记就是session的id。如图所示,由于请求自动携带了cookie,所以在两次请求中都可以找到id为1的session,自然就知道当前登录的用户是谁,还可以存储其他状态数据.这是session+cookie给http添加state的方案。你认为这个解决方案有问题吗?有问题,而且问题相当多。其中一个最大的问题就是臭名昭著的CSRF(跨站请求伪造):CSRF在请求的时候会自动带上cookie,然后你登录一个网站,再访问另一个网站,万一里面有个按钮会request对于那个网站,cookie还是可以带上的。然后你就不需要再登录了。那么如果你点了这个按钮,做了一些危险的操作呢?很危险吗?而且一般这种利用CSRF漏洞的网站都会伪装的很好,让你很难看出破绽。这种网站称为钓鱼网站。为了解决这个问题,我们一般会验证referer,即是哪个网站发起的请求。如果发起请求的网站有误,则将其屏蔽。但这仍然不能完全解决问题。如果你用的浏览器也有问题,你能伪造referer吗?所以通常使用一个随机值来解决问题。每次登录并放入会话时都会随机生成一个值。后续请求需要包含这个值,否则认为是非法的。这个随机值叫做token,可以放在参数里,也可以放在header里,因为钓鱼网站拿不到这个随机值,即使有cookie,也没有通过服务器的验证。这是session+cookie方案的缺点,但是有解决办法。它还有其他缺点,比如分布式的时候:分布式session会话保存的是服务器端的状态数据,那么问题来了,如果有多个服务器怎么办?当并发量增大时,单台服务器根本无法承受。自然需要集群,需要多台服务器提供服务。而现在后端会将不同的功能拆分成不同的服务,也就是微服务架构,自然需要多台服务器。如何同步不同服务器之间的会话?登录后session保存在某台服务器,以后可能会访问其他服务器。此时那个服务器没有对应的session,无法完成对应的功能。.这个问题有两种解决方案:一种是sessionreplication,即session通过一种机制自动复制到每台机器上,每次修改都是同步的。这个有相应的框架,比如java的spring-session。每个服务器都复制了session,所以你访问任何一个服务器都可以找到对应的session。另一种方案是将session保存在redis中,这样各个服务器都可以在那里查看。只要一台服务器登录,其他服务器也可以查看session,不需要复制。幸运的是,当会话被分发时,这个问题有一个解决方案。但你认为这就是结局吗?session+cookie也有跨域问题:跨域cookie为了安全是受域限制的。在设置cookies的时候,会指定一个域,只有对这个域的请求才会带Serve这个cookie。并且还可以设置过期时间、路径等:如果是异域请求怎么办?即跨域时如何带cookies?a.guang.com和b.guang.com都可以,只要把Domain设置成顶级域名guang.com即可,二级域名和三级域名不一样也可以自动带上。但是如果顶级域名也不同,那就没办法了。这个只能在服务器端中转,把两个域名统一成同一个。以上不是ajax请求。Ajax请求还有一个额外的机制:Ajax请求在跨域时不会携带cookie,除非你手动将withCredentials设置为true。并且还要求后台代码设置相应的header:Access-Control-Allow-Origin:"currentdomainname";Access-Control-Allow-Credentials:true这里的alloworigin设置*不起作用,必须指定一个特定的域名来接收跨域cookie。这是session+cookie方式的第三个坑,还好有解决办法。总结一下:session+cookie给http添加status的解决方案就是在server端保存session数据,然后把id放到cookie中返回。cookie是自动携带的。每个请求都可以通过cookie中的id找到对应的session,从而实现对请求的识别。这个方案可以满足要求,但是存在CSRF、分布式会话、跨域等问题,但是也有解决方案。session+cookie的方案确实不完美,我们换一种方式看:tokensession+cookie在client端存储的方案是在server端保存state数据,然后将id保存在cookie中来实现。既然这样的方案问题那么多,我就反其道而行之,不把状态保存在服务器上,而是全部放在request里,不是放在cookie里,而是放在header里。这会解决一堆问题吗?tokenscheme往往以json格式保存,称为jsonwebtoken,简称JWT。让我们以此为例。JWT是存储在请求头中的一个字符串(比如header名称可以叫authorization),分为三部分:如图所示,JWT由三部分组成:header、payload、verifysignature:header部分存储当前的加密算法,payload部分是具体存储的数据,verifysignature部分是对header、payload和salt进行一次加密后生成的。(salt,salt,是任意字符串,增加随机性)。这三部分会分别做Base64,然后连在一起就是JWTheader,放在一个header里比如authorization:authorization:barerxxxxx.xxxxx.xxxx请求这个header时,服务端可以解析出对应的header,payload、verifysignature是header的三部分,然后根据header中的算法对header、payload、salt进行一次加密。如果结果与验证签名相同,则令牌被接受。把状态数据保存在payload部分,这样就实现了有状态的http:而且这种方式不存在session+cookie的问题。不信我们单独来看一下:CSRF:因为不是通过自动带的cookie连接到服务端的session保存的状态,所以不存在CSRF问题,而且它不能被cookie攻击。分布式会话:因为状态没有保存在服务器端,所以访问哪个服务器并不重要,只要能从token中解析出状态数据即可。跨域:因为不是cookie集,自然没有跨域限制,手动带上JWT头即可。看来这个方法很完美?其实不然。JWT存在JWT的问题:security因为JWT将数据直接放在Base64之后的header中,所以其他人可以很容易的从中获取状态数据,比如用户名等,敏感信息也可以基于这个JWT伪造请求.所以JWT要和https一起使用,这样别人就获取不到header了。PerformanceJWT将状态数据保存在header中,每次请求都会带上。与只保存一个id的cookie相比,请求的内容会增加,性能会变差。所以不要在JWT中保存太多数据。JWTsession是不可能失效的,因为它存在于服务端,所以我们可以随时失效,但是JWT不行,因为它是保存在客户端的,那么我们就不能手动失效。所以JWT的过期时间不宜设置太长。因此,JWT方案虽然解决了很多session+cookie的问题,但并不完美。总结:JWT方案是在header中保存状态数据,每次请求都需要手动携带。session+cookie方案不存在CSRF、分布式、跨域问题,但也存在安全、性能、不可控等问题。.说了这么多,还是把代码写下来比较安心:Nest.js实现两种方案下面我们就用Nest.js来实现两种方案吧,不能纸上谈兵。首先使用@nest/cli快速创建一个Nest.js项目。npxnestnewstatus会生成module、controller、service的基础代码:我们先实现session+cookie的方法:session+cookieNest。express解决方案:安装express-session及其ts类型定义:npminstallexpress-session@types/express-session然后在入口模块中启用:指定加密cookie的密码即可。然后session对象就可以注入到controller中了:我在session中放了一个count变量,每次访问加一,然后body返回计数。这样就可以判断http请求是否有状态。我们来测试一下:我们可以看到每次请求返回的数据都是不同的,返回的一个cookie是connect.sid,也就是session对应的id。因为在请求的时候会自动带上cookie,可以实现对请求的识别,给http请求加上状态。session+cookie的方式使用起来还是很简单的。再来看jwt的方法:jwtjwt需要导入@nestjs/jwt包,然后在入口Module中导入JwtModule:导入时指定密码,用于添加jwt中的salt,也可以指定token过期时间时间。因为我们引入了JwtModule,所以我们可以在Controller中实现依赖注入:声明对JwtService的依赖,Nest.js会自动注入对应的对象。然后定义一个controller方法,通过Resonse对象设置authorizationheader:使用jwtService生成token,记录count,然后放到header中返回,同样放到body中。下面这个请求是把header取出来,拿到里面的数据,+1之后再放回去:这样也实现了需要给http加上status,但是把数据保存在header里了。我们通过postman来测试一下:第一个请求会返回一个authorizationheader,body为1:手动在requestheader中添加这个header,再次请求:body变成2,也返回一个新的authorizationheader。将这个新的授权放入请求头中,再次请求:body变为3,同时返回一个新的授权头。有同学问,如果我不使用新的header,而是使用之前的header:那会报错:jwtcanonlybeusedoncegenerated,这个one-time也是它的一个特点。这样我们使用Nest.js分别实现session+cookie和jwt来保存http状态。代码上传到github:https://github.com/QuarkGluonPlasma/nestjs-exercize。总结一下,http是无状态的,就是请求和请求之间是没有关系的,但是我们很多功能的实现都需要保存状态。http添加状态有两种方式:session+cookie:将状态数据保存到服务器,将sessionid放入cookie中返回,这样每次请求都会带上一个cookie,通过它可以找到对应的session身份证。该解决方案存在CSRF、分布式会话和跨域问题。jwt:将token中的状态以json格式保存,放在header中。需要手动携带。cookie+session没有问题,但是也存在安全、性能、不可控、用完就失效等问题。这些解决方案都不是完美的,但有解决这些问题的方法。在软件领域很多情况都是这样。某种解决方案解决了一些问题,但也相应地带来了一些新的问题。没有灵丹妙药,但我们还是要熟悉它们的特性,根据不同的需求灵活选择。