目录
前言
HTTP 协议是无状态的,所谓无状态,指的是服务器无法确认多个 HTTP 请求之间是否存在关联,或者多个请求是否由同一个用户发出。这种简单的设计在静态类型页面(没有太多交互)的网站上工作良好,但是对于动态网站(根据不同的用户生成不同的页面)来说,无状态就很棘手了。
一些动态网站(比如:电商网站,博客平台等)需要标志出一个用户在访问一项业务(也许是添加多个商品到购物车,也许是浏览了几个页面需要记录历史浏览状态)的时候,需要借助 HTTP 的 cookie 和 session 技术来完成。
cookie
cookie 是一种在 web 服务器和 web 浏览器之间传递信息的技术,最早由美国的李维·蒙塔利(Lou Montulli)于 1994 年发明,通过在 HTTP response headers 中添加一个 set-cookie 字段来完成 web 服务器在 web 浏览器上保存一些信息,等下一次 web 浏览器发送请求时,就会携带 set-cookie 里的内容(放在 request header 的 cookie 字段)交给 web 服务器。
可以先把 cookie 简单的想象一个浏览器的存储功能,存储了针对于不同的网站的键值对,在 chrome/firefox/edge 等浏览器的 settings-隐私与安全中,可以找到所有网站的 cookies:
首先 web 浏览器可以通过这个字段,保存一些关于当前使用该浏览器的用户的个人信息,比如:用户设置偏好,用户名,购物车列表等等,这样用户打开特定网站时,web 浏览器就可以通过这些 cookie 内容,展示特定于当前用户的页面。
另外一方面,web 服务器为了从成千上万的 HTTP 请求中,辨认出哪几个 HTTP 请求属于某一个特定的用户或者浏览器,就可以在 cookie 里面塞一个 id(比如一个 UUID),这种模式就引申出了 cookie 的另外一个用法——维护 session。
session
cookie 本身并不是为了解决“在浏览器上存东西”而被发明,它的出现是为了解决 HTTP 协议无状态特性的问题。在网络的早期,当没有其他选择时,cookie 被用于数据存储目的(也就是上一小节提到的用法),现在建议使用新式存储 API,例如 Web Storage API(
localStorage
、
sessionStorage
)或者 IndexedDB。
如果说 cookie 是 HTTP 中一个实际的 header 字段,那么 session 更像是一个过程,它基于 cookie 实现:
- 用户使用账号名密码登录
- 登陆成功后,服务器为该用户生成一个 SessionId,并且在服务器内存中存储了这个 SessionId 对应的用户信息。
- 服务器通过 set-cookie 把 SessionId 告诉浏览器,浏览器存储到 cookie 中。
- 浏览器之后每一次访问该站点,会在 cookie 中携带这个 SessionId 向服务器表明自己的身份。
流程大致如下:
Session 存储策略
首先考虑一种最简单的场景和方式:用户使用用户名+密码的方式,登陆成功后,服务器通过数据库的 user 表确定了请求对应的用户信息(用户 id,购物车记录,登陆有效时间等等信息)。
服务器可以一股脑的把这些信息以明文的方式,set-cookie 交给浏览器,浏览器在后面每一个请求,都会带上这些信息。
但是生产环境一般不会这么做,因为这会把用户数据透明的交给浏览器,用户信息的安全性无法保证。
为了适应不同的应用场景,目前主流的做法有三种。
第一种
应用服务器在内存中维护一个键值对结构(Map),Key 是一个随即生成的 SessionId,Value 则是用户信息对象,服务器仅需要把 SessionId 交给服务器,这样用户信息存储在服务器端,安全性有了保障。
优点:
- 实现简单。
- 内存访问速度快。
缺点:
- 可扩展性差,一旦部署到分布式的环境下,服务器和服务器各自的内存无法共享。
- 服务器重启或者进程崩溃,导致用户 session 信息全部丢失。
- 用户数量极大时,会导致内存占用很高。
适用场景:适用于单服务器,场景简单,用户数量可控,session 丢失不敏感的应用。
第二种
和第一种类似,同样是把 SessionId 交给浏览器,不过用户信息是存储在数据库层面的(比如:Redis 或者 MySQL)
优点:
- 可扩展,所有应用服务器共享一个 session 存储。
- 持久性可以通过数据库来保证。
- 容量大,可以持久化磁盘,减轻内存压力。
缺点:
- 存在应用和数据库之间的网络开销。
- 复杂性更高。
- 要考虑在用户量极大时,数据库读写性能和网络性能。
适用场景:需要水平扩展到多个应用服务器,需要保障 session 高可用的应用。
第三种
第三种是一种自包含、无状态的 session,服务器不存储 session 信息,而是将用户信息序列化(如:JSON),再使用仅有服务器端保存的密钥,进行签名(防止客户端篡改),把最终生成的签名后的数据交给浏览器。
优点:
- 实现简单,有现成的方案——JWT,不需要在服务器端保存 session 数据。
- 可扩展,服务端不存储 session 数据,天然支持分布式。
缺点:
- 相比于 SessionId,这一种签名数据的占用空间更大,网络传输开销较大。
- CPU 压力高,每次请求响应涉及到 session 数据的解密和签名校验。
- 一旦签发,无法在短时间内撤销,因为服务器没有保存 session 数据,除非等到签发信息过期或者在服务器维护黑名单 session。
- 用户信息编码在签发的信息中,存在泄露信息的风险。
适用场景:无状态 API、微服务间认证、单点登录、客户端需要知道部分用户信息的场景(如显示用户名)。
Cookie 的生存时间
cookie 有两种控制生存时间的方式:
- 会话 Cookie
- 持久性 Cookie
会话 Cookie 仅在当前浏览器会话中有效,关闭浏览器就自动删除,这种 session cookie 常见于临时登陆,或者安全性较高的网站(比如:银行软件)。
持久性 Cookie 会持久化到本地磁盘,可以通过两种方式设置持久性 Cookie 的生存时间:
- Expires=$date
- Max-Age=$seconds
Expires 用于设置一个具体的过期时间,而 Max-Age 则可以设置从现在开始的存活秒数。
安全性
cookie 为了增强安全性,提供了以下字段:
- Path
- HttpOnly
- Secure
- SameSite
这一部分的内容可以参考: https://juejin.cn/post/6959830432519520292
参考
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies
- https://http.dev/cookies
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Overview#http_is_stateless_but_not_sessionless
- https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Reference/Headers/Set-Cookie
- https://zhuanlan.zhihu.com/p/688144675