HTTP 响应标头
下面会列出一些服务器跨域共享规范定义的 HTTP 标头,上面简单概述了一下,现在一起来认识一下,主要会介绍下面这些
- Access-Control-Allow-Origin
- Access-Control-Allow-Credentials
- Access-Control-Allow-Headers
- Access-Control-Allow-Methods
- Access-Control-Expose-Headers
- Access-Control-Max-Age
- Access-Control-Request-Headers
- Access-Control-Request-Method
- Origin
Access-Control-Allow-Origin
Access-Control-Allow-Origin
是 HTTP 响应标头,指示响应是否能够和给定的源共享资源。Access-Control-Allow-Origin 指定单个资源会告诉浏览器允许指定来源访问资源。对于没有凭据的请求 *
通配符,告诉浏览器允许任何源访问资源。
例如,如果要允许源 https://mozilla.org
的代码访问资源,可以使用如下的指定方式
Access-Control-Allow-Origin: https://mozilla.org Vary: Origin
如果服务器指定单个来源而不是*
通配符,则服务器还应在 Vary 响应标头中包含该来源。
Access-Control-Allow-Credentials
Access-Control-Allow-Credentials
是 HTTP 的响应标头,这个标头告诉浏览器,当包含凭证请求(Request.credentials)时是否将响应公开给前端 JavaScript 代码。
这时候你会问到 Request.credentials
是什么玩意?不要着急,来给你看一下,首先来看 Request 是什么玩意,
实际上,Request 是 Fetch API 的一类接口代表着资源请求。一般创建 Request 对象有两种方式
- 使用 Request() 构造函数创建一个 Request 对象
- 还可以通过 FetchEvent.request api 操作来创建
再来说下 Request.credentials 是什么意思,Request 接口的凭据只读属性指示在跨域请求的情况下,用户代理是否应从其他域发送 cookie。(其他 Request 对象的方法详见 https://developer.mozilla.org/en-US/docs/Web/API/Request)
当发送的是凭证模式的请求包含 (Request.credentials)时,如果 Access-Control-Allow-Credentials 值为 true,浏览器将仅向前端 JavaScript 代码公开响应。
Access-Control-Allow-Credentials: true
凭证一般包括 cookie、认证头和 TLS 客户端证书
当用作对预检请求响应的一部分时,这表明是否可以使用凭据发出实际请求。注意简单的
GET
请求不会进行预检。
可以参考一个实际的例子 https://www.jianshu.com/p/ea485e5665b3
Access-Control-Allow-Headers
Access-Control-Allow-Headers
是一个响应标头,这个标头用来响应预检请求,它发出实际请求时可以使用哪些HTTP标头。
示例
- 自定义标头
这是 Access-Control-Allow-Headers 标头的示例。它表明除了像 CROS 安全列出的请求标头外,对服务器的 CROS 请求还支持名为 X-Custom-Header
的自定义标头。
Access-Control-Allow-Headers: X-Custom-Header
- 多个标头
这个例子展示了 Access-Control-Allow-Headers 如何使用多个标头
Access-Control-Allow-Headers: X-Custom-Header, Upgrade-Insecure-Requests
- 绕过其他限制
尽管始终允许使用 CORS 安全列出的请求标头,并且通常不需要在 Access-Control-Allow-Headers 中列出这些标头,但是无论如何列出它们都将绕开适用的其他限制。
Access-Control-Allow-Headers: Accept
这里你可能会有疑问,哪些是 CORS 列出的安全标头?(别嫌累,就是这么麻烦)
有下面这些 Accep、Accept-Language、Content-Language、Content-Type ,当且仅当包含这些标头时,无需在 CORS 上下文中发送预检请求。
Access-Control-Allow-Methods
Access-Control-Allow-Methods
也是响应标头,它指定了哪些访问资源的方法可以使用预检请求。例如
Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Methods: *
Access-Control-Expose-Headers
Access-Control-Expose-Headers 响应标头表明哪些标头可以作为响应的一部分公开。默认情况下,仅公开6个CORS安全列出的响应标头,分别是
- Cache-Control
- Content-Language
- Content-Type
- Expires
- Last-Modified
- Pragma
如果希望客户端能够访问其他标头,则必须使用 Access-Control-Expose-Headers 标头列出它们。下面是示例
要公开非 CORS 安全列出的请求标头,可以像如下这样指定
Access-Control-Expose-Headers: Content-Length
要另外公开自定义标头,例如 X-Kuma-Revision,可以指定多个标头,并用逗号分隔
Access-Control-Expose-Headers: Content-Length, X-Kuma-Revision
在不是凭证请求中,你还可以使用通配符
Access-Control-Expose-Headers: *
但是,这不会通配 Authorization
标头,因此如果需要公开它,则需要明确列出
Access-Control-Expose-Headers: *, Authorization
Access-Control-Max-Age
Access-Control-Max-Age 响应头表示预检请求的结果可以缓存多长时间,例如
Access-Control-Max-Age: 600
表示预检请求可以缓存10分钟
Access-Control-Request-Headers
浏览器在发出预检请求时使用 Access-Control-Request-Headers 请求标头,使服务器知道在发出实际请求时客户端可能发送的 HTTP 标头。
Access-Control-Request-Headers: X-PINGOTHER, Content-Type
Access-Control-Request-Method
同样的,Access-Control-Request-Method 响应标头告诉服务器发出预检请求时将使用那种 HTTP 方法。此标头是必需的,因为预检请求始终是 OPTIONS,并且使用的方法与实际请求不同。
Access-Control-Request-Method: POST
Origin
Origin 请求标头表明匹配的来源,它不包含任何信息,仅仅包含服务器名称,它与 CORS 请求以及 POST 请求一起发送,它类似于 Referer
标头,但与此标头不同,它没有公开整个路径。例如
Origin: https://developer.mozilla.org
HTTP 条件请求
HTTP 具有条件请求的概念,通过比较资源更新生成的值与验证器的值进行比较,来确定资源是否进行过更新。这样的请求对于验证缓存的内容、条件请求、验证资源的完整性来说非常重要。
原则
HTTP 条件请求是根据特定标头的值执行不同的请求,这些标头定义了一个前提条件,如果前提条件匹配或不匹配,则请求的结果将有所不同。
- 对于
安全
的方法,像是GET
、用于请求文档的资源,仅当条件请求的条件满足时发回文档资源,所以,这种方式可以节约带宽。
什么是安全的方法,对于 HTTP 来说,安全的方法是不会改变服务器状态的方法,换句话说,如果方法只是只读操作,那么它肯定是安全的方法,比如说 GET 请求,它肯定是安全的方法,因为它只是请求资源。几种常见的方法肯定是安全的,它们是 GET、HEAD和 OPTIONS。所有安全的方法都是
幂等的
(这他妈幂等又是啥意思?)但不是所有幂等的方法都是安全的,例如 PUT 和 DELETE 都是幂等的,但不安全。幂等性:如果相同的客户端发起一次或者多次 HTTP 请求会得到相同的结果,则说明 HTTP 是幂等的。(我们这次不深究幂等性)
- 对于
非安全
的方法,像是 PUT,只有原始文档与服务器上存储的资源相同时,才可以使用条件请求来传输文档。(PUT 方法通常用来传输文件,就像 FTP 协议的文件上传一样)
验证
所有的条件请求都会尝试检查服务器上存储的资源是否与某个特定版本的资源相匹配。为了满足这种情况,条件请求需要指示资源的版本。由于无法和整个文件逐个字符进行比较,因此需要把整个文件描绘成一个值,然后把此值和服务器上的资源进行比较,这种方式称为比较器,比较器有两个条件
- 文档的最后修改日期
- 一个不透明的字符串,用于唯一标识每个版本,称为实体标签或
Etag
。
比较两个资源是否时相同的版本有些复杂,根据上下文,有两种相等性检查
- 当期望的是字节对字节进行比较时,例如在恢复下载时,使用
强 Etag
进行验证 - 当用户代理需要比较两个资源是否具有相同的内容时,使用
若 Etag
进行验证
HTTP 协议默认使用 强验证
,它指定何时进行弱验证
强验证
强验证保证的是字节
级别的验证,严格的验证非常严格,可能在服务器级别难以保证,但是它能够保证任何时候都不会丢失数据,但这种验证丢失性能。
要使用 Last-Modified
很难实现强验证,通常,这是通过使用带有资源的 MD5 哈希值的 Etag
来完成的。
弱验证
弱验证不同于强验证,因为如果内容相等,它将认为文档的两个版本相同,例如,一个页面与另一个页面的不同之处仅在于页脚的日期不同,因此该页面被认为与其他页面相同。而使用强验证时则被认为这两个版本是不同的。构建一个若验证的 Etag 系统可能会非常复杂,因为这需要了解每个页面元素的重要性,但是对于优化缓存性能非常有用。
下面介绍一下 Etag 如何实现强弱验证。
Etag 响应头是特定版本
的标识,它能够使缓存变得更高效并能够节省带宽,因为如果缓存内容未发生变更,Web 服务器则不需要重新发送完整的响应。除此之外,Etag 能够防止资源同时更新互相覆盖。
如果给定 URL 上的资源发生变更,必须生成一个新的 Etag
值,通过比较它们可以确定资源的两个表示形式是否相同。
Etag 值有两种,一种是强 Etag,一种是弱 Etag;
- 强 Etag 值,无论实体发生多么细微的变化都会改变其值,一般的表示如下
Etag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
- 弱 Etag 值,弱 Etag 值只用于提示资源是否相同。只有资源发生了根本改变,产生差异时才会改变 Etag 值。这时,会在字段值最开始处附加 W/。
Etag: W/"0815"
下面就来具体探讨一下条件请求的标头和 Etag 的关系
条件请求
条件请求主要包含的标头如下
- If-Match
- If-None-Match
- If-Modified-Since
- If-Unmodified-Since
- If-Range
If-Match
对于 GET
和 POST
方法,服务器仅在与列出的 Etag(响应标头)
之一匹配时才返回请求的资源。这里又多了一个新词 Etag
,我们稍后再说 Etag 的用法。对于像是 PUT
和其他非安全的方法,在这种情况下,它仅仅将上传资源。
下面是两种常见的案例
- 对于
GET
和POST
方法,会结合使用Range
标头,它可以确保新发送请求的范围与上一个请求的资源相同,如果不匹配的话,会返回416
响应。 - 对于其他方法,特别是
PUT
方法,If-Match
可以防止丢失更新,服务器会比对 If-Match 的字段值和资源的 Etag 值,仅当两者一致时,才会执行请求。反之,则返回状态码 412 Precondition Failed 的响应。例如
If-Match: "bfc13a64729c4290ef5b2c2730249c88ca92d82d" If-Match: *
If-None-Match
条件请求,它与 If-Match
的作用相反,仅当 If-None-Match
的字段值与 Etag
值不一致时,可处理该请求。对于GET
和 HEAD
,仅当服务器没有与给定资源匹配的 Etag
时,服务器将返回 200 OK
作为响应。对于其他方法,仅当最终现有资源的 Etag 与列出的任何值都不匹配时,才会处理请求。
当 GET
和 POST
发送的 If-None-Match
与 Etag
匹配时,服务器会返回 304
。
If-None-Match: "bfc13a64729c4290ef5b2c2730249c88ca92d82d" If-None-Match: W/"67ab43", "54ed21", "7892dd" If-None-Match: *
If-Modified-Since
If-Modified-Since
是 HTTP 条件请求的一部分,只有在给定日期之后,服务端修改了请求所需要的资源,才会返回 200 OK 的响应。如果在给定日期之后,服务端没有修改内容,响应会返回 304
并且不带任何响应体。If-Modified-Since 只能使用 GET
和 HEAD
请求。
If-Modified-Since 与 If-None-Match 结合使用时,它将被忽略,除非服务器不支持 If-None-Match。一般表示如下
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
注意:这是格林威治标准时间。HTTP 日期始终以格林尼治标准时间表示,而不是本地时间。
If-Range
If-Range
也是条件请求,如果满足条件(If-Range 的值和 Etag 值或者更新的日期时间一致),则会发出范围请求,否则将会返回全部资源。它的一般表示如下
If-Range: Wed, 21 Oct 2015 07:28:00 GMT If-Range: bfc13a64729c4290ef5b2c2730249c88ca92d82d
If-Unmodified-Since
If-Unmodified-Since
HTTP 请求标头也是一个条件请求,服务器只有在给定日期之后没有对其进行修改时,服务器才返回请求资源。如果在指定日期时间后发生了更新,则以状态码 412 Precondition Failed
作为响应返回。
If-Unmodified-Since: Wed, 21 Oct 2015 07:28:00 GMT
条件请求示例
缓存更新
条件请求最常见的示例就是更新缓存,如果缓存是空或没有缓存,则以200 OK
的状态发送回请求的资源。如下图所示
客户端第一次发送请求没有,缓存为空并且没有条件请求,服务器在收到客户端请求后,设置验证器 Last-Modified
和 Etag
标签,并把这两个标签随着响应一起发送回客户端。
下一次客户端再发送相同的请求后,会直接从缓存中提取,只要缓存没有过期,就不会有任何新的请求到达服务器重新下载资源。但是,一旦缓存过期,客户端不会直接使用缓存的值,而是发出条件请求。验证器的值用作 If-Modified-Since
和If-Match
标头的参数。
缓存过期后客户端重新发起请求,服务器收到请求后发现如果资源没有更改,服务器会发回 304 Not Modified
响应,这使缓存再次刷新,并让客户端使用缓存的资源。尽管有一个响应/请求往返消耗一些资源,但是这比再次通过有线传输整个资源更有效。
如果资源已经发生更改,则服务器仅使用新版本的资源返回 200 OK 响应,就像没有条件请求,并且客户端会重新使用新的资源,从这个角度来讲,缓存是条件请求的前置条件。
断点续传
HTTP 可以支持文件的部分下载,通过保留已获得的信息,此功能允许恢复先前的操作,从而节省带宽和时间。
支持断点续传的服务器通过发送 Accept-Ranges
标头广播此消息,一旦发生这种情况,客户端可以通过发送缺少范围的 Ranges
标头来恢复下载
这里你可能有疑问 Ranges
和 Content-Range
是什么,来解释一下
Range
Range
HTTP 请求标头指示服务器应返回文档指定部分的资源,可以一次请求一个 Range 来返回多个部分,服务器会将这些资源返回各个文档中。如果服务器成功返回,那么将返回 206 响应;如果 Range 范围无效,服务器返回416 Range Not Satisfiable
错误;服务器还可以忽略 Range 标头,并且返回 200 作为响应。
Range: bytes=200-1000, 2000-6576, 19000-
还有一种表示是
Range: bytes=0-499, -500
它们分别表示请求前500个字节和最后500个字节,如果范围重叠,则服务器可能会拒绝该请求。
Content-Range
HTTP 的 Content-Range 响应标头是针对范围请求而设定的,返回响应时使用首部字段 Content-Range
,能够告知客户端响应实体的哪部分是符合客户端请求的,字段以字节为单位。它的一般表示如下
Content-Range: bytes 200-1000/67589
上段代码表示从所有 67589
个字节中返回 200-1000
个字节的内容
那么上面的 Content-Range
你也应该知道是什么意思了
断点续传
的原理比较简单,但是这种方式存在潜在的问题:如果在两次下载资源的期间进行了资源更新,那么获得的范围将对应于资源的两个不同版本,并且最终文档将被破坏。
为了阻止这种情况的出现,就会使用条件请求
。对于范围来说,有两种方法可以做到这一点。一种方法是使用 If-Modified-Since
和If-Match
,如果前提条件失败,服务器将返回错误;然后客户端从头开始重新下载。
即使此方法有效,当文档资源发生改变时,它也会添加额外的 响应/请求
交换。这会降低性能,并且 HTTP 具有特定的标头来避免这种情况 If-Range
。
该解决方案效率更高,但灵活性稍差一些,因为在这种情况下只能使用一个 Etag。
通过乐观锁避免丢失更新
Web 应用程序中最普遍的操作是资源更新。这在任何文件系统或应用程序中都很常见,但是任何允许存储远程资源的应用程序都需要这种机制。
使用 put
方法,你可以实现这一点,客户端首先读取原始文件对其进行修改,然后把它们发送到服务器。
上面这种请求响应存在问题,一旦考虑到并发性,事情就会变得不准确。当客户端在本地修改资源打算重新发送之前,第二个客户端可以获取相同的资源并对资源进行修改操作,这样就会造成问题。当它们重新发送请求到服务器时,第一个客户端所做的修改将被第二次客户端的修改所覆盖,因为第二次客户端修改并不知道第一次客户端正在修改。资源提交并更新的一方不会传达给另外一方,所以要保留哪个客户的更改,将随着他们提交的速度而变化;这取决于客户端,服务器的性能,甚至取决于人工在客户端编辑文档的性能。例如下面这个流程
如果没有两个用户同时操作服务器,也就不存在这个问题。但是,现实情况是不可能只有单个用户出现的,所以为了规避或者避免这个问题,我们希望客户端资源在更新时进行提示或者修改被拒绝时收到通知。
条件请求允许实现乐观锁算法。这个概念是允许所有的客户端获取资源的副本,然后让他们在本地修改资源,并成功通过允许第一个客户端提交更新来控制并发,基于此服务端的后面版本的更新都将被拒绝。
这是使用 If-Match
或 If-Unmodified-Since
标头实现的。如果 Etag 与原始文件不匹配,或者自获取以来已对文件进行了修改,则更改为拒绝更新,并显示412 Precondition Failed
错误。
HTTP Cookies
HTTP 协议中的 Cookie 包括 Web Cookie
和浏览器 Cookie
,它是服务器发送到 Web 浏览器的一小块数据。服务器发送到浏览器的 Cookie,浏览器会进行存储,并与下一个请求一起发送到服务器。通常,它用于判断两个请求是否来自于同一个浏览器,例如用户保持登录状态。
HTTP Cookie 机制是 HTTP 协议无状态的一种补充和改良
Cookie 主要用于下面三个目的
会话管理
登陆、购物车、游戏得分或者服务器应该记住的其他内容
个性化
用户偏好、主题或者其他设置
追踪
记录和分析用户行为
Cookie 曾经用于一般的客户端存储。虽然这是合法的,因为它们是在客户端上存储数据的唯一方法,但如今建议使用现代存储 API。Cookie 随每个请求一起发送,因此它们可能会降低性能(尤其是对于移动数据连接而言)。客户端存储的现代 API 是 Web 存储 API(localStorage 和 sessionStorage)和 IndexedDB。
创建 Cookie
当接收到客户端发出的 HTTP 请求时,服务器可以发送带有响应的 Set-Cookie
标头,Cookie 通常由浏览器存储,然后将 Cookie 与 HTTP 标头一同向服务器发出请求。可以指定到期日期或持续时间,之后将不再发送Cookie。此外,可以设置对特定域和路径的限制,从而限制 cookie 的发送位置。
Set-Cookie 和 Cookie 标头
Set-Cookie
HTTP 响应标头将 cookie 从服务器发送到用户代理。下面是一个发送 Cookie 的例子
HTTP/2.0 200 OK Content-type: text/html Set-Cookie: yummy_cookie=choco Set-Cookie: tasty_cookie=strawberry [page content]
此标头告诉客户端存储 Cookie
现在,随着对服务器的每个新请求,浏览器将使用 Cookie 头将所有以前存储的 cookie 发送回服务器。
GET /sample_page.html HTTP/2.0 Host: www.example.org Cookie: yummy_cookie=choco; tasty_cookie=strawberry
Cookie 主要分为三类,它们是 会话Cookie
、永久Cookie
和 Cookie的 Secure 和 HttpOnly 标记
,下面依次来介绍一下
会话 Cookies
上面的示例创建的是会话 Cookie ,会话 Cookie 有个特征,客户端关闭时 Cookie 会删除,因为它没有指定Expires 或 Max-Age 指令。这两个指令你看到这里应该比较熟悉了。
但是,Web 浏览器可能会使用会话还原,这会使大多数会话 Cookie 保持永久状态,就像从未关闭过浏览器一样
永久性 Cookies
永久性 Cookie 不会在客户端关闭时过期,而是在特定日期(Expires)或特定时间长度(Max-Age)外过期。例如
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
Cookie的 Secure 和 HttpOnly 标记
安全的 Cookie 需要经过 HTTPS 协议通过加密的方式发送到服务器。即使是安全的,也不应该将敏感信息存储在cookie 中,因为它们本质上是不安全的,并且此标志不能提供真正的保护。
HttpOnly 的作用
- 会话 cookie 中缺少 HttpOnly 属性会导致攻击者可以通过程序(JS脚本、Applet等)获取到用户的 cookie 信息,造成用户cookie 信息泄露,增加攻击者的跨站脚本攻击威胁。
- HttpOnly 是微软对 cookie 做的扩展,该值指定 cookie 是否可通过客户端脚本访问。
- 如果在 Cookie 中没有设置 HttpOnly 属性为 true,可能导致 Cookie 被窃取。窃取的 Cookie 可以包含标识站点用户的敏感信息,如 ASP.NET 会话 ID 或 Forms 身份验证票证,攻击者可以重播窃取的 Cookie,以便伪装成用户或获取敏感信息,进行跨站脚本攻击等。
Cookie 的作用域
Domain
和 Path
标识定义了 Cookie 的作用域:即 Cookie 应该发送给哪些 URL。
Domain
标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前主机(不包含子域名)。如果指定了Domain
,则一般包含子域名。
例如,如果设置 Domain=mozilla.org
,则 Cookie 也包含在子域名中(如developer.mozilla.org
)。
例如,设置 Path=/docs
,则以下地址都会匹配:
/docs
/docs/Web/
/docs/Web/HTTP