web开发的跨域问题详解

做过 web 开发的同学,应该都遇到过跨域的问题,当我们从一个域名向另一个域名发送 Ajax 请求的时候,打开浏览器控制台就会看到跨域错误,今天我们就来聊聊跨域的问题。

1. 浏览器的同源策略

同源的定义是:如果两个页面的协议端口(如果有指定)和域名都相同,则两个页面具有相同的。同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。

2. 跨域错误信息产生的原因

为了说明问题,我们可以做如下实验,我们在本地搭建了开发环境, 由客户端 http://localhost:3001 向服务器 http://localhost:3000 发送两个请求,一个使用 javascript 异步请求数据,另一个使用 img 标签请求数据,服务器收到请求后,打印接收到请求的日志,如下图所示:

img客户端发送两个请求

img服务端打印日志并处理请求

代开客户端浏览器的控制台,可以看到发出了两个请求,并且都收到了状态码为 200 的响应,同时控制台报了一个错误,即 xhr 请求报错。由此我们可以知道,之所以产生跨域错误信息,原因有以下三条:

  • 浏览器端的限制(服务端收到了请求并正确返回)
  • 发送的是 XMLHttpRequest 请求(使用 img 标签发送的请求为 json 类型,并不会报错)
  • 请求了不同域的资源

只有同时满足了这三个条件,浏览器才会产生跨域错误。

3. 解决跨域的思路

既然我们知道了跨域错误产生的原因,那么解决思路就很直观了,针对出错的三个原因进行相应的处理即可,相应的解决思路也有三个方向:

  • 打破浏览器的限制
  • 不发送 XHR 请求
  • 解决跨域

下文将分别进行阐述。

3.1 打破浏览器的限制

由上面分析结论可知,之所以出现跨域的错误,实际上是客户端浏览器所做的限制,服务器并未进行限制,因此我们可以通过设置浏览器,使其不进行跨域检查。实际上浏览器也提供了对应的设置选项。

以 MacOS 下的 Chrome 浏览器为例,在终端中使用命令

open -n /Applications/Google\ Chrome.app/ --args --disable-web-security  --user-data-dir=/Users/your-computer-account/MyChromeDevUserData/

打开浏览器,即可禁用 Chrome 浏览器的安全检查功能,同时也会禁用跨域安全检查功能,这样再次拿前面的例子进行测试,发现此时不会报错,同时也可以正确拿到服务端返回的数据。

img禁用浏览器安全检查功能

这种方式虽然可以实现跨域,但是需要每个用户都对浏览器进行设置,同时可能导致潜在的安全隐患,正常情况下不实用。但这个例子充分说明了,跨域错误是前端浏览器所做的限制,与后台服务无关。

3.2 JSONP实现跨域

根据思路2,既然跨域问题产生的原因是因为客户端发送了 Ajax 请求,那么我们打破这个条件即可。具体实现方式就是使用 JSONP 来进行跨域请求。

JSONP,是 JSONwith Padding 的简称,它是 json 的一种补充使用方式,利用 script 标签来解决跨域问题。JSONP 是非官方协议,他只是前后端一个约定,如果请求参数带有约定的参数,则后台返回 javascript 代码而非 json 数据,返回代码是函数调用形式,函数名即约定值,函数参数即要返回的数据。

JSONP 的实现原理如下图所示:

imgJSONP实现原理

首先在客户端 js 中定义一个函数(假设名为 handler),然后动态创建一个 script 标签插入页面中,script 标签的 src 属性即要调用的地址,同时,在调用的 url 中加入一个服务端约定的参数(假设名为 callback,参数值为已定义的函数名 handler),服务端收到请求,如果发现请求的 url 中带有约定的参数,那么就返回一段函数调用形式的 javascript 代码,该段代码的函数名即为 callback 参数的值 handler,函数的参数即为待返回的数据。这样,客户端拿到返回结果后就会执行 handler 函数,对返回的数据进行处理。

我们使用 jquery 向服务端发送一个 JSONP 格式的请求,从浏览器控制台可以看到请求和对应的响应,如下图所示:

imgJSONP请求

imgJSONP请求的响应

由上图可以看到,发送JSONP请求时,请求的 Type 为 script 类型而非 xhr 类型,这样就打破了跨域报错的三个必要条件,不会产生跨域错误,同时也验证了服务端返回的数据格式为 javascript 代码调用的形式,其中 Jquery331045** 这一长串函数名是 jquery 自动生成的。

由于 JSONP 的原理是使用 script 标签来加载数据,所以它的兼容性很好,但是使用 JSONP 来解决跨域问题存在以下缺陷:

  1. 只能发送 GET 请求
  2. 发送的不是 XHR 请求,这样导致 XHR 请求中的很多事件都无法进行处理
  3. 服务端需要改动

3.3 跨域资源共享CORS

CORS 是一个 W3C 标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。CORS 基于 http 协议关于跨域方面的规定,使用时,客户端浏览器直接异步请求被调用端服务端,在响应头增加响应的字段,告诉浏览器后台允许跨域。

img跨域错误

回到文章开始的这个跨域错误信息,可以看到错误的具体信息是:服务端没有设置Access-Control-Allow-Origin 这个响应头从而导致报错,通过设置 Access-Control-Allow-Origin: * 这个响应头,我们可以解决问题。但是,这种设置能满足所有情况吗? 更进一步,使用 CORS 时浏览器如何检查跨域错误? 前面我们有讲到,虽然浏览器报错,但是在这之前服务端已经接受了请求,那么,浏览器总是先发出请求后再进行判断吗?下面我们一一讨论。

3.3.1 浏览器如何检查跨域错误

浏览器检查跨域错误的基本原理是:

浏览器检测到 ajax 请求的域与当前域不一致,会在请求头中增加 Origin 字段,然后检查服务端响应头 Access-Control-Allow-Origin,如果不存在或不匹配,则报跨域错误。

img浏览器检查跨域错误原理

3.3.2 浏览器总是先发出请求,然后根据是否有 Access-Control-Allow-Origin 响应头来判断吗

答案是,对于简单请求,是;而对于非简单请求,不是。非简单请求的情况下,浏览器并不是直接请求所需资源,而是会先发出一个预检请求,预检请求通过后才会对所需资源进行请求。

img非简单请求预检请求

这里涉及到的简单请求和非简单请求的概念,那么简单请求和非简单请求有什么区别呢?MDN 对非简单请求进行了定义,满足下列条件之一,即为非简单请求:

  1. 使用了下列 HTTP 方法:PUT、DELETE、CONNECT、OPTIONS、TRACE、PATCH
  2. 使用了除以下首部之外的其他首部:Accept、Accept-Language、Content-Language、Content-Type
  3. Content-Type首部的值不属于下列其中一个: application/x-www-form-urlencoded、 multipart/form-data、 text/plain
  4. 请求中的 XMLHttpRequestUpload 对象注册了任意多个事件监听器
  5. 请求中使用了ReadableStream对象

简单来说,除了我们平时使用最多的 GET 和 POST 方法,以及最常使用的 Accept、Accept-Language、Content-Language 和 类型为 application/x-www-form-urlencoded、 multipart/form-data、 text/plain 的 Content-Type 请求头,其他基本都是非简单请求。对于这些非简单请求,浏览器会发出两个请求,第一个为 OPTIONS 遇见请求,遇见请求的响应检查通过后才会发出对资源的请求。

img非简单请求过程

生产环境下,如果需要发送非简单跨域请求,每次两个请求会增加响应时间,为此,W3C 标准中增加了另一个响应头 Access-Control-Max-Age 参数,该响应头表明了对于非简单请求的预检请求浏览器的缓存时间,在缓存有效期内,非简单请求可以不发送预检请求,另外,实际开发中,可以在服务端设置接收到的请求方法是 OPTIONS 时,直接返回 200,这样也能加快响应。

3.3.3 设置 Access-Control-Allow-Origin: * 就行吗

img带cookie的跨域

当我们需要发送带 cookie 的请求时,Access-Control-Allow-Origin 直接设置为通配符 * 时是无法通过浏览器的检查的,此时该响应头的值必须与发出请求的域完全匹配才行,另外,还需要设置 Access-Control-Allow-Credentials 响应头的值为 true,表示支持带 cookie 的跨域请求。

3.3.4 CORS请求头和响应头总结

请求头:

  • Origin: 浏览器发出 Ajax 跨域请求之前会添加此头部,值为发送请求的域
  • Access-Control-Request-Method:使用了除 GET、POST 请求方法之外的方法,浏览器会添加此头部,值为当前请求方法
  • Access-Control-Request-Headers:使用了自定义头部或除了Accept、Accept-Language、Content-Language、Content-Type 之外的头部,浏览器会添加此头部,值为当前的请求方法

响应头:

  • Access-Control-Allow-Origin: 表示服务端允许哪些域请求资源
  • Access-Control-Allow-Methods: 当客户端包含 Access-Control-Request-Method 请求头时,服务端需要响应该头部,值通常由 Reauest 的 header 中 Access-Control-Request-Method 取得
  • Access-Control-Allow-Headers: 当客户端包含 Access-Control-Request-Headers 请求头时,服务端需要响应该头部,值通常由 Reauest 的 header 中 Access-Control-Request-Headers 取得
  • Access-Control-Expose-Headers: 指出客户端通过 XHR 对象的 getResponseHeaders 方法可以获取的响应头有哪些
  • Access-Control-Allow-Credentials: 允许带 cookie 的跨域请求
  • Access-Control-Max-Age: 预检请求的缓存时间

4. 总结

本文介绍了跨域的原因,重点介绍了使用 JSONP 和 CORS 解决跨域问题的方法。除此之外,实际开发中还其他各种解决跨域问题的思路,本质上,这些方法都是打破跨域错误的三个条件,大家可以自行查资料了解一下。

posted @ 2019-01-02 20:30  诗和远方-ysk  阅读(816)  评论(0编辑  收藏  举报