一、Http 协议无状态的含义
1.1 有状态协议
常见的许多七层协议实际上是有状态的,例如 SMTP 协议,它的第一条消息必须是 HELO,用来握手,在 HELO 发送之前其他任何命令都是不能发送的;接下来一般要进行 AUTH 阶段,用来验证用户名和密码;接下来可以发送邮件数据;最后,通过 QUIT 命令退出。可以看到,在整个传输层上,通信的双方是必须要时刻记住当前连接的状态的,因为不同的状态下能接受的命令是不同的;另外,之前的命令传输的某些数据也必须要记住,可能会对后面的命令产生影响。这种就叫做有状态的协议。
1.2 为什么说 http 协议是无状态协议
相反,为什么说 HTTP 是无状态的协议呢?因为它的每个请求都是完全独立的,每个请求包含了处理这个请求所需的完整的数据,发送请求不涉及到状态变更。即使在 HTTP/1.1 上,同一个连接允许传输多个 HTTP 请求的情况下,如果第一个请求出错了,后面的请求一般也能够继续处理(当然,如果导致协议解析失败、消息分片错误之类的自然是要除外的)。可以看出,这种协议的结构是要比有状态的协议更简单的,一般来说实现起来也更简单,不需要使用状态机,一个循环就行了。
1.3 为什么不改进 http 协议使之有状态
最初的 http 协议只是用来浏览静态文件的,无状态协议已经足够,这样实现的负担也很轻(相对来说,实现有状态的代价是很高的,要维护状态,根据状态来操作。)。随着 web 的发展,它需要变得有状态,但是不是就要修改 http 协议使之有状态呢?是不需要的。因为我们经常长时间逗留在某一个网页,然后才进入到另一个网页,如果在这两个页面之间维持状态,代价是很高的。其次,历史让 http 无状态,但是现在对 http 提出了新的要求,按照软件领域的通常做法是,保留历史经验,在 http 协议上再加上一层实现我们的目的("再加上一层,你可以做任何事")。所以引入了其他机制来实现这种有状态的连接。
1.4 无状态协议的优缺点
和许多人想象的不同,会话(Session)支持其实并不是一个缺点,反而是无状态协议的优点,因为对于有状态协议来说,如果将会话状态与连接绑定在一起,那么如果连接意外断开,整个会话就会丢失,重新连接之后一般需要从头开始(当然这也可以通过吸收无状态协议的某些特点进行改进);而 HTTP 这样的无状态协议,使用元数据(如 Cookies 头)来维护会话,使得会话与连接本身独立起来,这样即使连接断开了,会话状态也不会受到严重伤害,保持会话也不需要保持连接本身。另外,无状态的优点还在于对中间件友好,中间件无需完全理解通信双方的交互过程,只需要能正确分片消息即可,而且中间件可以很方便地将消息在不同的连接上传输而不影响正确性,这就方便了负载均衡等组件的设计。
无状态协议的主要缺点在于,单个请求需要的所有信息都必须要包含在请求中一次发送到服务端,这导致单个消息的结构需要比较复杂,必须能够支持大量元数据,因此 HTTP 消息的解析要比其他许多协议都要复杂得多。同时,这也导致了相同的数据在多个请求上往往需要反复传输,例如同一个连接上的每个请求都需要传输 Host、Authentication、Cookies、Server 等往往是完全重复的元数据,一定程度上降低了协议的效率。1.5 HTTP 协议是无状态协议,这句话本身到底对不对?
实际上,并不全对。HTTP/1.1 中有一个 Expect: 100-Continue 的功能,它是这么工作的:
- 在发送大量数据的时候,考虑到服务端有可能直接拒收数据,客户端发出请求头并附带 Expect: 100-Continue 的 HTTP 头,不发送请求体,先等待服务器响应
- 服务器收到 Expect: 100-Continue 的请求,如果允许上传,发送 100 Continue 的 HTTP 响应(同一个请求可以有任意个 1xx 的响应,均不是最后的 Response,只起到提示性作用);如果不允许,例如不允许上传数据,或者数据大小超出限制,直接返回 4xx/5xx 的错误
- 客户端收到 100 Continue 的响应之后,继续上传数据
可以看出,这实际上很明显是一个有状态协议的套路,它需要先进行一次握手,然后再真正发送数据。不过,HTTP 协议也规定,如果服务端不进行 100 Continue 的响应,建议客户端在等待较短的时间之后仍然上传数据,以达成与不支持 Expect: 100-Continue 功能的服务器的兼容,这样可以算是"能有状态则有状态,否则回到无状态的路上",这样说 HTTP 1.x 是无状态的协议也是没错的。
至于 HTTP/2,它应该算是一个有状态的协议了(有握手和 GOAWAY 消息,有类似于 TCP 的流控),所以以后说"HTTP 是无状态的协议"就不太对了,最好说"HTTP 1.x 是无状态的协议"
二、如何解决无状态问题
HTTP 协议是无状态的,无状态意味着,服务器无法给不同的客户端响应不同的信息。这样一些交互业务就无法支撑了。Cookie 应运而生。
2.1 Cookie
cookie 的传递会经过下边这 4 步:
Client 发送 HTTP 请求给 Server
Server 响应,并附带 Set-Cookie 的头部信息
Client 保存 Cookie,之后请求 Server 会附带 Cookie 的头部信息
Server 从 Cookie 知道 Client 是谁了,返回相应的响应
Cookie 的英文翻译是甜品,使用 Cookie 可以自动填写用户名、记住密码等,是给用户的一点甜头。
Server 拿到 Cookie 后,通过什么信息才能判断是哪个 Client 呢?服务器的 SessionID。
2.2 Session
如果把用户名、密码等重要隐私都存到客户端的 Cookie 中,还是有泄密风险。为了更安全,把机密信息保存到服务器上,这就是 Session。Session 是服务器上维护的客户档案,可以理解为服务器端数据库中有一张 user 表,里面存放了客户端的用户信息。SessionID 就是这张表的主键 ID。
Session 信息存到服务器,必然占用内存。用户多了以后,开销必然增大。为了提高效率,需要做分布式,做负载均衡。因为认证的信息保存在内存中,用户访问哪台服务器,下次还得访问相同这台服务器才能拿到授权信息,这就限制了负载均衡的能力。而且 SeesionID 存在 Cookie,还是有暴露的风险,比如 CSRF(Cross-Site Request Forgery,跨站请求伪造)。
如何解决这些问题呢?基于 Token 令牌鉴权。
2.3 Token
首先,Token 不需要再存储用户信息,节约了内存。其次,由于不存储信息,客户端访问不同的服务器也能进行鉴权,增强了扩展能力。然后,Token 可以采用不同的加密方式进行签名,提高了安全性。
Token 就是一段字符串,Token 传递的过程跟 Cookie 类似,只是传递对象变成了 Token。用户使用用户名、密码请求服务器后,服务器就生成 Token,在响应中返给客户端,客户端再次请求时附带上 Token,服务器就用这个 Token 进行认证鉴权。
Token 虽然很好的解决了 Session 的问题,但仍然不够完美。服务器在认证 Token 的时候,仍然需要去数据库查询认证信息做校验。为了不查库,直接认证,JWT 出现了。
2.4 JWT
JWT 的英文全称是 JSON Web Token。JWT 把所有信息都存在自己身上了,包括用户名密码、加密信息等,且以 JSON 对象存储的。
JWT 长相是 xxxxx.yyyyy.zzzzz,极具艺术感。包括三部分内容
- Header 包括 token 类型和加密算法(HMAC SHA256 RSA)
json{ "alg": "HS256", "typ": "JWT"}
- Payload
传入内容
json{ "sub": "1234567890", "name": "John Doe", "admin": true}
- Signature
签名,把 header 和 payload 用 base64 编码后"."拼接,加盐 secret(服务器私钥)。
scssHMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret);
最终的 token 就是这样一个字符串
inieyJhbGciOiJIUzI1NiJ9 .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ .yKOB4jkGWu7twu8Ts9zju01E10_CPedLJkoJFCan5J4;
给 Token 穿个外套
这就是我们在请求 Header 里面看到的内容格式了。