前端面试跨域问题小记

前端面试的问题中细分一下类别还是不少,今天主要对跨域这一块做一个整理、小记。

跨域也是实战中相当相当常见的问题,面试一般是考概念和解决方法。

总结下来有这么一些东西:

  • 什么是跨域问题?
  • 浏览器的同源策略是什么?
  • JSONP解决跨域
  • CORS解决跨域
  • 反代解决跨域

什么是跨域问题

跨域问题说得简单一些是我们在站点A的页面去请求站点B的API时,出于浏览器安全策略的问题,我们没有办法对API进行跨域调用。此举主要是避免加载到一些其他地方的恶意资源,但也对开发带来了跨域这个问题。

严格来说并不一定只有网页和接口域名不同的时候才算跨域,端口不同也算跨域。

跨域问题主要是出现在AJAX调用接口上,使用HTML标签加载资源一般是没有限制的。

什么是同源策略

同源的定义(来自MDN):

  • 如果两个页面的协议,端口(如果有指定)和主机都相同,则两个页面具有相同的源。我们也可以把它称为“协议/主机/端口 tuple”,或简单地叫做“tuple". ("tuple" ,“元”,是指一些事物组合在一起形成一个整体,比如(1,2)叫二元,(1,2,3)叫三元)

需要注意的是,IE的同源策略没有考虑端口号,而且存在授信范围,比如公司域名,这是在同源策略限制外的。

当页面要跨域加载资源的时候,请求会受到如下约束(来源MDN):

  • 通常允许跨域写操作(Cross-origin writes)。例如链接(links),重定向以及表单提交。特定少数的HTTP请求需要添加 preflight。
  • 通常允许跨域资源嵌入(Cross-origin embedding)。之后下面会举例说明。
  • 通常不允许跨域读操作(Cross-origin reads)。但常可以通过内嵌资源来巧妙的进行读取访问。例如可以读取嵌入图片的高度和宽度,调用内嵌脚本的方法,或availability of an embedded resource.

简单来说,同源策略就是浏览器默认只会允许页面从同源的路径加载资源,对于跨域加载路径资源会有限制。

JSONP跨域

最常见的跨域解决方案,对于jQuery在请求的时候把dataType从“json”改成“jsonp”就可以向目标URL发送JSONP请求。

JSONP在跨域的时候只支持GET请求,不支持POST,但是它兼容性好而且使用简单,同时可以兼容IE。

JSONP实际上是利用<script>标签跨域加载资源不受限制的“特性”(当然也可以说是漏洞)来实现用GET请求加载资源,在页面内<script>标签的src会指向一个不同源的API,该API会返回一个JSON格式的数据,但是这个数据是经过包装的,实际上是一行JS。

一般来说返回的内容是调用一个callback,而这个callback会被预先定义在页面内,被这个加载的JS调用。如果是使用jQuery,这个callback就相当于success函数。

JSONP发送的请求并不是XMLHTTPRequest,而是相当于正常加载一个JS脚本。

补充:

根据面试的情况,补充一些内容。

JSONP的异常捕捉方式:通过在标签上挂onerror实现。
JSONP的超时处理方式:设定callback会把数据放到一个指定的变量,用setTimeout去检测状态,没取到东西就抛出超时异常,且废弃掉已经设定好的callback。

CORS跨域

现在的跨域问题解决方案,对于XMLHttpRequest或者Fetch发起的请求,服务器可以添加一个请求头 —— “Access-Control-Allow-Origin”来告诉浏览器这个API允许被什么外域调用。

如果请求头如下(来源MDN):

1
2
3
4
5
6
7
8
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 00:23:53 GMT
Server: Apache/2.0.61
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml

指接口可被任意外域调用。

为了避免服务器可能会对跨域请求做出用户预期外的回复,进而造成用户数据受到影响,一部分跨域请求会被划分为“需预检的请求”,或者说“非简单请求”。

简单请求指HTTP的请求方法是HEAD、GET、POST之一,且HTTP投的信息不超出以下字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type (仅限application/x-www-form-urlencoded、multipart/form-data、text/plain)

特别注意Content-Type没有application/json和application/xml。

对于需预检的请求,这类请求在发送真正的跨域请求前会向服务器发送一个OPTIONS请求,验证服务器是否能够允许这样的跨域调用,如果服务器不允许,则真实的跨域请求不会被发起。

CORS还可以用Access-Control-Allow-Methods和Access-Control-Allow-Headers限定请求方法和请求头。但是需要注意,服务器CORS配置不当会导致不合理的跨域调用漏洞。

postMessage跨域

window.postMessage可以让同一个浏览器内打开的不同页面进行通信,即使A、B非同一个origin也可使用该方法通信。

MDN给出的使用方式:

发送

1
otherWindow.postMessage(message, targetOrigin, [transfer]);

接收

1
2
3
4
5
6
7
8
9
10
11
12
13
14
window.addEventListener("message", receiveMessage, false);

function receiveMessage(event)
{
// For Chrome, the origin property is in the event.originalEvent
// object.
// 这里不准确,chrome没有这个属性
// var origin = event.origin || event.originalEvent.origin;
var origin = event.origin
if (origin !== "http://example.org:8080")
return;

// ...
}

注:addEventListener第三个参数指事件在什么地方处理,设置为true则在捕获过程中处理,false为在冒泡过程中处理。

WebSocket跨域

WebSocket可以允许页面直接和服务器建立全双工的连接,实现数据的交互。浏览器的同源策略不覆盖WebSocket,使用WebSocket进行通信不存在跨域问题。

对比AJAX轮询:

服务端和客户端的通信可以用socket.io来解决,也可以另写通信方面的内容。

参考:
socket.io: Get Started

原生WebSocket(Client):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var ws = new WebSocket("ws://localhost:9998/echo");

ws.onopen = function () {
// Web Socket 已连接上,使用 send() 方法发送数据
ws.send("发送数据");
alert("数据发送中...");
};

ws.onmessage = function (evt) {
var received_msg = evt.data;
alert("数据已接收...");
};

ws.onclose = function () {
// 关闭 websocket
alert("连接已关闭...");
};

document.domain跨域

同一主域名下可以考虑使用给两边页面document.domain赋值同一主域名,然后使用iframe加载跨域内容,用onload触发内容的获取。

跨域加载的数据传递基于window.name(iframe.contentWindow.name)。

window.name跨域

window.name这个方法现在基本上是被改成用postMessage了,没什么实际作用。

window.name在页面跳转后仍然会保持,所以可以做一个简单的数据传递。

反代解决跨域

在Web服务器上解决跨域问题的方法,通过反代把外域反向代理成域内的某个路径,以规避跨域调用的限制。

这个严格来说不属于前端范畴,一般面试官也不怎么想问这个。

补充:XMLHttpRequest

XMLHttpRequest(XHR),调用API最最常用的东西,在AJAX中被大量使用。

前端开发者需要知道这个东西到底是什么,因为它是AJAX的底层,而且也要知道怎么用原生代码去构造、使用它,而不是使用类似jQuery等。

这个东西主要是用来和服务器交互,允许页面在不影响用户操作的情况下更新页面局部内容,获取某些数据,并且不要求页面刷新。

一个原生的AJAX请求如下所示:
GET:

1
2
3
4
5
6
7
8
var xhr = new XMLHttpRequest();
xhr.open('get', URL);
xhr.send();
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
}

POST:

1
2
3
4
5
6
7
8
9
var xhr = new XMLHttpRequest();
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.open('post', URL);
xhr.send(BODY);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
console.log(xhr.responseText);
}
}

把上面的内容封装成一个函数就能够得到一个简易的原生AJAX。

参考资料:
MDN - XMLHttpRequest