阅读 85

漫谈跨域

浏览器的同源策略

同源策略是对 JavaScript 代码能够操作哪些 Web 内容的一条完整的安全限制。脚本只能读取和所属文档来源相同的窗口和文档的属性。

同源策略要求协议、主机以及载入文档的 URL 端口相同。

  • 从不同 Web 服务器载入的文档具有不同的来源
  • 通过同一主机的不同端口载入的文档具有不同的来源

脚本本身的来源和同源策略并不相关,相关的是脚本所嵌入的文档的来源。

例如:假设一个来自主机 A 的脚本被包含到(使用 <script> 标记的 src 属性)宿主 B 的一个 Web 页面中。这个脚本的来源是主机 B,并且可以完整地访问包含它的文档的内容。如果脚本打开一个新窗口并载入来自主机 B 的另一个文档,脚本对这个文档的内容也具有完全的访问权限。但是,如果脚本打开第三个窗口并载入一个来自主机 C 的文档(或者是来自主机 A),同源策略就会发挥作用,阻止脚本访问这个文档。

同源策略还应用于使用 XMLHttpRequest 生成的 HTTP 请求。这个对象允许客户端 JavaScript 生成任意的 HTTP 请求到脚本所属的文档的 Web 服务器,但是不允许脚本和其它 Web 服务器通信。

不严格的同源策略

通过 document.domain 实现不严格的同源策略

同源策略给那些使用多个子域的大站点带来了问题。为了支持这种多子域的站点,可以使用 Document 对象的 domain 属性。

在默认情况下,属性 domain 存放的是载入文档的服务器的主机名,可以设置这一属性,不过使用的字符串必须具有有效的前缀或它本身,另外,domain 值中必须有一个点号,不能把它设置为 com 或其他顶级域名。

如果两个窗口(或窗体)包含的脚本把 domain 设置成了相同的值,这些文档就有了同源性,可以互相读取属性。

CORS(Cross-Origin Resource Sharing,跨域资源共享)

跨域资源共享标准新增了一组 HTTP 头部字段,允许服务器用头信息显式地列出源来声明哪些源站通过浏览器有权限访问哪些资源,或使用通配符来匹配所有的源并允许由任何地址请求资源。

另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法,浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务器是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

简单请求

某些请求不会触发 CORS 预检请求,这样的请求属于“简单请求”,如果请求满足以下所有条件,就可以视为简单请求。

  • 使用下列方法之一:
    • GET
    • POST
    • HEAD
  • Fetch 规范定义了对 CORS 安全的头部字段集合,不得人为设置该集合之外的其他头部字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • Content-Type 值仅限于下列三者之一:
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
  • 请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器,XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问
  • 请求中没有使用 ReadableStream 对象
预检请求

需要预检的请求必须首先使用 OPTIONS 方法发起一个预检请求到服务器,以获知服务器是否允许该实际请求。预检请求的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

当请求满足下述任一条件时,应首先发送预检请求:

  • 使用了下面任一 HTTP 方法:
    • PUT
    • DELETE
    • CONNECT
    • OPTIONS
    • TRACE
    • PATCH
  • 人为设置了对 CORS 安全的头部字段集合之外的其他头部字段:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type
    • DPR
    • Downlink
    • Save-Data
    • Viewport-Width
    • Width
  • Content-Type 的值不属于下列之一:
    • text/plain
    • multipart/form-data
    • application/x-www-form-urlencoded
  • 请求中的 XMLHttpRequestUpload 对象注册了任意多个事件监听器
  • 请求中使用了 ReadableStream 对象
携带身份凭证的请求

Fetch 和 CORS 的一个有趣的特性是,可以基于 HTTP Cookies 和 HTTP 认证信息发送身份凭证。一般来说,对于跨域 XMLHttpRequest 或 Fetch 请求,浏览器不会发送身份凭证信息。如果要发送凭证信息,需要设置 XMLHttpRequest 的某个特殊标志位。

var invocation = new XMLHttpRequest()
var url = 'http://bar.other/resources/credentialed-content/'

function callOtherDomain () {
    if (invocation) {
        invocation.open('GET', url, true)
        invocation.withCredentials = true
        invocation.onreadystatechange = handler
        invocation.send()
    }
}
复制代码

代码将 XMLHttpRequest 的 withCredentials 标志设置为 true,从而向服务器发送 Cookies。因为这是一个简单 GET 请求,所以浏览器不会对其发起预检请求。但是,如果服务器端的响应中未携带 Access-Control-Allow-Credentials: true,浏览器将不会把响应内容返回给请求的发送者。

需要注意的是,对于附带身份凭证的请求,服务器不得设置 Access-Control-Allow-Origin 的值为 *。另外,响应头部中也携带了 Set-Cookie 字段,尝试对 Cookie 进行修改。如果操作失败,将会抛出异常。

HTTP 请求头部字段

下文列出了可用于发起跨域请求的头部字段。请注意,这些头部字段无须手动设置。当开发者使用 XMLHttpRequest 对象发起跨域请求时,它们已经被设置就绪。

  • Origin:预检请求或实际请求的源站,不管是否为跨域请求,Origin 字段总是被发送
  • Access-Control-Request-Method:用于预检请求。作用是,将实际请求所使用的 HTTP 方法告诉服务器
  • Access-Control-Request-Headers:用于预检请求。作用是,将实际请求所携带的头部字段告诉服务器
HTTP 响应头部字段
  • Access-Control-Allow-Origin: | *:origin 参数的值指定了允许访问该资源的外域 URI。对于不需要携带身份凭证的请求,服务器可以指定该字段的值为通配符,表示允许来自所有域的请求。如果服务端指定了具体的域名而非 *,那么响应头部中的 Vary 字段的值必须包含 Origin。这将告诉客户端:服务器对不同的源站返回不同的内容
  • Access-Control-Expose-Headers:在跨域访问时,XMLHttpRequest 对象的 getResponseHeader() 方法只能拿到一些最基本的响应头,Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma,如果要访问其他头,则需要服务器设置本响应头
  • Access-Control-Max-Age:指定了preflight请求的结果能够被缓存多久
  • Access-Control-Allow-Credentials:指定了当浏览器的 credentials 设置为 true 时是否允许浏览器读取 response 的内容。当用在对 preflight 预检测请求的响应中时,它指定了实际的请求是否可以使用 credentials。请注意:简单 GET 请求不会被预检,如果对此类请求的响应中不包含该字段,这个响应将被忽略掉,并且浏览器也不会将相应内容返回给网页
  • Access-Control-Allow-Methods:用于预检请求的响应。指明了实际请求所允许使用的 HTTP 方法
  • Access-Control-Allow-Headers:用于预检请求的响应。指明了实际请求中允许携带的头部字段

CDM(Cross-Document Messaging,跨文档消息)

允许来自一个文档的脚本可以传递文本消息到另一个文档里的脚本,而不管脚本的来源是否相同。

调用 Window 对象上的 postMessage() 方法,可以异步传递消息事件(可以用 onmessage 事件句处理程序函数来处理它)到窗口的文档里。

一个文档里的脚本还是不能调用在其他文档里的方法和读取属性,但它们可以用这种消息传递技术来实现安全的通信。

衍生

iframe

为了把 iframe 作为传输协议使用,脚本首先要把发送给 Web 服务器的信息编码到 URL 中,然后设置 <iframe> 的 src 属性为该 URL。服务器能创建一个包含响应内容的 HTML 文档,并把它返回给 Web 浏览器,并且在 <iframe> 中显示它。脚本能通过遍历 <iframe> 的文档对象来读取服务端的响应,但是这种访问受限于同源策略。

JSONP

使用 <script> 元素作为传输的技术成为 JSONP,如果 HTTP 请求所得到的相应数据是经过 JSON 编码的,则适合使用该技术。

网页通过添加一个 <script> 元素,将请求放在 src 中,浏览器就会向服务器发送一个 HTTP 请求以下载 src 属性所指向的 URL,服务器接收到请求后,将数据放在一个执行名字的回调函数中传回来。

使用 <script> 元素进行传输的一个主要原因是,它不受同源策略的影响,因此可以使用它们从其它服务器请求数据。第二个原因是包含 JSON 编码的数据的响应体会自动解码。

需要注意的是,这种方式普遍用于可信的第三方脚本。

关注下面的标签,发现更多相似文章
评论