跨域知识一览

465 阅读7分钟

浏览器同源策略(same-origin policy)

跨域,是指浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript实施的安全限制。

同源策略限制了必须满足以下三个相同:

  • 协议相同
  • 域名相同
  • 端口相同

举例:http://www.example.com/dir/page.html,其中:

  • protocol:http://
  • domain: www.example.com
  • port: 80(默认省略)

相应判定:

  • http://www.example.com/dir2/other.html:同源
  • http://**example.com**/dir/other.html: 不同源(域名不同)
  • http://**v2.www.example.com**/dir/other.html:不同源(子域名不同)
  • http://www.example.com:**81**/dir/other.html: 不同源(端口不同)
  • **https**://www.example.com/dir/page.html: 协议不同

目的

保证用户信息安全,防止被恶意窃取数据。例如浏览危险网站时,网站可以去读取别的网站的cookie,由于提交表单不受同源策略显示,则可以拿到相应鉴权信息进行操作。

限制范围

  • CookieLocalStorageIndexDB 无法读取。
  • Dom无法获取
  • 无法发送Ajax请求

跨域方法

JSONP

设计模式中的代理模式的一种。由于浏览器允许在页面中通过标签从不同域名下加载静态文件,因此可以借用这个途径实现跨域。通常的做法是动态创建一个script标签,想服务器请求数据,服务器将数据放在一个指定名字的回调函数里传回来。

该方法只能实现GET请求

function addScriptTag(src) {
  var script = document.createElement('script');
  script.setAttribute("type","text/javascript");
  script.src = src;
  document.body.appendChild(script);
}

window.onload = function () {
  addScriptTag('http://example.com/ip?callback=foo');
}

function foo(data) {
  console.log('Your public IP address is: ' + data.ip);
};

我们还可以借助jquery来实现:

$.ajax({
    url: http://example.com/login',
    type: 'GET',
    dataType: 'jsonp',//请求方式为jsonp
    jsonpCallback: 'callback',
    data: {
        "username": "Mike"
    }
})

postMessage

H5引入的新API,我window对象新增了一个postMessage属性,允许跨窗口通信。

otherWindow.postMessage(message,targetOrigin);

otherWindow指的是目标窗口,也就是要给哪一个window发送消息,是window.frames属性的成员或者是window.open方法创建的窗口。postMessage方法第一个参数是具体的信息内容;第二个参数是接受消息的窗口的远,也可以设置为*,表示向所有窗口发送。父子窗口都可以通过监听message时间来监听对方的消息。

var popup = window.open('http://bbb.com', 'title');
popup.postMessage('Hello World!', 'http://bbb.com');

CORS

目前主流的跨域解决方案。CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。IE8+:IE8/9需要使用XDomainRequest对象来支持CORS。 整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。 因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

CORS分为简单请求非简单请求

满足以下两个条件为简单请求,否则为非简单请求:

  • 请求方式为HEAD、POST 或者 GET
  • http头信息不超出以下字段:Accept、Accept-Language 、 Content-Language、 Last-Event-ID、 Content-Type(限于三个值:application/x-www-form-urlencoded、multipart/form-data、text/plain)
简单请求

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8
  • Access-Control-Allow-Origin:该值可以是Origin字段的值,也可以是*,表示接受任意域名请求
  • Access-Control-Allow-Credentials:该值是一个布尔值,表示是否允许发送Cookie,如果设置为true,则需要在Ajax请求中开启withCredentials属性。当允许发送Cookie是,Access-Control-Allow-Origin必须指定明确的、与网页请求一直的域名
  • Access-Control-Expose-Headers: 可选值,可指定返回字段的值
非简单请求

非简单请求通常为请求方法是PUTDELETE,或者是Content-Type字段为application/json

非简单请求会在正式通讯前增加一次HTTP查询请求,先询问服务器当前网站所在的域名是否在许可范围内,以及可以使用哪些HTTP动词和头信息字段。当得到肯定的答复后,浏览器才会正式发出XHR请求,否则报错。

预先检查使用的请求方法是OPTION,头部信息中,带有关键字段Origin,表示请求来自哪个源。除此之外还会包含两个字段:

  • Access-Control-Request-Method:必填,用于列出浏览器请求会用到哪些HTTP方法
  • Access-Control-Request-Headers:用于指定浏览器请求会额外发送的头部信息字段

当服务器收到预先检查的请求之后,检查以上提到的三个字段以后,确认允许请求,就可以走出回应。如果浏览器否定了该请求,会返回一个正常的HTTP回应,会触发错误并被XHR对象的onerror捕获。

document.domain + iframe

使用场景:主域相同,子域不同的跨域应用场景。

浏览器允许通过设置document.domain共享 Cookie。举例来说,A网页是http://w1.example.com/a.html,B网页是http://w2.example.com/b.html,那么只要设置相同的document.domain,两个网页就可以共享Cookie。

document.domain = 'example.com'

现在,A网页通过脚本设置cookie,B网页可以通过document.cookie读到。LocalStorage 和 IndexDB 则无法通过这种方法。

或者,服务端可以设置Cookie的时候,指定Cookie所属域名为一级域名:

Set-Cookie: key=value; domain=.example.com; path=/

location.hash + iframe

当A与B页面进行跨域通信时,通过中间页面C来实现。三个页面中,不同域之间利用iframe的location.hash传值,相同域之间直接由JS访问来通信。

// A  (www.test1.com/a.html)
<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 向b.html传hash值
    setTimeout(function() {
        iframe.src = iframe.src + '#user=admin';
    }, 1000);
    
    // 开放给同域c.html的回调方法
    function onCallback(res) {
        alert('data from c.html ---> ' + res);
    }
</script>

// B (www.test2.com/b.html)
<iframe id="iframe" src="http://www.domain1.com/c.html" style="display:none;"></iframe>
<script>
    var iframe = document.getElementById('iframe');

    // 监听a.html传来的hash值,再传给c.html
    window.onhashchange = function () {
        iframe.src = iframe.src + location.hash;
    };
</script>

// C (www.test1.com/c.html)
<script>
    // 监听b.html传来的hash值
    window.onhashchange = function () {
        // 再通过操作同域a.html的js回调,将结果传回
        window.parent.parent.onCallback('hello: ' + location.hash.replace('#user=', ''));
    };
</script>

window.name + iframe

window对象中有一个name属性,在一个窗口的生命周期内,载入的所有页面都共享同一个window.name,且每个页面都能够对该属性进行读写。

// 某个Tab窗口内
window.name = 'test name'
setTimeout(() => {
	window.location.href = 'http://www.test.com'
}, 1000)

在进入www.test.com页面之后,此时的window.name仍然是test name

因此我们可以利用window.name的这种特性,在某个页面设置好window.name之后,跳转到另一个页面获取,用这种方法来实现跨域。

我们可以在页面中创建iframe标签,iframe中指向的页面中设置好window.name为我们需要的字符串。

var iframe = document.createElement('iframe')
iframe.src = 'http://localhost:8080/data.php'
var data = '';
iframe.onload = function() {
    data = iframe.contentWindow.name;
}

以上写法的问题在于如果iframe.src中地址不同源,则也无法操作iframe中的任何属性。

var iframe = document.createElement('iframe')
iframe.src = 'http://localhost:8080/data.php'
var data = '';
iframe.onload = function() {
    iframe.onload = function() {
    	data = iframe.contentWindow.name;
	}
    iframe.src = 'about:blank'
}

以上方法重复触发了iframe的加载刷新,其中的about:black可以替换为其他某个同源页面。

参考文档