HTTP 是没有状态的Web服务器,什么是无状态呢?
就像夏洛特烦恼中的台词一样,一次对话完成后下一次对话完全不知道上一次对话发生了什么。
如果说Web服务器只是用来管理静态文件还好说,对方是谁其实不重要,把文件从磁盘中读取出来发出去即可,客户端与服务器连接就会关闭,再次交换数据需要建立新的连接。这时候服务器无法从连接上跟踪会话。。但是随着网络的不断发展,比如电商中的购物车只有记住了用户的身份才能够执行接下来的一系列动作。所以此时就需要我们无状态的服务器记住一些事情;
会话,简单说是用户登录网站后的一系列动作,比如浏览商品添加到购物车并购买。Session 跟踪是 Web 程序里常用的技术,用来跟踪用户的整个会话;常用的会话跟踪技术是Cookie与Session。
Cookie 通过在客户端记录信息确定用户身份,
Session 通过在服务器端记录信息确定用户身份。
马上就面临一个问题,那就是如果管理会话,必须记住哪些人登录系统, 哪些人往自己的购物车中放商品, 也就是说我必须把每个人区分开,这也是一个不小的挑战,因为 HTTP 请求是无状态的,所以想出的办法就是给大家发一个会话标识 (session id) , 说白了就是一个随机的字串,每个人收到的都不一样, 每次大家向我发起HTTP请求的时候,把这个字符串给一并捎过来, 这样我就能区分开谁是谁了呀;
这样客户端是嗨皮了,但是服务器就不嗨皮了,每个人只需要保存自己的 session id,而服务器要保存所有人的session id ! 如果访问服务器多了, 就得由成千上万,甚至几十万个。
这对服务器说是一个巨大的开销 , 严重的限制了服务器扩展能力,
比如说我用两个机器组成了一个集群, 小F通过机器A登录了系统, 那 session id 会保存在机器A上, 假设小F的下一次请求被转发到机器B怎么办? 机器B可没有小F的 session id啊。
有时候可以用一点小伎俩: session sticky , 就是让小F的请求一直粘连在机器A上, 但是这也不管用, 要是机器A挂掉了, 还得转到机器B去。
那只好做session 的复制了, 把 session id 在两个机器之间搬来搬去, 快累死了。
后来有个叫Memcached的人想到个办法: 那不如把 session id 集中存储到一个地方, 所有的机器都来访问这个地方的数据, 这样一来,就不用复制了, 但是也增加了单点失败的可能性, 要是那个负责 session 的机器挂了, 所有人都得重新登录一遍, 估计得被人骂死。
也尝试把这个单点的机器也搞出集群,增加可靠性, 但不管如何, 这小小的 session 对我来说是一个沉重的负担
于是有人就一直在思考, 我为什么要保存这可恶的session呢, 只让每个客户端去保存该多好?
可是如果不保存这些 session id, 怎么验证客户端发给我的session id 的确是我生成的呢? 如果不去验证,我们都不知道他们是不是合法登录的用户, 那些不怀好意的家伙们就可以伪造 session id , 为所欲为了。
嗯,对了,关键点就是验证 !
比如说, 小F已经登录了系统, 我给他发一个令牌(token), 里边包含了小F的 user id, 下一次小F 再次通过 Http 请求访问我的时候, 把这个 token 通过Http header 带过来不就可以了。
不过这和session id没有本质区别啊, 任何人都可以可以伪造, 所以我得想点儿办法, 让别人伪造不了。
那就对数据做一个签名吧, 比如说我用 HMAC-SHA256 算法,加上一个只有我才知道的密钥, 对数据做一个签名, 把这个签名和数据一起作为token , 由于密钥别人不知道, 就无法伪造token了。
这个 token 我不保存, 当小F把这个 token 给我发过来的时候,我再用同样的 HMAC-SHA256 算法和同样的密钥,对数据再计算一次签名, 和token 中的签名做个比较, 如果相同, 我就知道小F已经登录过了,并且可以直接取到小F的 user id, 如果不相同, 数据部分肯定被人篡改过, 我就告诉发送者: 对不起,你没有认证;
Token 中的数据都是用明文保存的,虽然我会用Base64做下编码,但请注意那不是加密, 还是可以被别人看到的哦, 所以我不能在其中保存像密码这样的敏感信息。
当然, 如果一个人的 token 被别人偷走了, 那我也没办法, 我也会认为小偷就是合法用户, 这其实和一个人的 session id 被别人偷走是一样的。
这样一来, 我就可以不保存 session id 了, 我只是生成token , 然后验证token , 我用我的CPU来计算时间获取了我的 session 存储空间 !
解除了session id这个负担, 可以说是无事一身轻, 我的集群现在可以轻松地做水平扩展, 用户访问量增大, 直接加机器就行。 这种无状态的感觉实在是太好了!
前面说过 http 是无状态的协议,浏览器和服务器不可能凭协议的实现就可以辨别请求的上下文的。于是 cookie 登场,既然协议本身不能分清链接,那我就在请求头部手动带些上下文信息吧,好像还是有些虚:
我们去旅游的时候,到了景区可能我们需要放行李,被大包小包压着总感觉不爽。在存放行李后,服务员会给你一个牌子,上面写着你的行李放在哪个格子,离开时,你就能凭这个牌子和上面的数字对应起来就成功取出行李。
cookie 这个牌子做的正是这么一件事,旅客就像客户端,寄存处就像服务器,凭着写着数字的牌子,寄存处(服务器)就能分辨出不同旅客(客户端),就这么简单。
cookie 是一个非常具体的东西,指的就是浏览器里面能永久存储的一种数据,仅仅是浏览器实现的一种数据存放功能。
cookie由服务器生成,发送给浏览器,浏览器把 cookie 以 kv 形式保存到某个目录下的文本文件内,下一次请求同一网站时会把该 cookie 发送给服务器。由于cookie 是存在客户端上的,所以浏览器加入了一些限制确保cookie不会被恶意使用,同时不会占据太多磁盘空间,所以每个域的 cookie 数量是有限的。
接下来我们用代码演示一下服务器是如何生成,可以自己搭建一个后台服务器,这里我用的是 SpringBoot 搭建的,并且写入 SpringMVC 的代码:
@RequestMapping("/testCookies")
public String cookies(HttpServletResponse response){
response.addCookie(new Cookie("testUser","xxxx"));
return "cookies";
}
项目启动以后我们输入路径http://localhost:8005/testCookies,然后查看发的请求。可以看到下面那张图使我们首次访问服务器时发送的请求,可以看到服务器返回的响应中有 Set-Cookie 字段。而里面的 key=value 正是我们服务器中设置的值。
接下来我们再次刷新这个页面可以看到在请求体中已经设置好了 Cookie 字段&