cookie

客户端和服务器的传输使用 HTTP 协议,HTTP 协议是无状态的

无状态,就是每一次请求响应都是独立的,服务器不知道这一次请求,和之前某次请求是不是同一个人。

有什么办法能够让服务器标识出请求的身份呢?

服务器按照下面的流程来认证客户端的身份:

  1. 客户端上次请求后(如:登录成功),服务器会给客户端一个出入证
  2. 后续客户端的每次请求,都必须要附带这个出入证

服务器发扬了认证不认人的优良传统,就可以很轻松的识别身份了。

但是,用户不可能只在一个网站登录,于是客户端会收到来自各个网站的出入证,因此,就要求客户端要有一个类似于卡包的东西,能够具备下面的功能:

  1. 能够存放多个出入证:这些出入证来自不同的网站,也可能是一个网站有多个出入证,分别用于出入不同的地方
  2. 能够自动出示出入证:客户端在访问不同的网站时,能够自动的把对应的出入证附带请求发送出去。
  3. 正确的出示出入证:客户端不能将肯德基的出入证发送给麦当劳。
  4. 管理出入证的有效期:客户端要能够自动的发现那些已经过期的出入证,并把它从卡包内移除。
  5. 保证出入证安全:对出入证有一定的安全保障措施。

能够满足上面所有要求的,就是 cookie,cookie 类似于一个卡包,专门用于存放各种出入证,并有着一套机制来自动管理这些证件。

cookie 是浏览器中特有的一个概念,它就像浏览器的专属卡包,管理着各个网站的身份信息。

每个 cookie 就相当于是属于某个网站的一个卡片,它记录了下面的信息:

  • key:键
  • value:值
  • domain:域,表达这个 cookie 是属于哪个网站的
    • 用于判断某个请求是否携带该 cookie
    • 如果不设置,浏览器会自动将其设置为当前的请求域
    • 如果服务器响应了一个无效的域(响应的域连服务器的根域都不一样),浏览器会忽略
  • path:路径,表达这个 cookie 是属于该网站的哪个基路径的
    • 用于判断某个请求是否携带该 cookie
    • 如果不设置,浏览器会将其自动设置为当前请求的路径
  • secure:是否使用安全传输(HTTPS)
    • 用于判断某个请求是否携带该 cookie
  • expire:过期时间,表示该 cookie 在什么时候过期,绝对时间
    • 是一个有效的 GMT 时间(格林威治标准时间)字符串,如 Fri, 17 Apr 2020 09:35:59 GMT
  • max-age:过期时间,表示该 cookie 在什么时候过期,相对于当前时间
    • expiremax-age 通常仅设置一个即可
    • 如果 expiremax-age 都没有,则表示会话结束后过期(对于大部分浏览器而言,关闭所有浏览器窗口意味着会话结束)
  • httponly:设置 cookie 是否仅能用于传输。如果设置了该值,表示该 cookie 仅能用于传输,而不允许在客户端通过 JS 读写
    • 这对防止跨站脚本攻击(XSS)会很有用
  • sameSite
    • Strict:严格,请求来自设置 cookie 的站点才会携带该 cookie
    • Lax:宽松,cookie 不会在跨站请求中被发送,如:加载图像或框架(frame)的请求。但 cookie 在用户从外部站点导航到源站时,cookie 也会被发送(例如,访问一个链接)。这是 SameSite 属性未被设置时的默认值。
    • None:无,浏览器在跨站和同站请求中均会发送 cookie。在设置这一属性值时,必须同时设置 Secure 属性,就像这样:SameSite=None; Secure。如果未设置 Secure,则会出现 以下错误
    • 用于判断某个请求是否携带该 cookie
    • 可以在一定程度上防范跨站请求伪造攻击(CSRF

SameSite=None 但未设置 Secure 的报错:

Cookie "myCookie" rejected because it has the "SameSite=None" attribute but is missing the "secure" attribute.
 
This Set-Cookie was blocked because it had the "SameSite=None" attribute but did not have the "Secure" attribute, which is required in order to use "SameSite=None".

当浏览器向服务器发送一个请求的时候,它会瞄一眼自己的卡包,看看哪些卡片适合附带捎给服务器

如果一个 cookie 同时满足以下条件,则这个 cookie 会被附带到请求中:

  • cookie 没有过期
  • cookie 中的域和这次请求的域是匹配的
    • 比如 cookie 中的域是 yuanjin.tech,则可以匹配的请求域是 yuanjin.techwww.yuanjin.techblogs.yuanjin.tech
    • 比如 cookie 中的域是 www.yuanjin.tech,则只能匹配 www.yuanjin.tech 这样的请求域
    • cookie 是不在乎端口的,只要域匹配即可
  • cookie 中的 path 和这次请求的 path 是匹配的
    • 比如 cookie 中的 path 是 /news,则可以匹配的请求路径可以是 /news/news/detail/news/a/b/c 等等,但不能匹配 /blogs
  • 验证 cookie 的安全传输
    • 如果 cookie 的 secure 属性是 true,则请求协议必须是 HTTPS,否则不会发送该 cookie
    • 如果 cookie 的 secure 属性是 false,则请求协议可以是 HTTP,也可以是 HTTPS
  • 验证 sameSite 规则是否符合

具体加入的方式是,浏览器会将符合条件的 cookie,自动放置到 Cookie 请求头中,值的格式是 key1=value1; key2=value2; ...,每一个键值对就是一个符合条件的 cookie。

cookie 中包含了重要的身份信息,永远不要把你的 cookie 泄露给别人!否则,他人就拿到了你的证件,就具备了为所欲为的可能性。

由于 cookie 是保存在浏览器端的,同时,很多证件又是服务器颁发的

所以,cookie 的设置有两种模式:

  • 服务器响应:这种模式是非常普遍的,当服务器决定给客户端颁发一个证件时,它会在响应的消息中包含 cookie,浏览器会自动的把 cookie 保存到卡包中
  • 客户端自行设置:这种模式少见一些,不过也有可能会发生,比如用户关闭了某个广告,并选择了「以后不要再弹出」,此时就可以把这种小信息直接通过浏览器的 JS 代码保存到 cookie 中。后续请求服务器时,服务器会看到客户端不想要再次弹出广告的 cookie,于是就不会再发送广告过来了。

服务器可以通过设置响应头,来告诉浏览器应该如何设置 cookie,响应头按照下面的格式设置:

Set-Cookie: cookie1
Set-Cookie: cookie2

通过这种模式,就可以在一次响应中设置多个 cookie 了。其中,每个 cookie 的格式如下:

key=value; path=?; domain=?; expire=?; max-age=?; secure; httponly

每个 cookie 除了键值对是必须要设置的,其他的属性都是可选的,并且顺序不限。

当这样的响应头到达客户端后,浏览器会自动的将 cookie 保存到卡包中,如果卡包中已经存在一模一样的卡片(其他信息,如 path、domain 相同),则会自动的覆盖之前的设置。

如何删除浏览器的一个 cookie 呢?

如果要删除浏览器的 cookie,只需要让服务器响应一个同样的域、同样的路径、同样的 key,只是时间已经过期的 cookie 即可:

set-cookie: token=; domain=yuanjin.tech; path=/; max-age=-1

所以,删除 cookie 其实就是修改 cookie 让其过期。浏览器按照要求修改了 cookie 后,会发现 cookie 已经过期,于是自然就会删除了。

注意

无论是修改还是删除,都要注意 cookie 的域和路径,因为完全可能存在域或路径不同,但 key 相同的 cookie,因此无法仅通过 key 确定是哪一个 cookie

既然 cookie 是存放在浏览器端的,所以浏览器向 JS 公开了接口,让其可以设置 cookie:

document.cookie = "key=value; path=?; domain=?; expire=?; max-age=?; secure";

可以看出,在客户端设置 cookie,和服务器设置 cookie 的格式一样,只是有下面的不同:

  • 没有 httponly
  • path 的默认值
    • 在服务器端设置 cookie 时,如果没有写 path,使用的是请求的 path。
    • 而在客户端设置 cookie 时,也许根本没有请求发生。因此,path 在客户端设置时的默认值是当前网页的 path
  • domain 的默认值,和 path 同理
    • 服务器端设置的默认值是服务器的 domain
    • 客户端设置时的默认值是当前网页的 domain
  • 安全性:不能在本地存储敏感信息、CSRF 攻击等
  • 浏览器独有:微信小程序、手机 APP 等没有该技术,一套后端,多套前端的技术实践需要更多的工作

session

原理

cookie 的数据都是存到客户端的,很不安全,session 基于 cookie 实现。

其原理就是:服务器开辟一块空间(可以是狭义的 session 内存空间,也可以是 Redis 等缓存数据库),将该用户相关的数据存储到该空间,给此空间分配一个 session id(sid)并通过 cookie 返回给客户端,下次该客户端请求服务器就会携带 sid,服务器可以通过 sid 获取空间中存储的内容

消除 session

session 会占用服务器资源,会话结束就要将其销毁,通常有两种方式:

  1. 过期时间:当客户端长时间没有传递 session id 过来时,服务器可以在过期时间之后自动清除 session
    • 一般都会设置,比较好的解决方案
  2. 客户端主动通知:可以使用 JS 监听客户端页面关闭或其他退出操作,然后通知服务器清除 session
    • 客户端主动通知有不确定性(如客户端直接关闭浏览器或关闭电脑,本地 JS 可能监听不到)

面试题

cookie、sessionStorage、localStorage 的区别

cookie、sessionStorage、localStorage 都是保存本地数据的方式。

cookie 用于标识请求端身份信息

  • cookie 兼容性较好,所有浏览器均支持。
  • 浏览器针对 cookie 会有一些默认行为,比如当响应头中出现 set-cookie 字段时,浏览器会自动保存 cookie 的值;浏览器发送请求时,会附带匹配的 cookie 到请求头中。这些默认行为,使得 cookie 长期以来担任着维持登录状态的责任。
  • 与此同时,也正是因为浏览器的默认行为,给了恶意攻击者可乘之机,CSRF 攻击就是一个典型的利用 cookie 的攻击方式。虽然 cookie 不断的改进,但前端仍然需要时刻注意 cookie 的安全问题
  • 不要在 cookie 中存放敏感信息,敏感信息应在服务器或数据库中,即使是数据库也不能明文存储,如密码需要进行二次加密、敏感数据需要脱敏处理
  • cookie 的大小是有限制的,一般浏览器会限制同一个域下的 cookie 总量不超过 4KB
  • cookie 会与服务器的 domain、path 关联,端口任意

HTML5 新增了 sessionStorage 和 localStorage,前者用于保存会话级别的数据,后者用于更持久的保存数据。

  • 浏览器针对它们没有任何默认行为,这样一来,就把保存数据、读取数据的工作交给了前端开发者,这就让恶意攻击者难以针对登录状态进行攻击。
  • sessionStorage 和 localStorage 拥有更大的空间,多数浏览器一般在 2.5MB~10MB
  • sessionStorage 和 localStorage 只与客户端的域关联,包括协议、域名、端口

浏览器离线存储

cookie、session、token 的区别

cookie 和 session:

  1. cookie 的数据保存在浏览器端;session 的数据保存在服务器
  2. cookie 的存储空间有限;session 的存储空间不限
  3. cookie 只能保存字符串;session 可以保存任何类型的数据
  4. cookie 中的数据容易被获取;session 中的数据难以获取
  5. session 基于 cookie,cookie 基于浏览器

token(如:JWT):

  1. token 将数据存到浏览器端,服务端通过验证签名防止篡改和伪造
  2. token 不基于浏览器实现
  3. token 不能放敏感信息,当然可以基于 token 实现 session 就可以在服务器放敏感信息了