为了更了解跨域的原理,可以阅读参考来源中的文章,里面对跨域的原理讲解很详细到位。
说实话,当初整理过一篇文章,然后作为了一个解决方案,但是后来发现仍然有很多人还是不会。无奈只能耗时又耗力的调试。然而就算是我来分析,也只会根据对应的表现来判断是否是跨域,因此这一点是很重要的。
ajax 请求时,如果存在跨域现象,并且没有进行解决,会有如下表现:(注意是 ajax 请求,请不要说为什么 http 请求可以,而 ajax 不行,因为 ajax 是伴随着跨域的,所以仅仅是 http 请求 ok 是不行的)。
注意:具体的后端跨域配置请看题纲位置。
第一种现象:No "Access-Control-Allow-Origin" header is present on the requested resource,并且 The response had HTTP status code 404


出现这种情况的原因如下:
解决方案: 后端允许 options 请求
第二种现象:No "Access-Control-Allow-Origin" header is present on the requested resource,并且 The response had HTTP status code 405

这种现象和第一种有区别,这种情况下,后台方法允许 OPTIONS 请求,但是一些配置文件中(如安全配置)阻止了 OPTIONS 请求,才会导致这个现象
解决方案:后端关闭对应的安全配置
第三种现象:No "Access-Control-Allow-Origin" header is present on the requested resource,并且 status 200

这种现象和第一种和第二种有区别,这种情况下,服务器端后台允许 OPTIONS 请求,并且接口也允许 OPTIONS 请求,但是头部匹配时出现不匹配现象
比如 origin 头部检查不匹配,比如少了一些头部的支持(如常见的 X-Requested-With 头部),然后服务端就会将 response 返回给前端,前端检测到这个后就触发XHR.onerror,导致前端控制台报错。
解决方案:后端增加对应的头部支持
第四种现象:heade contains multiple values ","

表现现象是,后台响应的 http 头部信息有两个 Access-Control-Allow-Origin:*
说实话,这种问题出现的主要原因就是进行跨域配置的人不了解原理,导致了重复配置。
关于跨域,有两个误区:
之所以会跨域,是因为受到了同源策略的限制,同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。
如下图所示:

这三个源分别由于域名、协议和端口号不一致,导致会受到同源策略的限制。
1、不能向工作在不同源的的服务请求数据(client to server)
这里有个问题之前也困扰了我很久,就是为什么http://home.com加载的http://cdn.home.com/index.js可以向http://home.com发请求而不会跨域呢?其实http://home.com加载的JS是工作在http://home.com的,它的源不是提供JS的cdn,所以这个时候是没有跨域的问题的,并且script标签能够加载非同源的资源,不受同源策略的影响。
2、无法获取不同源的 document/cookie 等 BOM 和 DOM,可以说任何有关另外一个源的信息都无法得到 (client to client)
1、为什么要限制不同源发请求?
假设用户登陆了 http://bank.com,同时打开了 http://evil.com,如果没有任何限制,http://evil.com 可以向 http://bank.com 请求到任何信息,进而就可以在 http://evil.com 向 http://bank.com 发转账请求等。
如果这样,为什么不直接限制写,只限制读?
因为如果连请求都发不出去了,那就不能做跨域资源共享了,无法读取返回结果,http://evil.com 就无法继续下一步的操作,如获取转账请求的一些必要的验证信息。
2、为什么限制跨域的 DOM 读取?
如果不限制的话,那么很容易就可以伪装其它的网站,如套一个 iframe 或者通过 window.open 的方法,从而得到用户的操作和输入,如账户、密码。
另外,添加这个 http 头可以限制别人把你的网站套成它的iframe:
X-Frame-Options: SAMEORIGIN
同源策略提供了安全的同时也造成了不方便,因为有时候我们需要跨域请求,如获取第三方提供的服务信息,由于第三方的源和本网站的源不一样,所以这个时候就受到跨域的限制。
跨域最常用的方法,应当属 CORS,如下图所示:

只要浏览器检测到响应头带上了 CORS,并且允许的源包括了本网站,那么就不会拦截请求响应。
CORS 把请求分为两种,一种是简单请求,另一种是需要触发预检请求,这两者是相对的,怎样才算不简单?只要属于下面的其中一种就不是简单请求:
这个时候就认为需要先发个预检请求,预检请求使用OPTIONS方式去检查当前请求是否安全,如下图所示:

代码里面只发了一个请求,但在控制台看到了两个请求,第一个是 OPTIONS,服务端返回:

返回头里面包含了允许的请求头、请求方式、源,以及预检请求的有效期,上图是设置了20天,在这个有效期内就不用再发一个 options 的请求,实际上浏览器有一个最长时间,如 Chrome 是5分钟。如果在预检请求检测到当前请求不符合服务端设定的要求,则不会发出去了直接抛异常,这个时候就不用去发复杂的请求了。
如本源不在允许的源范围内,则会抛异常,无法获取返回结果:

为了支持 CORS,nginx 可以这么配:
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS, PUT, DELETE';
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
add_header 'Access-Control-Expose-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Content-Range,Range';
}
第二种常用的跨域的方法是 JSONP,JSONP 是利用了 script 标签能够跨域,如下代码所示:
function updateList (data) {
console.log(data);
}
$body.append(‘<script src=“http://otherdomain.com/request?callback=updateList"></script>');
代码先定义一个全局函数,然后把这个函数名通过 callback 参数添加到 script 标签的 src,script 的 src 就是需要跨域的请求,然后这个请求返回可执行的 JS 文本:
// script响应返回的js内容为
updateList([{
name: 'hello'
}]);
由于它是一个 js,并且已经定义了 upldateList 函数,所以能正常执行,并且跨域的数据通过传参得到。这就是 JSONP 的原理。
所以由于 script/iframe/img 等标签的请求默认是能带上 cookie(cookie 里面带上了登陆验证的票 token),用这些标签发请求是能够绕过同源策略的,因此就可以利用这些标签做跨站请求伪造(CSRF),如下面代码所示:
// 转账请求
<iframe></iframe>
// 配置路由器添加代理
<img style="display:none">
如果相应的网站支持 GET 请求,或者没有做进一步的防护措施,那么如果用户在另外一个页面登陆过了,再打开一个有毒的网站就中招了。
而动态ajax请求默认是不带 cookie 的,如果你要带 cookie,可以设置 ajax 的一个属性 withCredentials,如下代码所示:
// 原生请求 let xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open("GET", "http://otherdomain.com/list");
xhr.send();
// jquery请求
$.ajax({
url: "http://otherdomain.com/list",
xhrFields: {
withCredentials: true
}
});
这个时候就和 img/script 标签一样,能带上 cookie,并且还支持除 GET 之外的其它方式。所以这种方式也是能实现 CSRF 的,如下图所示:

所以如果转账请求只是不支持 GET,没做其它的防护措施,仍然有 CSRF 攻击的风险。那怎么办呢?
方法一是每次请求都要在参数里面显示地带上 token 即登陆的票,虽然跨域请求能带上 cookie,但是通过 document.cookie 仍然是获取不到其它源的 cookie 的,所以攻击者无法在代码里面拿到 cookie 里面的 token,所以就没办法了。方法一的缺点是会暴露 token,所以需要带 token 的最好不能是 GET,因为 GET 会把参数拼在 url 里面,用户可能会无意把链接发给别人,但不知道这个链接带上了自己的登陆信息。
方法二是每次转账请求前都先请求一个随机串,这个串只能用一次转账或者支付请求,用完就废弃,只有这个串对得上才能请求成功,攻击者是无法拿到这个串的,因为如果跨域请求带 cookie,浏览器要求 Access-Control-Allow-Origin 不能为通配符,只能为指定的源,如:
Access-Control-Allow-Origin: http://renren.com
由于攻击者所在的域名不在这个源里面,所以它是无法得到请求结果,所以请求不到随机串。因此这种方式也是可以避免 CSRF 攻击。
假设 Allow-Origin 为 *,ajax 设置 withCredentials 为 true 时,浏览器会抛异常,无法得到返回结果:

另外服务还需要指定 Allow-Credentials 的头部,如下代码所示:
add_header "Access-Control-Allow-Origin" "http://fedren.com";
add_header "Access-Control-Allow-Credentials" "true";
关于 cookie 还有两个地方值得注意,如下图所示:

讨论完了 client to server,我们再讨论 client to client,即如何和一个 frame 通信,包括 iframe 或者使用 window.open 打开的页面。
iframe 访问父页面可通过 window.parent 得到父窗口的 window 对象,通过 open 打开的可以用 window.opener,进而得到父窗口的任何东西;父窗口如果和 iframe 同源的,那么可通过 iframe.contentWindow 得到 iframe 的 window 对象,如果和 iframe 不同源,则存在跨域的问题,这个时候可通过 postMessage 进行通讯。
使用 postMessage 的基本原理如下图所示:
// main frame
let iframeWin = document.querySelector("#my-iframe")
.contentWindow;
iframeWin.postMessage({age: 18}, "http://parent.com");
iframeWin.onmessage = function(event) {
console.log("recv from iframe ", event.data);
};
// iframe
window.onmessage = function(event) {
// test event.origin
if (event.origin !== expectOrigin) {
return;
}
console.log("recv from main frame ", event.data);
};
window.parent.postMessage("hello, this is from iframe ", "http://child.com");
以页面嵌入 youtobe 视频为例,通过以下代码可以在页面嵌入一个 youtobe 视频,嵌入的是一个跨域的 iframe,所以就涉及到如何和 iframe 进行通信的问题。如怎么知道 iframe 的状态,触发父页面定义的事件 onPlayerReady,这个是 iframe 通知父页面,而父页面可以调 player.stopVideo 控制 iframe 的行为,这个是父页面通知 iframe。

iframe 通知父页面是通过 window.parent.postMessage,同时监听 message 事件:

经检查上面代码4304行的c就是 window.parent,这个 embed-player.js 是 iframe 的 js,iframe 的 js 通过 postMessage 发送了一个消息,如上图右边的窗口所示,然后在父窗口的 widgetapi.js 就收到了这个消息。
同样地,父窗口的 JS 也是使用 postMessage 向 iframe 发送消息,如下图所示:

当然 postMessage 不限于跨域,同域的也可以使用,只是同域的话可以通过 window 对象互相操作,你可能需要额外定义一些全局变量或者函数供其它 frame 使用,或者是定义一套事件机制(可以借助原生事件/jQuery/Vue 事件等)。
这里有一个特例,就是子域如 http://mail.hello.com 要跨 http://hello.com 的时候,可以显式地设置子域的 document.domain 值为父域的 domain:
document.domain = "hello.com";
就不会有跨域的问题了。
补充一点,如果需要和同源的不同标签页进行通信可以使用 localStorage,即一个页面设置 localStorage,其它页面就会触发 storage 事件:
window.addEventListener('storage', function(e) {
e.key;
e.oldValue;
e.newValue;
e.url;
e.storageArea;
});
这个我没试过,读者可以试一下。
再补充一点,websocket 是不受同源策略限制的,没有跨域的问题。CSS 的字体文件是会有跨域问题,指定 CORS 就能加载其它源的字体文件(通常是放在 cdn 上的)。而canvas 动态加载的外部 image,也是需要指定 CORS 头才能进行图片处理,否则只能画不能读取。
最后,跨域分为两种,一种是跨域请求,另一种访问跨域的页面,跨域请求可以通过 CORS/JSONP 等方法进行访问,跨域的页面主要通过 postMesssage 的方式。由于跨域请求不但能发出去还能带上 cookie,所以要规避跨站请求伪造攻击的风险,特别是涉及到钱的那种请求。

