理解 HTTP 协议对构建网络应用是一个非常基础的要求,比如爬虫类程序,必须深入理解 Request 和 Resonse 各首部信息(当然,这个前提是建立在对方站点完全遵循协议)。若是网站类程序,更需要理解这些含义,因为客户端往往就是浏览器,一般来讲都会严格遵循协议。参考:httpwg(全面权威)、MDN、wiki、协议
一、基础(杂项)首部
Request 首部
请求主机:端口
Host: domain.com:8080
请求来源的 主机:端口 和 页面URL
Origin: http://domain.org:8080
Referer: http://domain.org:8080/page
连接信息:是否允许复用 TCP 连接:HTTP 是建立在 TCP 之上的,告知本次请求结束后,是否关闭 TCP,还是在请求下一个页面时直接复用 TCP 通道,若复用,还可以设置保持连接时长、最大复用次数。
该首部在使用 HTTP /2 协议时不会(得)发送,总是 keep-alive。
Connection: close
Connection: keep-alive
Keep-Alive: timeout=5, max=1000
请求报文的创建时间,一般即为当前时间,几乎没有客户端会发送该首部
Date: Wed, 21 Oct 2015 07:28:00 GMT
客户端信息,可能是浏览器/App/蜘蛛等任何可用信息
User-Agent: Mozilla/5.0 (compatible; MSIE 9.0; Windows Phone OS 7.5; Trident/5.0; IEMobile/9.0)
比如爬虫程序,若请求较为频繁,可携带该首部,便于对方网站联系你
From: [email protected]
通知服务端,客户端即将传输大文件,希望服务端返回 100 响应,以便客户端继续;
没有浏览器会使用这个,但如 cURL 等会使用,服务端可以响应:同意(100)、拒绝(4XX)
Expect: 100-continue
是否禁止追踪 (君子协定,需要看服务端什么态度)
DNT: 0
(允许) 或DNT: 1
(禁止)
Response 首部
连接信息,参见 Request 首部说明
Connection: close | keep-alive
Keep-Alive: timeout=5, max=1000
报文的创建时间,根据 RFC 7231,除非是服务器时钟不准确,都必须发送该首部
Date: Wed, 21 Oct 2015 07:28:00 GMT
在无法处理 Request method 时发送 405 响应,通过该首部告知客户端可处理的 method (可以为空)
Allow: GET, POST, HEAD
网站维护,当 Response 为 503 或 301 响应时,可告知再次可访问的时间;对于浏览器客户端用处不大,但对于蜘蛛等自动程序,该首部有一定意义。
Retry-After: Wed, 21 Oct 2015 07:28:00 GMT
Retry-After: 120
处理请求的软件或者产品(或组件产品)的名称
Server: Apache/2.4.1 (Unix)
服务端处理请求的各种调试信息,生产版切勿发送,有安全隐患
Server-Timing: cpu;dur=2.4, total;dur=123.4
服务端应用程序信息,生产版切勿发送,有安全隐患
X-Powered-By: PHP/7.2
语义上与 Link 相同,目前仍处于 草案 阶段,感觉这个不实用,前后端分离搞了这么多年,这个岂不是开倒车了。但可能对于某些全局通用的 Link 有那么一丢丢作用,可作为通用的缺省 Link。另外对于追求极致体验的,可以帮助浏览器更快(先于页面或同时加载)请求 CSS 、JS 资源,快速完成页面渲染,相当于在页面加载完成前,预加载所需资源。
Link:
; rel="icon", ; rel="stylesheet" 来源信息控制,可禁止跟踪(在当前 Response 页面打开外链不发送 referrer);一般不使用 header 发送,而是通过页面的 meta 等设定(详情)。https -> http 跳转默认不发送,http 若是广告主,可能就需要设置该首部让浏览器发送以便对方统计。
Referrer-Policy: no-referrer | unsafe-url | .....
通常为一些 Css / Js 资源,指明 SourceMap 地址用于帮助在浏览器调试,但由于资源本事直接支持设置 SourceMap,所以该首部很不常用。非标准协议,但 支持度 还不错。
SourceMap: /path/to/file.js.map
针对浏览器的,设置允许 调试 加载信息的域名。比如当前 response 为一张图片,嵌入到其他网站,那么就可以使用该首部来 允许/禁止 该站点调试。
Timing-Allow-Origin: *
Timing-Allow-Origin: https://domain.com
针对浏览器的,加载页面实体时就开始预读取图片、静态资源、甚至是链接的 DNS 以加快渲染;该指令也可通过 meta 设定:参见
X-DNS-Prefetch-Control: on
/X-DNS-Prefetch-Control: off
针对 IE 浏览器,等同于
X-UA-Compatible: IE=Edge,chrome=1
告知跟踪情况,详情
Tk: N
二、HTTP 认证
Response: 未认证请求,服务端响应 401,并告知认证方式
WWW-Authenticate:
realm= Request: 客户端携带认证首部进行访问
Authorization:
Response: 服务端为代理服务器,且也需要认证;响应 407,告知客户端认证方式
Proxy-Authenticate:
realm= Request: 同样的,客户端携带代理服务器的认证信息
Proxy-Authorization:
逻辑较为简单,更多信息可 参考
三、内容协商
内容格式: Accept & Accept-Charset / Content-Type & Accept-Patch
Request: 表明客户端可处理的格式,可能的单个、多个 或 任意
Accept: text/html
Accept: text/html, application/xhtml+xml, image/*, application/xml;q=0.9, */*;q=0.8
Accept: */*
Request: 表明客户端可处理的字符集
Accept-Charset: utf-8, iso-8859-1;q=0.5
Response: 指明响应资源的 格式;字符集 (若 Response 可满足客户端需求)
Content-Type: text/html
Content-Type: text/html; charset=utf-8
Response: 若无法满足客户端需求,返回 406 或 415
HTTP 406
(告知无法返回所支持的格式)
HTTP 415
(无法满足需求,同时告知服务端可响应的格式、字符集)
Accept-Patch: application/example, text/example
Accept-Patch: text/example;charset=utf-8
一般情况下,服务端对格式协商不太准守协议,无论客户端是否可处理,总是任性的返回资源格式/字符集。客户端若无法处理响应格式,往往表现为 “保存为文件”。
若严格准守协议,一般也是返回 406;即使返回 415 并告知支持格式,客户端仍然无法进行修正。因为客户端通常已经在 Accept 中已告知了所有可处理的格式,即使通过 Accept-Patch 告知也无济于事。
但该首部仍然有一定意义,比如对于 API 接口,可根据请求返回 json 或 xml 以加强接口健壮性。
内容语言:Accept-Language / Content-Language & Content-Location
Request: 客户端可处理的语言
Accept-Language: zh-CN,zh;q=0.9,ca;q=0.8,en;q=0.7,zh-TW;q=0.6,ja;q=0.5,cs;q=0.4,ko;q=0.3
Response: 当前资源包含的语言
Content-Language: de, en
对于支持多语言的网站可通过这两个首部判断返回。
但不建议依赖该 header , 最好设计其他逻辑由用户自主设置语言,比如通过 url path 或 cookie 等作为依赖。对于同一个 url 多次访问可能返回不同内容(比如多语言页面)。可对展示不同语言的页面设置一个实际 Content-Location。 当两次访问同一个 URL,但返回的 Content-Location 不同,客户端就不会使用上次的缓存内容。比如访问 index.html,则可以根据当前语言设置首部
Content-Location: /index_zh.html
Content-Location: /index_en.html
所以:再次不建议同一个 url 展示多语言,有很多不易处理的逻辑。
内容压缩:Accept-Encoding / Content-Encoding
Request: 客户端可处理的压缩算法
Accept-Encoding: gzip, deflate, br
Response: 响应使用的压缩算法
Content-Encoding: gzip
内容传输:Accept-Ranges & Content-Disposition / If-Range & Range / Content-Length & Content-Range
Response: 对于首次请求,服务端应返回是否支持的断点传输,none 就是不支持
Accept-Ranges: none
(缺省)
Accept-Ranges: bytes
Response: 希望客户端如何处理: 使用默认方式打开内容 (inline) 或 保存为文件 (attachment)
Content-Disposition: inline
(缺省)
Content-Disposition: attachment
Content-Disposition: attachment; filename="filename.jpg"
Request: 若允许断点下载,通过 If-Range (或 If-Match, 不推荐) 设置获取到的 Etag (推荐) 或 Last-Modified,以便服务端判断该文件在上次传输之后是否发生变动,能否续传。
If-Range: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-Range: Wed, 21 Oct 2015 07:28:00 GMT
Request: 告知本次请求所需内容范围,可能为多段。start-end (end 为空代表请求到结束为止)
Range: bytes=200-1000, 2000-6576, 19000-
Response: 返回内容总长度,无论是否为续传,该值总应该返回
Content-Length:
Response: 对于断点续传,根据 Request Range 返回本次传输数据的起始范围/总长度
Accept-Ranges: bytes
(仍需返回该首部)
Content-Range: bytes 200-1000/67589
Content-Range: bytes 200-1000/*
内容分块: TE / Transfer-Encoding & Trailer
HTTP 协议支持 TCP 通道复用,这意味着一个 TCP 通道可处理多次 HTTP 的数据传输,所以需要知道每次 HTTP 实体资源的长度以便区分、避免混淆,也就是为什么 Response 必须要指明
Content-Length
。但有时候 Response 实体是动态生成的,需要完整生成后才能知道长度,较为耗时,更好的方案是边生成、边传输的分块传输。Request: 告知客户端希望分块传输使用的压缩算法
TE: trailers, gzip;q=0.8, deflate;q=0.5
常用compress
/deflate
/gzip
,还有一个特殊的trailers
; 这里是“希望”,而不是“必须”、“仅支持”。客户端支持的压缩算法由Accept-Encoding
提供,实际上,很少有浏览器会发送该报文。Response: 返回 分块传输 所用的压缩算法
Transfer-Encoding: chunked
(告知要分块传输,并未压缩)
Transfer-Encoding: gzip, chunked
(告知要分块传输,且使用了 gzip 压缩)Response: 告知分块数据中携带额外的 元信息
Trailer: header-names
服务端若想传输该报文,前提是客户端发送报文中包含TE: trailers
(表示客户端可以处理元信息),但由于浏览器基本都不会发送TE
报文,所以Trailer
报文很少用到。相对于
Content-Encoding
,Transfer-Encoding
的特点 :
- 必须包含
chunked
,当使用压缩算法时,“务必”将chunked
放到最后- 采用分块传输时,Response 不应该发送
Content-Length
报文Transfer-Encoding
针对的是分块数据使用的压缩算法,分块数据在经过代理服务器时,可以被解码并重新使用其他算法再压缩,并同时修改Transfer-Encoding
报文分发给下级;而Content-Encoding
是针对完整实体的压缩算法,也就是说,客户端在获取完整实体后,根据该报文整体解码,代理服务器中途不得修改。
● 但考虑到分块传输一般是在不方便获取完整实体时才使用的手段,并且二次压缩并不能获得较大收益,还会增加服务器负担,所以不应该有两个报文同时存在
● 该结论仅根据文档得出,需实际验证,事实上不同客户端对此的理解可能有偏差。- 与断点传输不同之处在于:分块传输是在一次 HTTP 通信中将消息实体分块传输;断点传输的每一次传输都是一个完整的 HTTP 通信,比如客户端暂停下载后重新开始,或多进程发送请求分段下载,最后合并。所以断点传输按照正常请求响应,使用
Accept-Encoding
/Content-Encoding
协商机制即可。
内容上传:Content-*
Request: 比如 POST 或 PUT 操作,会发送实体内容给服务端,以下首部也可用在请求的报文中
Content-Type: multipart/form-data; boundary=**
Content-Length:
Content-Encoding: gzip
Content-Language: zh
通常,在浏览器中,POST 请求会自动设置实体报文,一般只有
Content-Type
和Content-Length
,这对于常见的表单提交已够用。但如果需要上传大文件,是无法简单通过 HTTP 协议进行优化的,需要前后端程序实现相关逻辑,一般有以下优化方向:
- 若需要断点上传,可考虑将 Request 实体分片传输(分片后可并发同时传输提升效率),这需要服务端程序配合,不再是协议范围内的知识了。可 参阅
- 浏览器默认是不会对 Request 实体进行压缩的,可考虑使用如 pako 这样的 JS 扩展来压缩传输实体以减少传输流量、提升速度。并使用
Content-Encoding
报头标明压缩算法。Request: 在 MDN 将分块传输首部归到了 Response header,但根据 RFC 7230 的描述来看,Request 消息实体也是可以分块传输的,当然,浏览器无法直接支持该操作,需自行通过程序处理。
Transfer-Encoding:chunked
内容摘要:Content-MD5 / Want-Digest & Digest
可使用在 Request 和 Response,用于对方校验接收到的实体内容是否完整。
Content-MD5:
该首部曾经属于标准协议,但 rfc7231 [page 92] 移除了该首部,目前浏览器客户端已不会针对 Response 的 Content-MD5 进行验证,但这并不妨碍代理服务器进行识别验证,也能用于设计分片断点上传、分块传输的数据一致性校验。
新的 草案 设计了一组新的报文用于校验消息实体
Request: 客户端希望服务端使用摘要算法
Want-Digest: SHA-256;q=0.3, sha;q=1
Response: 服务端返回的实体消息摘要
Digest: sha-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=
Want-Digest
只能用于 Request,Digest
按协议可用于 Request 或 ResponseDigest
支持的算法可参见 草案,摘要值使用 Base64 格式。Digest
仅针对消息实体,计算摘要时不能包括报文。且是针对完整消息,即使对于断点传输,摘要也应该是完整实体消息的摘要。该报文也可用于无实体消息响应,比如 Method Header 的请求,依然可以提前返回摘要信息。- 吐槽:为啥不用
Accept-Digest
和Content-Digest
与实体首部保持一致性呢?
内容跳转: Location
Response: 将页面重新定向至的地址。
Location:
30X 跳转响应:
- 301 Moved Permanently:永久迁移,旧地址不在使用,搜索引擎应将索引改为新地址,不再爬取旧地址
- 302 Found:资源移动,旧地址仍可使用。搜索引擎应仍索引旧地址,但内容从新地址抓取。
- 303 See Other :PUT 或 POST 操作后,没有转向新资源,而是转向一个过渡页(如消息确认、上传进度页)
- 307 Temporary Redirect:临时移动,日后应仍访问旧地址以确认是否恢复
- 308 Permanent Redirect:永久迁移,且保持 method (如 301 在跳转时会将 POST 改为 GET;308不能这样)
- 305 / 306 已不再(不推荐)使用;304 为内容缓存可用时的响应(见下面),不用于跳转。
201 Created:该响应也能携带 Location 首部,跳转到新建成功的资源地址。
Response: 刷新 / 跳转
Refresh:
[; url= ] 这不是一个非标准首部,RFC 标准未规定过该首部,但浏览器实际上基本都支持。作用就是在 seconds 秒之后刷新,若指定 url,则是跳转到指定地址。其效果等同于
,推荐使用 meta 方式实现目的,该 meta 的 支持度 非常好
四、内容缓存
缓存时长: Cache-Control & Expires
Response: 告知客户端资源缓存的最大时长,过期后应该重新请求。(其中 max-age 优先级更高)
Cache-Control: max-age=36000, s-maxage=700
(缓存时长)
Expires: Wed, 21 Oct 2015 07:28:00 GMT
(直接告知过期时间;HTTP/1.0 定义,已不推荐使用)Response: 若使用
Expires
设置缓存时长,另外牵涉下面两个首部
Date: Wed, 21 Oct 2015 07:28:00 GMT
Age: 300
说明:
- 代理服务器缓存时长优先级
s-maxage
>max-age
,客户端会忽略s-maxage
; 若没有Cache-Control
,客户端、代理服务器都应根据Expires
进行缓存。- 若使用
Expires
,强烈建议设置Date
(报文创建时间,一般即为服务器的当前时间,对于代理服务器,应该缓存资源时的时间), 客户端可使用Expires - Date = maxAge
计算出缓存实际生命长度,若没有 Date,将使用客户端时钟,但客户端时钟可能不准确,便无法准确计算。- 对于代理服务器,建议设置
Age
(告知资源已在代理服务器上已存活的时长,不设置则认为是 0),若未设置Age
,客户端可使用client_now - Date = Age
计算缓存实际已消耗的生命长度,客户端的最终对资源可缓存时长为maxAge - Age
,这样便可在过期后进行了校验了。但鉴于对客户端时钟的不信任,建议直接返回Age
首部。- 推荐使用
Cache-Control: max-age
直接设置,但建议同时返回Expires
、Date
、Age
以兼容不支持Cache-Control
的客户端。更详细说明可参见 RFC 2616在未过期时间内,客户端都不应重新请求服务端,而是直接使用缓存。如果使用浏览器验证的话:
- 访问资源,直接刷新,每次都会请求(在 Chrome 中已可看到 304 响应,注意不要勾选开发者工具的 "Disable cache");测试方法:打开新的 Tab 直接访问,就可以看到,使用的是缓存。
- 也可以使用内嵌元素,如 img,设置其 max-age,将其嵌套在另外一个页面进行测试,除非是 ctrl + F5 强制刷新,普通刷新会直接使用缓存。
- 设置缓存时长特别适用于静态资源,现在很少有直接使用原 URL 替换静态资源的,一般都是新建静态资源替换,那么就可以设置尽可能长的 max-age 来利用缓存策略。
- 对于非静态资源,这种方式无法让用户获得及时的更新,可设置
max-age=0
,即让客户端缓存,但每次都要请求服务端进行校验,校验通过,仅响应 304 即可,无需发送实体,减少数据传输。
缓存策略:Cache-Control & Pragma
用于 Response 首部
Cache-Control:no-store
客户端、代理服务器都不能缓存,设置该首部后,即使 Response 返回了指纹(如 Etag 或 Last-Modified),客户端也不缓存,下次请求也不会携带指纹。可用于随时会变动的页面,如首页、时间线页面等。
Cache-Control:no-cache
该名称非常具有迷惑性,其实际作用并不是不能缓存,而是客户端、代理服务器都可缓存,但每次都需要进行校验(前提是 Response 报文包含验证指纹),相当于Cache-Control:max-age=0
Cache-Control:private
最终客户端可缓存,但代理服务器不能缓存,另外,对于多用户浏览器、或多用户系统,不同用户之间也不能共用缓存。比如用在登录后才能看到的页面。
Cache-Control:public
客户端、代理服务器都可缓存,广泛用于静态资源(如图片等)
Cache-control: must-revalidate
基本相当于public
(都可缓存),但缓存过期后必须校验,且确保资源新鲜后才能返回内容;若校验失败,应返回 4xx 或 5xx。而public
则不然,比如碰到源服务器宕机,未能校验成功,有可能使用过期的缓存,代理服务器还应发送warning
报头提醒。
Cache-Control:proxy-revalidate
相当于代理服务器使用must-revalidate
策略,客户端使用public
策略
Cache-Control:no-transform
针对代理服务器,不能对缓存内容进行转换,比如为节省流量,有些代理服务器可能会对图像格式进行转换。
Cache-Control:immutable
针对客户端,比如上面举例中 刷新 或 ctrl+F5 强刷,设置该值是告诉浏览器即使在强刷情况下,也无需校验,仍使用未过期缓存。该值有兼容性问题,并不是所有浏览器都已实现,但对于确定永远不会发生变化的静态资源可返回该首部,对于不支持的浏览器也不会有什么副作用。还有两个试验性的,并发所有浏览器都支持。
Cache-Control:stale-while-revalidate=
当验证时 - 过期缓存有效。如果响应是一个较大的资源,可以使用该值。客户端在缓存过期时可直接使用过期缓存,异步获取新鲜资源。但如果过期时长超过了指定秒数,客户端不可直接使用过期资源,必须重新校验。
Cache-Control:stale-if-error=
当发生错误时 - 过期缓存有效。与上面的类似
- 若未设置,大部分浏览器会按照
private
来处理。- 以上值并不是互斥的,可设置多个,比如
Cache-Control: proxy-revalidate, no-transform
;- 另外,浏览器总是会倾向于更严格的缓存策略,比如
public, private
则使用private
;public, no-store
则使用no-store
;no-cache, max-age=100
则会自动忽略max-age
。- 对于不希望客户端缓存的,可设置为
Cache-Control: no-store, no-cache, must-revalidate
,这样可避免某一个值不被客户端支持,只要客户端支持任意一个值,浏览器就可以收到二次请求。
Cache-Control 也可用于 Request 报头,这些报头一般是针对代理服务器而言的,源服务器大部分情况下无需针对该报头做特殊处理,可用值如下:
Cache-Control:no-store
必须返回新鲜资源,即代理服务器、源服务器都不应该返回 304,而应该返回 200 并传输实体内容。
Cache-Control:no-cache
可以返回304,但代理服务器必须校验是否最新(源服务器无论有没有该报头总应该这么做)
Cache-Control:no-transform
告知代理服务器,不要转换格式
Cache-Control:only-if-cached
针对代理服务器,若已缓存,不要进行校验,请直接返回。看语义,若未缓存,应该返回 4xx 响应。应该很少有客户端有这么变态的要求,可能适合用在一些数据丢失,尝试从代理服务器恢复的场景。
Cache-Control:max-age=
代理服务器缓存时长若超过max-age
的话,请校验新鲜度。所以max-age=0
等价于no-cache
,必须进行新鲜度校验。
Cache-Control:min-fresh=
代理服务器的缓存剩余生命长度不得低于该秒数
Cache-Control:max-stale[=
客户端可接受过期缓存【若设置秒数,表示过期时长不能超过该秒数】]
Pragma 首部,可同时用于 Request 和 Response
这是一个 HTTP/1.0 版本时规定的首部,仅有一个no-cache
值 (RFC 7234),当与Cache-Control
同时出现时,后者优先Request: 请代理服务器返回新鲜资源,不要缓存
Pragma: no-cache
Response: 请客户端不要缓存资源
Pragma: no-cache
指纹验证:ETag / If-Match & If-None-Match
Response: 返回资源的标识符
Etag: "c0bea9ae76c87756d20d0dd9012f8a52"
(包括引号,强验证)
Etag: W/"0815"
(弱验证,W必须大写)强弱与否,对于客户端可能没有意义,只是方便服务端知道。以便在下次验证时做区分:对于强验证,哪怕一个字节发生变化,都应返回新资源;对于弱验证,只有主体内容变化,才需返回新资源(比如一个页面内容无变化,仅某一个广告发生变化,可能就无需重新返回)
Request: 若客户端缓存了上次 Response 资源,下次请求会携带
etag
,有两种携带方式
- 缓存验证(较为常用)
If-None-Match: "bfc13a64729c4290ef5b2c2730249c88ca92d82d"
If-None-Match: W/"67ab43", "54ed21", "7892dd"
If-None-Match: *
客户端携带之前缓存的标识符发送请求:
- 若服务端无法匹配标识符(None-Match = true),返回正常的 200 响应,并发送最新 Etag;
- 若够匹配到(None-Match = false),返回 304 (并携带原本可能出现在 200 响应中的首部:Cache-Control、Content-Location、Date、ETag、Expires 和 Vary ),无需发送实体,大大减少了数据传输
- 避免“空中碰撞”
If-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-Match: W/"67ab43", "54ed21", "7892dd"
If-Match: *
比如编辑页面,访问页面后获取到 etag,在更新时携带获取到的 etag:
- 若匹配成功(Match=true),说明该资源没有被其他操作二次改动过,更新成功,返回正常的 200、新的 etag;
- 若匹配失败(Match=false),说明在提交前已被其他用户修改,返回 412 响应
时间验证:Last-Modified / If-Modified-Since & If-Unmodified-Since
Response: 返回资源的最后修改时间
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
Request: 若客户端缓存了上次 Response 资源,下次请求可使用最后修改时间验证
缓存验证
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
若服务端文件修改过(Modified=true),返回新的资源内容,正常 200 响应;若未修改过(Modified=false),则返回 304 响应。避免“空中碰撞”
If-Unmodified-Since: Wed, 21 Oct 2015 07:28:00 GMT
更新内容时,需保证资源未修改(Unmodified=true),若验证通过,才会返回正常 200 响应,否则返回 412 响应。对比一下就会发现,Modified 与 etag 二者逻辑是完全相同的;实际上 Modified 首部出现较早 (HTTP/1.0),etag 首部出现较晚 (HTTP/1.1),是用来替代 Modified 的;显而易见,etag 验证更加稳定准确,毕竟修改时间的改变并不代表内容一定发生变化,所以 推荐使用 etag;若二者同时出现,etag 优先,当前所有浏览器都已支持 etag。
但如果需要兼容旧版本浏览器,则建议二者同时发送;如果想减少报文大小或服务端不方便计算 etag,也可以仅发送 Modified,实际上,即使在今天,仍有不少大公司仅发送 Modified 首部。
缓存条件:Vary
Response: 缓存条件:若服务端在 Request 报文不同时,返回的内容不同。必须在 Response 告知客户端下次请求,只有在全部或指定的 Request header 完全相同时,才能直接使用缓存,否则必须发送请求验证资源是否更新或直接重新请求资源。
Vary: *
Vary: User-Agent, Content-Type, ...
另外关于缓存的几点总结
- 服务端仅设置了 “验证首部(指纹或时间)”报文,客户端每次都会重新发送请求,服务端可返回 304 或 响应 200 重新发送资源实体。
- 服务端仅设置了 “缓存时长”,客户端在缓存到期后会重新发送请求,由于没有 “验证首部”,即使服务端资源没有任何变化,也无法验证、无法返回 304,只能响应 200,重新发送资源实体。
- 二者都设置了,客户端在缓存到期后会重新发送请求,服务端可返回 304 或 200。
私人信息:Cookie
Response: 设置cookie,可多次发送
Set-Cookie: sessionid=38afes7a8; HttpOnly; Path=/
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
Request: 携带已设置 cookie
Cookie: sessionid=38afes7a8; id=a3fWa
缓存清除:Clear-Site-Data
Response: 告知要清除的缓存,可参见:Clear-Site-Data
Clear-Site-Data: "cache", "cookies", "storage", "executionContexts"
Clear-Site-Data: "*"
五、内容安全
以下报文相对而言都比较新,有些甚至还尚在实验中,并不是所有浏览器都支持,并且有些不仅报文可以实现,也可以通过其他方式。关于安全性的问题都比较复杂,下面每一个首部可能都值得、或者说需要一篇单独的文章才能讲清楚,这里仅列一个大概
Resonse:
Content-Security-Policy
、Content-Security-Policy-Report-Only
用于降低 XSS 风险,也可使用 meta 方式,可 参见,特别适合 UGC 类型,比如 facebook、instagram 就设置了该报文Response:
X-XSS-Protection
另外一个降低 XSS 风险的报文:参见Response:
Cross-Origin-Opener-Policy
、Cross-Origin-Embedder-Policy
、Cross-Origin-Resource-Policy
用于降低跨域资源或钓鱼造成的攻击,可参考 XS-Leaks、跨域隔离、跨域策略Response:
Origin-Isolation
来源隔离机制: 参见Response:
Feature-Policy
在直接访问或 Iframe 访问时,启用/禁用 浏览器的一些特性:参见Response:
X-Content-Type-Options
nosniff
: 禁止浏览器对 css/js 进行自动嗅探,降低 XSS 风险:参见Response:
X-Frame-Options
确保网站没有被嵌入到别人的站点里面,可参考 Clickjacking攻防Response:
X-Download-Options: noope
针对 IE 浏览器的报文,下载文件的弹出框不显示“打开”选项 (仅保留“保存”选项),避免钓鱼攻击,也可通过 meta 设置,参见Response:
X-Permitted-Cross-Domain-Policies
针对crossdomain.xml
(允许跨域策略文件)的报文,主要是如 flash/Silverlight/Flex 等嵌入式旧技术,现在很少用的到,如有需要请自行找资料
六、客户端信息
以下为一些针对现代浏览器,尤其是为不同尺寸终端设计的报文,并未获得广泛良好的 支持,目前仍属于 草案,需谨慎依赖。(吐槽一下,google 对这种收集客户端信息的特别上心,参与提交草案,且 chrome 基本实现,苹果就比较反感这种有隐私顾虑的,估计 Safari 肯定不上心。另外 一例)
Response: 期望客户端请求携带的信息、以及该配置的有效时长
Accept-CH:
,如Accept-CH: DPR, Viewport-Width
Accept-CH-Lifetime:
, 如Accept-CH-Lifetime: 86400
Request: 根据服务端响应,携带其所需报文,这里列举一些,可能有会有更多
DPR: 1.0
客户端设备的像素比
Device-Memory: 1
客户端设备内存的近似大小
Viewport-Width: 667
布局视口宽度
Width: 240
物理像素宽度服务端可根据这些报文,针对不同的设备参数返回不同的响应,但大部分情况,在前端使用 JS 处理可能会更好一点。之所以设计出这种报文,可能是为了充分利用客户端缓存功能,以便尽可能减少流量传输。所以这里就不得不说一下,若出于此目的,切记 Response 使用 Via 指明缓存有效的报文字段,如:
Via: DPR, Viewport-Width
Request:客户端在网络状况不好或设备性能不足时,可使用该报文希望服务端发送简洁版本的响应,以减少加载时长、降低对设备的性能需求。
Save-Data: on
(需要) 或Save-Data: off
(不需要)
七、代理服务器相关
如果不是编写代理软件,以下报文只有 “Forwarded” 需要在应用程序中关心。其他报文通常是代理软件处理,如 Nginx / Apache 等网关应用、云厂商提供的负载均衡产品等。但了解这些报文,也能更好的使应用程序与代理软件交互。
Request & Response:
Cache-Control
有些值是专门针对代理服务器的,参见上面缓存策略章节说明Response: 报文的创建时间,已缓存时长,参见上面缓存时长章节说明
Date: Wed, 21 Oct 2015 07:28:00 GMT
Age: 300
Request & Response: 代理服务器信息,请求/响应中均可使用,可用于分析请求链、防止循环请求等
Via: [
如:"/" ] [ ":" ] Via: 1.1 cdn.com
/Via: HTTP/1.1 cdn.com:8080
或 (pseudonym 为内部代号)
Via: [
如:"/" ] Via: 1.1 Name
/Via: HTTP/1.1 Node
Response: 警告报文,返回给客户端或下一级代理服务器不新鲜的缓存资源,同时发出警告。
Warning:
[ ]
该首部可多次发送;也可用于 Request,但较为少见
Request: 代理服务器在收到客户端请求,向源服务器转发请求时,可能会丢失一些信息,所以一些应用程序或 CDN厂商 自创了一些 header 首部来拟补这些损失,目前有部分首部已纳入到协议标准。
Forwarded: by=
; for= ; host= ; proto= 但一些旧程序仍然在使用原来的首部,所以源服务端也应该认真对待
Request: 在进入该级代理服务器前,请求 ip 链,第一个即为实际客户端的 IP
X-Forwarded-For: 2001:db8:85a3:8d3:1319:8a2e:370:7348, 70.41.3.18, 150.172.238.178
(常见)
X-ProxyUser-Ip: ip, ip
Request: 客户端实际请求的 host,比如是 CDN 节点 HOST,源服务器自身的 HOST 一般与此不同。
X-Forwarded-Host: id42.cdn.com
Request: 客户端请求实际的 scheme,源服务器可能是 http 服务器,https 在代理服务器层完成.
X-Forwarded-Proto: https
(常见)
X-Forwarded-Protocol: https
X-Url-Scheme: https
X-Forwarded-Ssl: on
Front-End-Https: on
标准首部
Forwarded
其实就是合并版,其中for
格式如下,ipv6 会使用方括号括起来
Forwarded: for=192.0.2.43, for="[2001:db8:cafe::17]"; host=id42.cdn.com; proto=https
Forwarded
by
介绍 :The "by" parameter is used to disclose the interface where the request came in to the proxy server
,看样子好像是当前代理服务器的上一级请求的 IP (可能是客户端或代理服务器),这样子其实是与 for 重复了,该值的介绍有点模糊,根据实际情况而定。注意:由于该值十分容易伪造,所以对于源服务器而言,应该仅信任确定是代理服务器发送的值。
Request: 可以看到,代理服务器可能有多个,这对于客户端,就需要一直等候,所以这里有一个新 草案 报文,按照协议,每经一级代理服务器,该值就减小 1,当减小至 0 时,不应继续向上级请求,而是直接返回。另外有一个关于该报文的 问题
Max-Forwards:3
八、跨域请求
若是符合以下条件的简单请求:
- Method 为 : GET、POST、HEAD
- Request Header 为安全首部:如 Accept,Accept-Language 等
无论是否跨域,浏览器都将直接发送请求,根据返回的报文决定是拦截,还是将内容返回给请求方
Response: 允许跨域查询的域名,可使用 “*” 允许所有域名跨域
Access-Control-Allow-Origin:
/Access-Control-Allow-Origin: *
(指定域名需包括 scheme,如:Access-Control-Allow-Origin: https://domain.com
)Response: 是否允许 Request 包含认证信息,比如 cookie / authorization headers / TLS client certificates
Access-Control-Allow-Credentials: true
(如果 Response 包含该报文,Access-Control-Allow-Origin
不能使用 “*” 通配符,必须指定域名)
(如果 Response 不含该报文,而 Request 又发送了认证信息,浏览器会拦截响应,即客户端无法获得资源)Response: 允许暴漏给外部的响应首部,默认仅暴漏 简单首部
Access-Control-Expose-Headers:
, , ...
非简单请求,客户端会首先使用 OPTIONS Method 预检请求
Request: 告知即将使用何种 Method 发送请求
Access-Control-Request-Method: PUT
Request: 告知会携带的非安全 header 首部
Access-Control-Request-Headers:
, , ... Response: 返回允许的 Method
Access-Control-Allow-Methods:
, , ... Response: 返回允许 Request 使用的非安全 header 首部
Access-Control-Allow-Headers: *
或Access-Control-Allow-Headers:
[, ]* Response: 其他所需信息(参见上面简单请求的说明)
Access-Control-Allow-Origin
Access-Control-Allow-Credentials
Access-Control-Expose-Headers
Response: 预检配置的有效时长,即在指定秒数内,无需再次预检,直接使用本次返回的结果。
Access-Control-Max-Age: 86400
Request: 在使用 XMLHttpRequest 执行 Ajax 请求时,浏览器可能会携带该首部,但并不是一定,该首部并不属于协议,所以服务端不能武断的使用该首部判断是否为 Ajax 请求。
X-Requested-With: XMLHttpRequest
Request: “Sec-Fetch” 请求元数据系列,该首部由浏览器添加(仅在 HTTPS 下发送),代理服务器等不得更改,不属于协议,仍处于 实验阶段;
Sec-Fetch-Dest
/Sec-Fetch-Mode
/Sec-Fetch-Site
/Sec-Fetch-User
服务端可利用这些报文判断访问合法性,可参见:MDN、详解,但由于 HTTP 头可以伪造,跨域安全性问题不应依赖该报文。
九、HTTPS / WebSocket 等
HTTP 协议版本:从 HTTP的发展 来看,最主要的为 1.0 / 1.1 / 2。简单介绍下版本的选择机制:
- 客户端直接使用 HTTPS 请求,Nginx 等服务器软件可使用的 ALPN 协商让客户端直接使用 HTTP/2 协议,应用层无感。
- 若服务端 ALPN 协商告知不支持 HTTP/2 或 客户端使用 HTTP 请求:当下,浏览器都默认使用 HTTP / 1.1;但不排除有其他客户端会使用 HTTP/1.0
- 若客户端使用 HTTP/1.0 请求,则服务端应最好是仅发送 1.0 支持的首部(不支持的报文会被客户端忽略),且不应依赖在 1.1 才有的请求首部。主要区别:
Host
:Request 不发送 Host 首部,这个最蛋疼的一个问题。同一个 IP 可能会绑定多个域名,若 Request 不发送 Host,就没办法确定请求的是哪一个网站。所以如果需要支持这种请求,就需要给域名单独分配一个 IP,比如百度;好在这种请求现在越来越少了,对于不发送 Host 的请求,一般返回 400,比如知乎。Connection
/Keep-Alive
:1.0 不支持 TCP 复用,一次请求结束后立即关闭 TCP 通道,Request 不会发送该首部,Response 也不应返回该首部,所以 Response 也可以不发送Content-Length
首部。Accept-*
:1.0 还未引入协商机制,请求报文不会包括相关首部。但服务端应返回Content-*
首部告知格式、语言等,且需注意,1.0 虽支持消息压缩传输,但压缩算法 支持 与 1.1 并不同。Transfer-Encoding
:1.0 不支持分块传输Cache-Control
/ETag
:1.0 不支持这种缓存策略的报文,仅支持Pragma
/Last-Modified
- 最后:1.0 与 1.1 并不是完全隔绝的两种东西,事实上,在 1.1 标准确定之前,好多 1.0 客户端已会发送
Host
首部、支持复用 TCP(发送Connection
首部),所以服务端对于 1.0/1.1 请求以收到的首部为准。- 服务端的 Response 协议版本不应大于 Request 所使用的版本,否则客户端可能无法处理。
- ALPN 协商可以让客户端直接使用 HTTP/2,但其实对于 HTTP/1.1 请求,还可通过
Upgrade
首部协商升级(浏览器通常不会做,服务端对于这种请求一般是 301 到 支持 HTTP/2 的 https 地址以完成升级)。Request: 请求可使用 HTTP/1.1 连接,之后发送首部协商升级。该用法仅支持 1.1,1.0 还未设计该机制,2 不支持协商(用意应该是不能降级,后续版本的协商方式也不是通过首部)
Connection: Upgrade
Upgrade: h2c
HTTP Method
HTTP/1.0 协议只定义了 GET、HEAD、POST;其他的一些 Method 是在 1.1 甚至是补充协议中定义的。对于一些严格验证 Method 的服务端,比如一些 REST API 服务端,那么在无法发送 PUT 等 Method 的客户端通常会使用 POST 发送,但通过首部来指明 Method 语义,常用的有以下两个
X-METHOD-OVERRIDE: PUT
X-HTTP-METHOD-OVERRIDE: PUT
HTTP/2 相关
该版本进行了大量优化,但大部分细节都是在 TCP 通信部分,主要由诸如 Nginx 等服务器软件完成。对于网络应用程序而言,并无太大差异,但对于以前奉为圭臬的一些网络优化手段,则发生了较大变化,这里简单提一下:
- 域名散列:之前为了提高并发吞吐,会采用多域名策略,在 http/2 这样做,反而会降低性能。这主要是 http/2 使用了多路复用技术,域名少反而更有优势
- 资源合并:原因与上面相同,http/2 并不介意同一个页面请求更多资源,因为并不会创建更多的 TCP 通道,资源合并反而因为缓存更容易失效成为劣势,想像一下,任何一个小文件的改动都会导致整个合并资源需要更新。
- 资源内联:之前为了让页面加载即渲染,可能将 css 直接内置到页面中,但这样却无法让客户端缓存样式资源、也会让不同页面无法使用同一个资源缓存。
推荐方式
- 尽可能给响应添加 etag 报文,利用缓存
- 多域名解析到一个 ip,充分利用 http/2 的多路复用
- 多域名共用一个证书,减少证书验证的时间消耗
http/2 作为一个协议,本事是可以支持 HTTP 的,但浏览器厂商都没有去实现,仅支持 HTTPS。另外, http/2 也新增了一些功能:
Request & Response: 推送,该功能也是在 Nginx 层面完成的,只需要配置即可,以下两个报文仍处于 草案,如果不是服务器程序开发者,也无需关心。
Accept-Push-Policy
/Push-Policy
对于应用程序开发者而言,若不想在服务器软件内配置,Link
首部其实可以作为推送的代替品(参见最上面基础首部的介绍),但推送并不是一定有益,可参考 这篇 文章适场景而用。Response: 备选服务(草案),支持度 还不错
Alt-Svc:
; ma= ; persist=1, ; ma= ; persist=1
- 通过改报文可提供一个或多个备选服务,设置不同的通信协议 / 服务端IP,浏览器会缓存这些配置;
- 在
max-age
内,若服务端不可用,浏览器会尝试使用这些备选服务发出请求,比如 google 就设置了该报文。- 另外还有一个
persist
值,是否在网络发生变化(如:wifi 变 4g)时,仍保持备选服务的有效性,默认为否。- 想像一下,是不是可以用国内服务器给未备案的域名加速呢,感觉云厂商也会防堵这个。不过,该报文对于提高服务可用性还是用处不小的,可尝试一下。另外,该技术是不是可以在一定程度上预防 DNS 投毒。
Request: 若客户端使用备选服务发出请求,会携带该报文告知所选用的主机
Alt-Used: uri-host [ ":" port ]
HTTPS 相关:
互联网已经基本进入了 HTTPS 时代(无论是使用 HTTP/1.1 还是 HTTP/2,现在绝大多数网站都开启了 HTTPS),但由于要对旧世界兼容,所以仍然支持 HTTP。这对不少场景造成了一些问题,表现尤为严重就是流量劫持、中间人攻击,下面几个报文主要是为了降低这种问题带来的危害。Response: HSTS(HTTP Strict Transport Security,RFC6797),严格使用安全协议传输
Strict-Transport-Security: max-age=
; includeSubDomains; preload
- 启用 HTTPS 后,访问 HTTP 一般会 301 到 HTTPS,但在跳转前仍然为 HTTP 响应,面临着被劫持的风险。跳转后的 HTTPS 可发送该报头(该报头仅能用在 HTTPS 响应中),那么在
max-age
时间内,用户再次访问 HTTP,浏览器不会发送请求,而是直接转而请求 HTTPS。可选:includeSubDomains
,子域名是否也强制启用 https,启用该项,子域名首次被劫持风险也消除了。- 可以看到,使用该报文也无法避免首次被劫持的风险,所有 google 搞了一份清单,收录那些永久采用 HTTPS 的域名,这样首次访问也直接跳转,目前各浏览器厂商基本都支持了该清单。添加
preload
的意思就是同意加入该清单,但并不是添加了就一定会被收录,还有有一些其他 条件。在确定永久采用 HTTPS 前,不要添加该值,否则以后想使用 HTTP 时域名就废了,目前不晓得怎么从清单删除。Request: 客户端支持优先使用 HTTPS 加载资源
Upgrade-Insecure-Requests: 1
- HTTPS 响应页面中可能会包含 HTTP 资源。若客户端请求发送该报文,则表明客户端支持无痛替换页面内所有 HTTP 资源为 HTTPS 资源(是直接替换,不会访问 HTTP 了)。服务端可通过
Content-Security-Policy
首部告知客户端该如何处理(参阅“内容安全”章节,提到了该报文):block-all-mixed-content (不加载 HTTP 资源)
、upgrade-insecure-requests (升级 HTTP 为 HTTPS)
- 浏览器厂商除了避免服务端被劫持,也要为用户考虑,所以为了提高大家替换为 HTTPS 的积极性,如果 HTTPS 页面包含 HTTP 资源,会提醒用户该网站不安全(不同浏览器的提示方式也不尽相同)。如果是程序开发商,不知道程序被实际使用时是否会采用 HTTPS,内嵌资源可以使用无协议 URL ( //domian.com),浏览器会自动使用其所在页面的协议。否则就要排查一切非 HTTPS 资源,包括 css/js/图片/字体/异步接口、甚至是 form action.
- 思考题:有些 APP 会在手机内建一个
127.0.0.1
的内网 server,以便于自家的网络产品使用,那么在浏览器内访问该接口是否会触发不安全提示、该如何处理。Response: 发送证书透明度检测报告 (草案)
Expect-CT: max-age=
; enforce; report-uri=" "
- HTTPS 主要是为了加密传输,但也不是不可 解密。对于网络应用而言,避免中间人获取传输数据,最重要的就是保护好证书私钥。但这仍然不能保证完全安全,比如著名的 DigiNotar 事件,如果 CA 机构出现问题,对于使用其证书的公司很难第一时间得到预警,消除影响。
- 于是有了 certificate transparency 用于监测审计证书透明度,以便第一时间消除影响。该机构推进证书厂商支持 CT,好消息是现在几乎所有证书颁发机构,包括 Let's Encrypt 都已支持 CT。网站运维人员可以在 google、crt.sh 搜索查看证书的透明度报告。
- 由 Google 主导的 RFC 6962 则是为了更进一步,服务端可通过
report-uri
提供一个 URL,浏览器收到该报文后,会在每个max-age
时间后发送一份透明度检测报告到该 URL,以便管理员可以更及时应对。若指定为enforce
,浏览器会在不符合 CT 安全性要求的情况下拒绝建立连接(慎用)。Response: HPKP 指令报文
Public-Key-Pins
、Public-Key-Pins-Report-Only
这是 chrome 在 CT 规范之前尝试使用的一种证书验证方式,目前已移除 chrome,应该不会再有浏览器支持了,就不再细说了。
WebSocket 相关
还记得上面说的Upgrade
首部吧,该首部作用就是将 HTTP/1.1 升级为其他协议,浏览器利用该特性新增了 WebSocket 协议以支持双向通信。Request: 客户端发出请求
Connection: Upgrade
Upgrade: websocket
(通知服务端要升级为 websocket 协议)
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
(客户端标识)
Sec-WebSocket-Version: 13
(使用的 websocket 协议版本)
Sec-WebSocket-Protocol: soap, wamp
(不常用: 使用的 websocket 子协议)
Sec-WebSocket-Extensions: permessage-deflate
(不常用: 客户端支持的 websocket 扩展)
Origin: https://domian.com
(其他任意 HTTP 首部)Response: 服务端响应
Sec-WebSocket-Accept: s3p=
(由于 Sec-WebSocket-Key 计算的值,返回给客户端验证)以上便完成了 HTTP 到 WebSocket 的升级。当然,升级过程对客户端应用程序开发者而言一般是透明的,即无需了解具体细节,比如浏览器提供了 相关API,微信小程序提供的 相关API,只需要直接使用 API 操作即可,无需关心通信是如何建立的。
但如果是需要以程序方式使用客户端,或创建服务端,则需要自行处理协议,服务器软件一般不提供。可参考 RFC 6455 和 RFC 8441 标准,另外可以在 iana 查阅已注册的 websocket 子协议和扩展。具体的消息通信协议这里不做展开,网络上有很多资料。并且还有很多已经封装好、可以直接使用的 websocket 库,有兴趣的话可以自己写一个,其实并不难。
值得在这里一提的是:若想在不支持 websocket 的客户端完成双工通信,可考虑服务端提供一个 SSE 保持消息推送,客户端使用 AJAX 或 Fetch 等手段、通过 HTTP 协议向服务器发送信息。
Service Workers
一种可以构建离线应用的技术,可参考:渐进式 Web 应用、PWA 应用实战、Service Workers这里的篇幅不足以解释的更清楚,仅是为了列举一个在这种技术下的的首部
Response: 设置离线服务的控制范围
Service-Worker-Allowed:
DNS over HTTPS (DoH)
由 RFC 8484 发布标准,该技术与网络开发者没什么关系。仅针对客户端开发者,一般指浏览器,但也包括如 cURL 等软件,目的是作为 DNS 的一种补充,保护 DNS 查询时的用户隐私,但也会带来一些 风险,有兴趣的可以看一下。
其他 : 以下都不属于标准协议,列举几个有意思的
Response: 这是 WordPress 定义的一个首部,用于不同博客间的引用通知,如果是开发博客类程序,可了解一下,这里有一篇 文章 对此作了说明
X-Pingback:
Response: 针对搜索引擎一个首部,告知其索引规则,可 参阅
X-Robots-Tag: googlebot: nofollow
Response: 告知错误报告的发送地,也许可以用在自动测试的相关程序上,详情 参阅
NEL: { "report_to": "name_of_reporting_group", "max_age": 12345, "include_subdomains": false, "success_fraction": 0.0, "failure_fraction": 1.0 }
十、结尾
以上主要介绍 HTTP 报文、内容传输方面的,其实还有较为重要的 HTTP 请求方法 (扩展:IANA)和 HTTP 响应代码 (扩展:IANA),因为比较简单,就不再展开。
HTTP / 3 马上也要来了,好消息是对于网络应用而言,并无特别需要注意的地方,终于可以松一口气了。可以看到,HTTP 协议越来越复杂了,就在现在,还有各种 草案 在不断的提出、等候纳入标准,IANA 还整理一份 Header 清单;伴随着浏览器支持的接口越来越多,提供的基础能力越来越丰富,估计以后会越来越多。
不知道跟 google 有没有关系,反正感觉 Chrome 占据绝对份额后,WEB 发展就越来越快了。google 自然有绝对的利益驱动这件事,如果大家都不搞 APP,全部以 WEB 提供服务,google 恐怕做梦都会笑醒。但也要小心 Chrome 屠龙少年变恶龙,成为新时代的 IE6,比如 名场面,若是 google 无法推动某项技术成为标准,借助 Chrome 也能搞成既定标准,所以对于网络应用开发者,建议大家最好是尽可能让自己的产品兼容 Firefox 等其他浏览器。