HTTP学习笔记(六)

Cookie

Cookie相关字段

  • Set-Cookie:(响应字段)值:key=value
    • 浏览器收到响应报文,看到里面有 Set-Cookie,知道这是服务器给的身份标识,于是就保存起来,下次再请求的时候就自动把这个值放进 Cookie 字段里发给服务器。
    • 服务器有时会在响应头里添加多个 Set-Cookie,存储多个“key=value”。但浏览器这边发送时不需要用多个 Cookie 字段,只要在一行里用“;”隔开就行。
  • Cookie:(请求字段)
1
2
3
4
5
6
7
8
9
10
11
12
13
// 第一次请求
GET/ HTTP/1.1
Host : xxxx

// 响应
HTTP/1.1 200 OK
Set-Cookie: a=xxX
Set-Cookie: b=yyy

// 第二次请求
GET/ HTTP/1.1
Host: xxxx
Cookie: a=xxxp; b=yyy
1
2
3
4
5
6
7
8
9
10
11
12
13
14

const router = require('koa-router')()
router.get('/', async (ctx, next) => {
// 第一次访问
if (!ctx.header.cookie) {
ctx.cookies.set('name', 'hrm');
ctx.body = 'first visit'
} else {
ctx.body = `you is ${ctx.header.cookie.split('=')[1]}`
}

})

module.exports = router

Cookie的属性

Cookie 就是服务器委托浏览器存储在客户端里的一些数据,而这些数据通常都会记录用户的关键识别信息。所以,就需要在“key=value”外再用一些手段来保护,防止外泄或窃取,这些手段就是 Cookie 的属性。

Cookie的属性均由服务端设定,因为Set-Cookie是响应字段。

首先,设置 Cookie 的生存周期,也就是它的有效期,让它只能在一段时间内可用,就像是食品的“保鲜期”,一旦超过这个期限浏览器就认为是 Cookie 失效,在存储里删除,也不会发送给服务器。

  • Expires:俗称“过期时间”,用的是绝对时间点,可以理解为“截止日期”
  • Max-Age:相对时间,单位是秒,浏览器用收到报文的时间点再加上 Max-Age,就可以得到失效的绝对时间。
    • Expires 和 Max-Age 可以同时出现,两者的失效时间可以一致,也可以不一致,但浏览器会优先采用 Max-Age 计算失效期。

其次,我们需要设置 Cookie 的作用域,让浏览器仅发送给特定的服务器和 URI,避免被其他网站盗用。

  • Domain
  • Path
    • “Domain”和“Path”指定了 Cookie 所属的域名和路径,浏览器在发送 Cookie 前会从 URI 中提取出 host 和 path 部分,对比 Cookie 的属性。如果不满足条件,就不会在请求头里发送 Cookie。

最后,Cookie的安全性

在 JS 脚本里可以用 document.cookie 来读写 Cookie 数据,这就带来了安全隐患,有可能会导致“跨站脚本”(XSS)攻击窃取数据。

  • HttpOnly:告诉浏览器,此 Cookie 只能通过浏览器 HTTP 协议传输,禁止其他方式访问,浏览器的 JS 引擎就会禁用 document.cookie 等一切相关的 API,脚本攻击也就无从谈起了。
  • SameSite:可以防范“跨站请求伪造”(XSRF)攻击,设置成“SameSite=Strict”可以严格限定 Cookie 不能随着跳转链接跨站发送,而“SameSite=Lax”则略宽松一点,允许 GET/HEAD 等安全方法,但禁止 POST 跨站发送。
  • Secure:表示这个 Cookie 仅能用 HTTPS 协议加密传输,明文的 HTTP 协议会禁止发送。但 Cookie 本身不是加密的,浏览器里还是以明文的形式存在。

Cookie的应用

  • 身份识别:保存用户的登录信息,实现会话事务。
  • 广告跟踪:很多的广告图片,这些图片背后都是广告商网站(例如 Google),它会“偷偷地”贴上 Cookie 小纸条,这样上其他的网站,别的广告就能用 Cookie 读出你的身份,然后做行为分析,再推给你广告。
    • 这种 Cookie 不是由访问的主站存储的,所以又叫“第三方 Cookie”

注意:Cookie 并不属于 HTTP 标准(RFC6265,而不是 RFC2616/7230),所以语法上与其他字段不太一致,使用的分隔符是“;”,与 Accept 等字段的“,”不同

知识补充:

  • 早期Cookie直接就是磁盘上的一些小文本文件,现在基本上都是以数据库记录的形式存放的(通常使用的是Sqlite)。浏览器对Cookie的数量和大小也都有限制,不允许无限存储,一般总大小不能超过4K
  • 如果不指定Expires或 Max-Age 属性,那么Cookie仅在浏览器运行时有效,一旦浏览器关闭就会失效,这被称为会话Cookie (sessioncookie)或内存Cookie (in-memory cookie),在Chrome里过期时间会显示为“Session”或“N/A”。
  • Max-Age=0,表示不能缓存,但在会话期间是可用的,浏览器会话关闭之前可以用cookie记录用户的信息。Max_Age<0,统一按0算,立即过期。

缓存控制

服务器缓存

相关字段

  • Cache-Control:(通用字段)

    服务器可以发“Cache-Control”头,浏览器也可以发“Cache-Control”,也就是说请求 - 应答的双方都可以用这个字段进行缓存控制,互相协商缓存的使用策略。

    • 属性

    • public和prevate

      public 表示该资源可以被所有客户端和代理服务器缓存,

      private 表示该资源仅能客户端缓存。默认值是 private,当设置了 s-maxage 的时候表示允许代理服务器缓存,相当于 public

    • Max-Age和S-Maxage:表示资源的有效期,值为number,秒钟。

      • 时间的计算起点是响应报文的创建时刻(即 Date 字段,也就是离开服务器的时刻),而不是客户端收到报文的时刻,也就是说包含了在链路传输过程中所有节点所停留的时间。
      • 服务器设定“max-age=5”,但因为网络质量很糟糕,等浏览器收到响应报文已经过去了 4 秒,那么这个资源在客户端就最多能够再存 1 秒钟,之后就会失效。
      • 两者是 cache-control 的主要字段,它们是一个数字,表示资源过了多少秒之后变为无效。在浏览器中,max-ages-maxage 都起作用,而且 s-maxage 的优先级高于 max-age在代理服务器中,只有 s-maxage 起作用。 可以通过设置 max-age 为 0 表示立马过期来向服务器请求资源。
    • no-store:不允许缓存,用于某些变化非常频繁的数据,例如秒杀页面;

    • no-cache:并不是不允许缓存,而是可以缓存,但在使用之前必须要去服务器验证是否过期,是否有最新的版本,如果有就使用最新的数据(协商缓存)

      • 即:no-cache总是使用服务端的最新数据,如果没有最新的数据在使用本地缓存.
      • 但是,在使用no-cache属性时,也向服务端发送了一次请求,同样有效率损耗,但是这个报文很小,不像是最新的资源,会传输最完整的报文,导致成本高.
      • 由于no-cache和no-store都不考虑缓存情况而是直接与服务器交互,所以当 no-cacheno-store 存在时会直接忽略 max-age 等。
      • 如果Cache-Control没有设置no-cacheno-store属性,则默认进行强缓存(本都磁盘读取)。
    • must-revalidate:如果缓存不过期就可以继续使用,但过期了如果还想用就必须去服务器验证。

      • no-store:买来的西瓜不允许放进冰箱,要么立刻吃,要么立刻扔掉;
      • no-cache: 可以放进冰箱,但吃之前必须问超市有没有更新鲜的,有就吃超市里的;
      • **must-revalidate:**可以放进冰箱,保鲜期内可以吃,过期了就要问超市让不让吃。
      属性对比图图示

客户端缓存

客户端缓存具体参看,

HTTP学习笔记(七)

1
2
3
4
router.get('/', async (ctx, next) => {
ctx.set("Cache-Control", 'Max-Age=5');
ctx.body = Math.random()
})

当浏览器多次刷新,会发现每次得到的随机数还是会变化

原因是:

  • 浏览器会在请求头里加一个“Cache-Control: max-age=0”。因为 max-age 是“生存时间”,max-age=0 表示缓存立即过期,需要最新的数据,而本地缓存里的数据至少保存了几秒钟,所以浏览器就不会使用缓存,而是向服务器发请求。
    • 即:按F5刷新页面,浏览器直接让缓存过期,与服务端进行协商缓存
  • 服务器看到 max-age=0,也就会用一个最新生成的报文回应浏览器。
  • Ctrl+F5 的“强制刷新”:它其实是发了一个“Cache-Control: no-cache”(浏览器强制设置),含义和“max-age=0”基本一样,强制获取最新的资源,就连 if-modified-since 等其他缓存协议字段都会被吃掉。

点击浏览器的“前进”“后退”按钮,再看开发者工具,发现“from disk cache”的字样,意思是没有发送网络请求,而是读取的磁盘上的缓存。另外,浏览器的重定向和跳转,也使用了缓存。

图示

可以理解为:在“前进”“后退”“跳转”这些重定向动作中浏览器不会“夹带私货”,只用最基本的请求头,没有“Cache-Control”,所以就会检查缓存,直接利用之前的资源,不再进行网络通信。

条件请求

浏览器用“Cache-Control”做缓存控制只能是刷新数据,不能很好地利用缓存数据,又因为缓存会失效,使用前还必须要去服务器验证是否是最新版。这也是条件请求产生的原因。

浏览器可以用两个连续的请求组成“验证动作”:先是一个 HEAD,获取资源的修改时间等元信息,然后与缓存数据比较,如果没有改动就使用缓存,节省网络流量,否则就再发一个 GET 请求,获取最新的版本。

但这样的两个请求网络成本太高了,所以 HTTP 协议就定义了一系列“If”开头的“条件请求”字段,专门用来检查验证资源是否过期,把两个请求才能完成的工作合并在一个请求里做。而且,验证的责任也交给服务器,浏览器只需“坐享其成”。

相关字段:

条件请求一共有 5 个头字段,我们最常用的是“if-Modified-Since”(请求字段)和“If-None-Match”(请求字段)这两种。

其中,和if-Modified-Since配合的响应字段是Last-Modify

If-None-Match配合的响应字段是Etags

第一种:

  • Last-modified:(响应头字段)表示文件的最后修改时间。在浏览器第一次请求某一个URL时,服务器端的返回状态会是200,内容是客户端请求的资源,同时有一个Last-Modified的属性标记此文件在服务期端最后被修改的时间。
  • 在客户端后来的请求中(非第一次),服务端会对比该字段和资源的最后修改时间(客户端携带的If-Modify-Since字段),若一致则证明没有被修改,告知浏览器可直接使用缓存并返回 304;若不一致则直接返回修改后的资源,并修改 last-modified 为新的值。
  • if-Modified-Since:(请求头字段)客户端第二次以及之后请求请求此URL时,根据 HTTP 协议的规定,浏览器会向服务器传送If-Modified-Since 报头(在第一次访问时的时间被存储的值),询问该时间之后文件是否有被修改过,具体的时间表示本地浏览器存储的文件修改时间
    • 如果服务器端的资源没有变化,则时间一致,自动返回HTTP状态码304(Not Changed.)状态码,内容为空,客户端接到之后,就直接把本地缓存文件显示到浏览器中,这样就节省了传输数据量。
    • 如果服务器端资源发生改变或者重启服务器时,时间不一致,就返回HTTP状态码200和新的文件内容,客户端接到之后,会丢弃旧文件,把新文件缓存起来,并显示到浏览器中。

缺点:

  1. 只要编辑了(例如增加无用的空格),不管内容是否真的有改变,都会以这最后修改的时间作为判断依据,当成新资源返回,从而导致了没必要的请求响应,而这正是缓存本来的作用,即避免没必要的请求。
  2. 时间的精确度只能到秒,如果在一秒内的修改是检测不到更新的,仍会告知浏览器使用旧的缓存。

第二种:

第二种方式的出现解决了第一种方式的缺点。

  • If-None-Match:(请求头字段):服务端传过来的Etag的值
  • ETag:(通用字段)“Entity Tag”,实体标签,资源的唯一标识,主要是用来解决修改时间无法准确区分文件变化的问题。
    • 比如,一个文件在一秒内修改了多次,但因为修改时间是秒级,所以这一秒内的新版本无法区分。
    • 比如,一个文件定期更新,但有时会是同样的内容,实际上没有变化,用修改时间就会误以为发生了变化,传送给浏览器就会浪费带宽。
    • 使用 ETag 就可以精确地识别资源的变动情况,只要文件有改动,就生成最新的Etag的值(解决上述缺点2),让浏览器能够更有效地利用缓存。
    • ETag的强弱之分:
      • 强 ETag 要求资源在字节级别必须完全相符,
      • 弱 ETag 在值前有个“W/”标记,只要求资源在语义上没有变化,但内部可能会有部分发生了改变(例如 HTML 里的标签顺序调整,或者多了几个空格)。
图示

Etag的工作原理

  • Etag在服务器上生成后,客户端通过If-Match或者说If-None-Match这个条件判断请求来验证资源是否修改.我们常见的是使用If-None-Match.
  • 请求一个文件的流程如下: 新的请求 客户端发起HTTP GET请求一个文件(css ,image,js);
  • 服务器处理请求,返回文件内容和一堆Header(包括Etag,例如”2e681a-6-5d044840”),http头状态码为为200.
  • 同一个用户第二次这个文件的请求 客户端在一次发起HTTP GET请求一个文件,注意这个时候客户端同时发送一个If-None-Match头,这个头中会包括上次这个文件的Etag(例如”2e681a- 6-5d044840”)
  • 这时服务器判断发送过来的Etag和自己计算出来的Etag,因此If-None-Match为False,不返回200,返 回304,客户端继续使用本地缓存;

需要第一次的响应报文预先提供“Last-modified”或者“ETag”,然后第二次请求时就可以带上缓存里的原值,验证资源是否是最新的。

如果资源没有变,服务器就回应一个“304 Not Modified”,表示缓存依然有效,浏览器就可以更新一下有效期,然后放心大胆地使用缓存了。

条件请求里其他的三个头字段是“If-Unmodified-Since”“If-Match”和“If-Range”,掌握了“if-Modified-Since”和“If-None-Match”,可以轻易地“举一反三”。

  • 注意:
    • 服务器又设置了Cache-Control:max-age和Expires时,会同时使用,也就是说在完全匹配If-Modified-Since和If-None-Match,即检查完修改时间和Etag之后,服务器才能返回304.
    • Expires属于HTTP1.0规定的缓存字段,若两者在响应头中均出现,Cache-Control的优先级更高
    • Etag比last-modify优先级高。

这两种方式,

其他知识补充

  • 浏览器也可以发送“Cache-Control”字段,使用“max-age=0”(刷新)或“no_cache”(Ctrl + F5强制刷新)刷新数据。

  • 即使有Last-Modify和Etags响应字段,浏览器依然可以使用Ctrl+F5强制刷新,得到的状态码是200和最新的资源。

    • 原因:强制刷新是因为请求头里的 If-Modified-Since 和 If-None-Match 会被清空所以会返回最新数据
  • no-cache可以理解为,’Max-Age=0, Must-revalidate’,

  • 除了“Cache-Control”,服务器也可以用“Expires”字段来标记资源的有效期,它的形式和Cookie 的差不多,同样属于“过时”的属性,优先级低于“Cache-Control”。

  • 如果响应报文里提供了“Last-modified”,但没有“Cache-Control”或“Expires”,浏览器会使用“启发”(Heuristic)算法计算一个缓存时间,在RFC里的建议是:(Date -Last-modified) * 10%。

  • 每个Web服务器对ETag的计算方法都不一样,只要保证数据变化后值不一样就好,但复杂的计算会增加服务器的负担。Nginx的算法是“修改时间+长度”,实际上和Last-modifed基本等价。

  • cache和cookie的不同点以及相同点:

    • 不同点:
    • Cookie 会随请求报文发送到服务器,而 Cache 不会,但可能会携带 if-Modified-Since(保存资源的最后修改时间)和 If-None-Match(保存资源唯一标识) 字段来验证资源是否过期。
    • Cookie 在浏览器可以通过脚本获取(如果 cookie 没有设置 HttpOnly),Cache 则无法在浏览器中获取(出于安全原因)
    • Cookie 通过响应报文的 Set-Cookie 字段获得,Cache缓存的是完整的报文
    • 用途不同。Cookie 常用于身份识别,Cache 则是由浏览器管理,用于节省带宽和加快响应速度。
    • Cookie 的 max-age 是从浏览器拿到响应报文时开始计算的,而 Cache 的 max-age 是从响应报文的生成时间(Date 头字段)开始计算。
  • cache-control中的private识别:缓存策略取决于服务器,它认为这个缓存只能存放在客户端,不能存放在代理上,就设置private。

  • 强缓存和协商缓存

    • 强缓存:浏览器直接从本地缓存中获取数据,不与服务器进行交互,返回的状态码是 200
    • 协商缓存:浏览器发送请求到服务器,服务器判断是否可使用本地缓存.会去服务器比对,若没改变才直接读取本地缓存,返回的状态码是 304
      • Last-Modify和If-Modify-Since搭配
      • Etag和If-None-Match搭配

缓存参考文档: