你登录了吗?- 聊一聊 cookie , session , token
🌀前置小知识
🌈HTTP 是无状态的
HTTP (Hyper Text Transfer Protocol)是一种无状态的网络数据交换协议!专业点叫 《超文本传输协议》
它就像一条鱼 🐟 , 仅在一次连接时 ,有记忆 , 能够对当前不同的连接进行区分!
但连接断开后,再次连接,它就会忘记你是谁了!相当于重新认识!
之所以如此是因为在 http 设计之初 ,就没有这个需求和必要!当时的功能很简单,就是一台电脑使用 http 协议 ,与另一台电脑进行连接后 ,可以访问到另一台电脑里的 html 文件!
因此, 在当时, 鱼就够用了!
然而 ,这一切仅仅是开始!在之后的发展中 , 仅仅预览或下载 html 文件是远远满足不了人们的需求了!因此 http 在之后的发展中 ,http 可以进行其他数据的交流 ,比如图片 , 视频 ,脚本文件等等!
但是 ,由于 http 的特性 , 始终是无法解决无状态的问题的!
❓为什么需要状态?
如今的网站 ,大多数是需要用户注册的 ,也就是它需要区分不同用户!假设一种情况 ,你登录了某个网站 , 但是这个登录状态仅在你发出的登录请求这个过程中有效 ,那么在需要登录后才能进行的请求 ,你可能需要每次都要携带上账号密码!
对此们可以在登录后返回用户一个登录的凭证 ,比如 JS 存放在全局变量中 ,每次请求带上即可!然而这还有一个问题 ,就是如果你关闭了游览器或者关闭标签页,或者刷新一下 ,数据就丢失了,你又需要重新登录!
也就是 , 不同的连接之间 ,没有联系 ,没有状态 ,服务器需要游览器在这些连接之间携带身份信息,或者身份凭证 ,从而辨别身份!
🍪Cookie
Cookie 是一个保存在客户机中的简单的文本文件 。它的出现解决了 http 无状态特性所带来的问题!
Cookie 所记录的数据与文档关联,可以保存用户在该文档上的一些操作记录和数据!比如登录状态!比如用户的身份信息等!
🥔Cookie 的组成
Cookie 本质来说,是文本数据。形式上是 键值对 。 键值对之间通过 ; 分隔!
通过 document.cookie 可以访问和设置 cookie !
比如
document.cookie='h_key=h_value'
在游览器的效果就是:
从上图可以发现 ,Cookie 不仅仅只有一个 ,单个 Cookie 除了名称和值 ,还有其他属性!比如 Domain , Path , Expires 等等!
比如 :
h_key=h_value; expires=Mon, 22-Jan-07 07:10:24 GMT; domain=.abc``.com;path=/;Secure;HttpOnly
但实际发送的只有 cookie 的名/值对。也就是名称和值,其他是告诉游览器一些 cookie 的信息的,如该不该发送,什么时候删除等
cookie 在浏览器中是由以下属性构成的:
name (名称)
唯一标识 cookie 的名称。cookie 名不区分大小写,但实践时最好区别大小写,因为可能有的服务器软件区别。cookie 名必须经过 URL 编码。
它可以用于表示 cookie 的作用和含义 ,比如记录账号的 ,就可以叫 username 。
value (值)
存储在 cookie 里的字符串值。于 name 组成键值对 ,就是一个 cookie 的基本信息。 这个值也必须经过 URL 编码。
Domain (域)
cookie 的有效域。也就是什么域可以使用这个 cookie 。 发送到这个域的所有请求默认都会包含对应的 cookie。这个值可能包含子域,也可能不包含。如果不明确设置,默认为设置使得域名
Path (路径)
请求 URL 中包含这个路径才会把 cookie 发送到服务器。也就是指定的路径才会在发送请求时把 cookie 包含进去。 默认值是设置 cookie 使得路径
Expires (过期时间)
表示何时删除 cookie 的 时间戳(即什么时间之后就删除)。
默认情况下(没有设置 Expires),浏览器会话结束后(关闭游览器)会删除该 cookie。
如果设置了时间,值是 GMT 格式(Wdy, DD-Mon-YYYY HH:MM:SS GMT),用于指定删除 cookie 的具体时间。在有效时间内即使关闭游览器也不会被删除。当过期后,游览器会删除该 cookie
Secure (安全标志)
设置之后,只在使用 SSL 安全连接 (https 请求)的情况下才会把 cookie 发送到服务器 。 使用安全标志只要在 cookie 中增加 Secure 即可。
比如 document.cookie=k=v;Secure
HttpOnly
只能通过后台设置 ,设置后, JS 无法对 cookie 进行读取和修改 。 仅游览器可以对其操作。
🔧Cookie 的设置
document.cookie
可以设置和访问非 httponly 的 cookie 。
比如:
document.cookie = `h_key=h_value; expires=${new Date()};domain=127.0.0.1;path=/;secure;
设置非存在的过期的 cookie , 不会被设置上去
每一次给执行 document.cookie ,都会生成新的 cookie (非覆盖,非删除操作时)。
比如:
document.cookie='h_key=h_value'
document.cookie='h_key1=h_value1'
document.cookie='h_key2=h_value2'
不同的 cookie 通过 name 标识 ,不同的 name , 就会生成新的 cookie !
当两次以上的设置 同 name 的 cookie 时 ,会覆盖之前的 cookie 。
🪚Cookie 的删除
不同于 Storage 有 remoteItem 接口可以指定删除某个数据 。 Cookie 的删除,必须通过覆盖!
也就是重新设置 document.cookie 。
Cookie 是否删除取决于 expires , 也就是过期时间!当设置一个过去的时间, 0 或者负数时,游览器在发送请求时,就不会带上改 cookie ,并且会删除改 cookie !
删除 cookie 的几种情况
- 设置成过去的时间
设置成过去的时间,比如 new Date(Date.now() - 8 * 3600 * 1000)
设置成一个过期的时间 , cookie 马上删除!
需要注意一点 ,游览器看是否过期是根据 GTM 时间 ,实际来说 ,通过 new Date()
获取的时间是 UTC 时间 。与 GTM 时间仅有一秒内的差距 ,不过问题在于 , new Date
默认获取到的时间是在 GMT 的时间的基础上加上区时差。也就是 ,我们获取到的是北京时间(东八区) ,比 GMT 快 8个小时 。
-
设置成 0 或者 负数
🦖Session
📞什么是 session
Session 也叫做会话!是服务器为了保存用户状态而创建的一个特殊的对象。
用于存储服务端与客户端之间的会话信息等。可以理解为存储在后台的 cookie 。
当客户端于服务端第一次建立连接后 ,服务端会创建一个 session 对象 。此对象有一个唯一的 id 。 一般是 sessionId 。 这个 sessionId 会以 cookie 的形式发送给客户端 ,客户端保存这个 id , 在之后的通信中带上这个 id , 服务器通过 Id 就可以找到对应的 session , 就可以对客户端进行身份识别并获取一些会话记录!
为什么说 , session 就像保存在 后台的 cookie 呢! 因为他们的功能其实是一样的 。目的就是记录会话信息 。比如 cookie 可以保存用户名 ,用户 id , 用户游览记录等等! Session 的目的也是如此。
不过 session 是保存的服务端的 ,因此要区分不同游览器对应不同的 session , 需要给游览器一个 sessionId , sessionId 是以 cookie 的形式传递给游览器的!
Session 基本上依赖于 cookie , 但也可以不依赖于 cookie 。 比如 URL重写 ,即将 cookie 放在 URL中 ,不过这相比于 cookie 更麻烦且更不安全了!
❓为什么使用 session
如此看来, 倒不如直接使用 cookie ! 那为什么还需要 session 呢?
之所以还需要 session , 主要有以下这些原因:
- Cookie 大小有限制
Cookie 被限制在 4k 以内 ,单个 cookie 的大小也有限制 ,数量也有限制 。因此无法通过 cookie 保存过多的信息!
- Cookie 本身不安全
Cookie 容易被劫持和伪造。如果在 cookie 中保存了用户的一些信息,那么很容易被劫持后盗取信息 。另外伪造一个 cookie 也相对容易,那么当安全性较低时 ,用户身份就很可能被盗用!或者伪造一个身份向服务器发送请求。
- Cookie 数据会加大传输压力
对应网站的所有未过期的 cookie 在每次请求时都会带上,如果在 cookie 中存大量数据,但是其实用到的 ,只有用于身份验证的 cookie , 那么会导致传输压力变大!
而Session 是为什么没有这些问题的呢?
- Session 保存在后台 ,本身就没有大小的限制。因此你可以存放很多信息
- Session 只需要给游览器一个 sessionId , 这个 sessionId 可以加密 ,不易伪造和破译。相对安全很多。并且将信息存放在后台 ,即使 cookie 被他人获取 ,也难以通过 cookie 就获取用户信息。
-
因为数据都存储在后台 ,那么就不需要传输来传输去 ,那么就减少了请求中请求头的大小。
🧨Sesssion 的问题
当一个服务的业务越来越大 ,访问量越来越多 。为了持续稳定的提供服务 ,服务器可以考虑从垂直扩展和水平扩展两个方面扩展负载能力。
- 垂直扩展
垂直扩展即加强服务器本身的负载能力 ,提高加强硬件处理能力 。比如 CUP 处理能力 , 储存大小等等 , 甚至从软件上也可以择优使用!但这始终有瓶颈!
- 水平扩展
水平扩展即从一台服务器扩展到多台服务器去提高服务!从而将负载分担到不同服务器中 。
- 水平扩展有两种方法:
-
- 分布式服务器:一个服务分成多个子服务,不同服务器服务不同需求
- 集群服务器 (负载均衡): 不同服务器提供相同的服务,服务相同的需求
(简而言之,分布式就是不同人干不同活,你负责生产,我负责销售。集群就是自己干自己的,即生产,也销售,但是多个人一起干)
然而 ,无论是分布式还是集群 ,如果使用了 session 就会有一个问题 ,就是 session 可能在服务器 A 中创建 , 但是下一个请求,可能访问了服务器 B 。 但是服务器 B没有这个 session , 也就是对服务器 B 来说 ,此时的请求,它是不认识的!
而使用 cookie 记录会话信息就不会有这个问题 。因为 cookie 保存在游览器 ,无论访问服务器A 还是 服务器 B 都会带上!
🪠问题解决
那如何解决水平扩展后 session 的问题呢?
可以将解决方法分为三类:
- Session 保持
- Session 复制
-
Session 共享
Session 保持
仅适用于集群服务器 ,即负载均衡 。 其原理就是将同一个客户端的请求发到同一个服务器上 。使客户端始终与同一个服务器进行会话。
这个方法实现简单 ,只需要设置适当的负载均衡算法即可!
不过问题在于可能导致一个服务器接受过多的请求(这与 负载均衡 算法有关)。如果该服务器宕机了, 会话就会丢失!
Session 复制
Session 复制是在一个服务器创建了 session 后 ,把 session 复制到其他服务器 ,并且在之后保持 session 的同步!那么无论请求发到哪个服务器 ,都能保持会话!
Session 共享
Session 共享是将 session 集中在一个数据库中保存 。不同的服务器接受到请求后 ,通过 sessionId 向服务器获取 session 即可!
🗝️Token
🔑什么是 Token
Token 叫做令牌 。是用户在服务器登录后 ,服务器颁发给服务器一个令牌 。这个令牌保存了用户的一些基本信息 ,比如用户名称 ,用户权限等,也需要保存一些特有的数据 ,比如签名 (sign),过期时间等!
Token 都会经过加密后才发给游览器 ,这在一定程度上保证了数据不会轻易泄露!
应用在接收到 token 后缓存起来,在之后的请求时带上 , 服务器接收到 token 后解密 token , 从而可以识别用户身份!
❓为什么使用 Token
为什么使用 token , 主要有以下几点原因:
- Token 完全由应用管理,所以它可以避开同源策略
- Token 可以避免 CSRF 攻击
-
Token 可以是无状态的,可以在多个服务间共享
这些优点怎么理解和体现,就需要对比 cookie 和 session 的缺点了:
- Cookie 不安全
Cookie 容易被跨站攻击。也就是 CSRF 攻击 。因为游览器在请求时会默认带上 Cookie 。而 Token 是完全由应用管理 ,也就是带不带 ,由我们决定!
- Cookie 有跨域限制
Cookie 的 domain 和 path 属性可以限制 cookie 应该在哪些请求中带上 cookie 。但如果有跨域的请求 , 我们希望带上 cookie , 但游览器不允许!而 token 由我们控制,就可以带上 ,不受同源策略限制!
- 大量的 Session 会给造成服务器巨大压力
因为 session 是保存在服务器端的,如果访问量过多,就会给服务器造成过大的压力!使得响应变慢。而 token 是保存在客户端的 ,缓解了服务器的压力
- Session 不太适合分布式和集群
Token 可以是无状态的(服务器不知道它是否已经失效) 。也就是用户登录后把用户信息保存在 Token 中 , 这样子客户端请求其他服务器时只需要带上 token , 服务器解析 token ,就能获取身份 。
🪧JWT
JWT (Json Web Token) 是 一种轻量级的认证规范 ,本质是一种 token !
Token 分为有状态和无状态!
JWT 就是一种状态的 token ! 所谓无状态是指在 token 的过期时间到达之前,服务器无法主动让其失效。同时,服务器也不知道它是否应该失效!
相当于你给别人你家的密码 ,你一个星期换一次密码 ,但一个星期内,你不知道密码是否已经泄露。因此这个时候 ,只要有密码就能进你家 ,被偷了东西,你还不知道!
因此对于 JWT 的使用需要十分谨慎!一般很少使用!
不过 JWT 也有它的特点,就是可以去中心化 。也就是在集群和分布式中 ,一个 token , 可以到处访问!还有实现单点登录!也超级的方便简单!
🎐有状态 Token
有状态 Token 其实也是无状态 Token, 它的有状态是因为后台也保存了 Token 。 这其实就和 session 差不多了 。只不过造成的负担会小一些!
之所以使用有状态的 Token ,是因为无状态的 Token (比如 JWT ) 有一定的风险 ,如果 token 泄露,那么服务器没办法知道这个 token 是不是被盗用的。也没有办法让这个 token 失效!
而有状态 Token ,可以在用户登出,或者服务器在发现该 Token 的请求有一定的问题后 ,让该 Token 失效。从而降低风险!
让 Token 失效的方法,一般有两种:
- 白名单
白名单是将生成有效的 token 缓存起来 ,在验证 Token 时 ,先验证是否有该 Token ,没有即失效 ,有就解密该 Token ,获取用户身份信息 。 如果需要一个 Token 失效 ,在白名单中删除该 Token 即可!
- 黑名单
黑名单即将失效的 Token 储存起来 ,请求的 Token 需要在黑名单中查找 ,如果有这个Token ,则说明已经失效 , 没有则有效!在用户登出后,即可将该 Token 存储在黑名单中,这里需要注意一个点 ,就是黑名单保存 Token 的时间需要比 Token 本身的有效时间长 。