文章目录
前言
最近准备面试刷题刷到了跨域,因此根据自己的学习和网上的资料进行了一个整理,也相当于复习了。本文根据cookie、iframe和AJAX三个方向总结如何解决跨域。文章部分内容参考了阮一峰大佬的博客,也推荐大家读一下。
https://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html
一、什么是“同源策略”
浏览器安全的基石就是“同源策略”,即如果A网站设置的cookie,B网站如果想访问必须满足三个要求:同一种协议、同一个域名、同一个端口号。
举例来说,http://www.example.com/dir/page.html这个网址,协议是http://,域名是www.example.com,端口号是80(可以省略,http默认80,https默认443)
例如:
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 :不同源(端口不同)
如果没有“同源策略”的话,当你正在访问淘宝,并且登录了账号,这时候点击进入到了另外一个页面。而别人就可以利用这个页面来获取你淘宝页面的cookie信息,伪造你的登录信息来操控你的淘宝账号(CSRF攻击)或者是利用在这个页面中插入iframe标签,让他的src指向淘宝,便可以通过插入js代码来操控你的淘宝界面。
并且随着互联网的发展,"同源政策"越来越严格。目前,如果非同源,共有三种行为受到限制。
a、DOM无法操作
b、AJAX请求不能发生
c、Cookie、LocalStorage 和 IndexDB 无法读取
二、如何解决跨域
1.cookie
cookie是服务器写入浏览器的一小段信息,如果不是同源的话无法获取。但是如果两个网页的一级域名相同,只是二级域名不同,浏览器可以通过设置document.domain来共享cookie。
代码如下(示例):
比如A网页为 http://xl.example.com/a.html,B网页为 https://ts.example.com/b.html
那么只要他们设置相同的document.domain,就可以共享cookie
document.domain = 'example.com';
然后在A网页设置cookie
document.cookie = 'Vonger hahah';
在B网页中就可以获取
let allCookie = document.cookie;
但是此方法只适用于cookie和iframe,不能获取Localstorage和IndexDB,规避同源政策,而要使用下文介绍的PostMessage API
另外,服务器也可以在设置cookie的时候可以指定cookie的一级域名,设置document.domain比如.example.com
Set-Cookie: key=value; domain=.example.com; path=/
这样的话二级域名、三级域名不需要做任何设置就可以共享cookie
2.iframe
如果两个页面不同源,就无法获取对方的DOM,经典例子是iframe窗口或者window.open打开的窗口,他们与父窗口无法通信
比如父窗口运行下面代码,如果iframe不是同源,就会报错
代码如下(示例):
document.getElementById("#myIFrame").contentWindow.document
// Uncaught DOMException: Blocked a frame from accessing a cross-origin frame.
同样如果iframe窗口想获取父窗口的DOM也会报错
window.parent.document.body
// 报错
如果两个页面的一级域名相同,只是二级页面不同,可以通过上面的方法设置window.domain,这样就可以获取DOM
如果是完全不同域的两个窗口,有三种方法可以解决跨域
a、片段标识符(hash)
片段标识符实质URL的#后面的部分,比如http://example.com/x.html#fragment的#fragment,如果只是改变片段标识符,页面并不会刷新
父窗口可以把信息写在子窗口的片段标识符
document.getElementByID('#myIframe').src = origin.URL + '#' + data;
子窗口可以通过监听hashchange事件得到通知来调用回调函数
window.onhashchange = function() {
let message = window.location.hash;
// ... 一系列操作
}
当然子窗口也可以通过更改父窗口的片段标识符来传递消息
parent.location.href = target + '#' + hash;
b、window.name
window.name最大的特点是无论是否同源,只要在一个窗口中,一个页面设置了window.name,其他页面都可以获取这个属性
方法原理:A页面通过iframe加载B页面。B页面获取完数据后,把数据赋值给window.name。然后在A页面中修改iframe使他指向本域的一个页面。这样在A页面中就可以 直接通过iframe.contentWindow.name获取到B页面中获取到的数据。
或者
父窗口先打开子窗口,载入一个不同源的网页,该网页将信息写进window.name,接着子窗口跳回一个与父窗口同源的网页,然后父窗口就可以获取子窗口的window.name了
这个方法的好处是window.name属性加载与不同的页面(即使域名不同),如果name值没有被修改他就一直存在,并且很大有2M,缺点是必须监听子窗口的window.name属性的变化,影响网页性能
c、跨文档通信API(window.postMessage)
上面两种方法都属于程自己想的序员破解的方法,H5为了解决整个问题,引入了一个新的API(跨文档通信API)
这个API为window新增了一个方法postMessage,允许跨窗口通信,无论两个窗口是否同源
比如,如果父窗口http://father.com向子窗口http://son.com发送消息,就可以直接调用window.postMessage
let son = window.open('http://son.com', 'title'); // 第二个参数为给这个窗口取得名字
son.postMessage('Hello World', 'http://son.com');
postMessage的第一个参数为所要发送的信息,第二个参数为接收信息的源(即协议+域名+端口号),也可以是*表示不限制,向所有窗口发送
子窗口向父窗口发送消息类似
window.opener.postMessage('Hello 世界', 'http://father.com'); // opener可读写,表示创建这个窗口的window对象
子窗口和父窗口都可以通过message事件监听对方的消息
window.addEventListener('message', function(e) {
console.log(e.data);
})
// message属性的event对象提供一下三个属性
// data:消息内容
// source: 发送消息的窗口
// origin: 消息发送的网址
通过event.source,子窗口可以引用父窗口来向其他窗口发送消息
window.addEventListener('message', function(e) {
e.source.postMessage('给0们拜年啦!', '*');
// 子窗口调用父窗口给全体窗口拜年了
})
通过event.origin可以过滤掉不是发送给本窗口的消息
window.addEventListener('message', function(e) {
// if (e.origin !== '本窗口的源(http://son.com)') return;
// 还可以帮助转发给本来要发给的窗口
if (e.origin !== '本窗口的源(http://son.com)') {
if (e.data !== 'Hello World') {
e.source.postMessage('找你找得好辛苦', e.origin);
}
} else {
console.log(e.data)
}
})
3.AJAX
“同源策略”规定,AJAX请求只能发送给同源的网址,否则会报错
解决AJAX跨域的方法,一共四种。除了架设代理服务器(浏览器先请求同源服务器,然后再由同源服务器请求目标服务器)外,还有三种
a、JSONP
JSONP应该是解决跨域最先想到的也是最常见的方法,他的优点在于简单易用且兼容性很高,可以用来解决低版本浏览器的跨域问题,老式浏览器全部支持并且对服务器的改动很小,但是只能接收GET请求(因为是script标签来加载资源)
他的基本思想是动态添加一个script标签(因为script、link、img不受同源限制),向服务器请求JSON数据;服务器收到请求后,将数据放在一个指定的回调函数的参数中传回来
// 先动态添加一个script标签 由他向跨域原网址发出请求
function addScriptTag(src) {
let script = document.creat('script');
script.setAttribute('type', 'text/javascript');
script.src = src;
document.body.appendChild(script);
}
// 当页面加载完毕时调用函数
window.onload = function() {
addScript('http://example.com/ip?callback=foo');
}
function foo() {
console.log('Your public IP address is: ' + data.ip);
}
上面的通过动态添加script标签,通过script标签向服务器example.com发出请求。该请求的查询字符串有一个callback参数,该参数是不可省略的,用来指定回调函数的函数名
服务器接收到该函数名后,会将数据传入指定回调函数的参数,然后返回
foo({'ip': '8.8.8.8'});
因为script请求的脚本,直接作为代码被解析运行,所以这是,只要浏览器定义了foo函数,该函数就会立刻执行。因为JSON作为参数直接被JavaScript解析为对象,而不是字符串,所以也避免了使用JSON.parse()将JSON格式的字符串转为对象的步骤
JSONP返回的就是数据格式一般就是一个js脚本,这个脚本有以下特点:
①返回的js脚本通常是服务端动态生成的。
②整个脚本通常有且仅有一条语句,且是一个函数调用。
③脚本中调用到的函数,是页面上存在的一个函数,其函数名通过get参数传
④递给服务端,服务端再将其回写到js脚本中。
⑤函数的参数,是服务端处理后的结果数据,以json格式直接写在脚本中。这也是jsonp得名的由来。
b、websocket
websocket是一种通信协议,使用ws://(非加密,对应80端口)和wss://(加密,对应443端口)作为协议前缀。该协议不实行“同源政策”,只要浏览器支持,就能够通过他进行跨域通信
下面是一个例子,浏览器发出的WebSocket请求头信息
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com
上面代码中Origin字段表示请求源,即发送自哪个域名
正是因为有了origin字段,所以WebSocket才没有实行“同源政策”,服务器只需要根据这个字段判断是否可以通行,如果该域名在白名单内,则可以进行通行,否则失败,如果通信成功服务器会做出如下回应
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat
c、CORS(跨域资源共享)
CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是W3C标准,是跨源AJAX请求的根本解决方法。相比JSONP只能发GET请求,CORS允许任何类型的请求。
它允许浏览器向跨域服务器发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制
CORS需要浏览器和服务器同时支持,目前,所有浏览器都支持CORS,IE需要版本在IE10以上
整个CORS通信都是浏览器自动完成,不需要用户的参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样
浏览器一旦发现AJAX跨域请求,就会自动添加一些附加的头部信息,有时还会多出一条附加的请求,但用户是察觉不到的
因此,实现CORS通信的关键是服务器,只要服务器实现了CORS接口,就可以跨域通信
浏览器把CORS跨域请求分为两类:简单请求(simple request)和非简单请求(not-so-simple request)
同时满足一下两大要求的为简单请求
(1) 请求方法是以下三种方法之一:
HEAD
GET
POST
(2)HTTP的头信息不超出以下几种字段:
Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain
这是为了兼容表单,因为表单是可以直接跨域提交的。AJAX的跨域设计就是,只要表单可以发,AJAX就可以直接发
对于不满足以上两点的,为非简单请求,浏览器对于简单请求和非简单请求的处理不一样
i. 简单请求
对于简单请求,浏览器直接发出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
如果服务器同意了此次请求,则会在返回的响应中添加几个新的字段到头部信息中
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
上面的头部信息中有三个与CORS有关,以Access-Control开头
- Access-Control-Allow-Origin:该字段是必须的,他的值要么是请求是的origin值,要么是*表示接收任意域名的请求
- Access-Control-Allow-Credentials:该字段可选,表示是否允许发送cookie。他的值只能是true,如果不允许发送cookie直接删除该字段即可
- Access-Control-Expose-Headers:该字段可选,CORS请求的时候,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本的字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers字段中指定。上面的例子指定,getResponseHeader(‘FooBar’)可以返回FooBar字段的值。
withCredentials 属性:上面说到,CORS通信默认不发送cookie和HTTP认证信息。如果要把cookie发送到服务器,一方面需要服务器同意,指定Access-Control-Allow-Credentials:true,另一方面,开发者必须在AJAX中打开withCredentials属性并设置为true,否则,即使服务器同意发送cookie或者服务器要求发送cookie,浏览器也不会处理。
但是有些浏览器,即使没有设置withCredentials:true,也会发送cookie,这时可以显式关闭withCredentials
withCredentials: false;
需要注意的是,如果要发送cookie,Access-Control-Allow-Origin的值就不能设置为星号*,必须要指定明确的、与请求头origin字段相同的域名才行。同时cookie遵循“同源政策”,只有用服务器域名设置的cookie才会上传,其他域名的cookie不会上传,并且如果是跨域的话原网页中的document.cookie也无法读取到服务器的cookie
i. 非简单请求
非简单请求是对那种浏览器有特殊要求的请求,比如请求方法是PUT、DELETE,或者Content-Type的值是application/json(不满足简单请求的两个条件的请求都是非简单请求)
非简单请求的CORS请求,会在正式请求之前,增加一次HTTP查询请求,称为“预检”请求(preflight)
浏览器会先询问服务器,当前网页所在的域名是否在服务器的许可名单中,以及可以使用哪些HTTP动词的和头信息字段。只有得到肯定的答复,才会发出正式的XMLHttpRequest请求,否则就会报错
下面是一段浏览器的JavaScript脚本
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();
因为是HTTP请求的方法是PUT,并且发送了一个自定义头信息X-Custom-Header,所以为非简单请求
浏览器会在正式连接之前先自动发送一个“预检”请求,要求浏览器确认可以这样请求,下面是这个“预检”请求的HTTP头部信息
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
“预检”请求用的请求是OPTIONS方法,表示这个请求是用来询问的。头信息里面,关键字是Origin,表示请求来自哪个源
除了Origin字段以外,“预检”请求还包含两个特殊字段
- Access-Control-Request-Method:该字段是必须的,用来列出CORS请求会用到哪些HTTPS的方法,上例是PUT
- Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header
服务器收到“预检”请求后,检查了Origin、Access-Control-Request-Method、Access-Control-Request-Headers三个字段后,确认允许跨域,就可以回应
如果浏览器否定了“预检”请求,会返回一个正常的HTTP响应,但是没有包含任何CORS相关的字段。这时浏览器就会认定服务器不同意“预检”请求,因此触发一个错误,该错误会被XMLHttpRequest对象的onerror回调函数捕获,控制台会打印出报错信息
XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
如果浏览器允许跨域请求,就会返回一个带有和CORS相关字段的HTTP响应
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain
上面的字段中最关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以发出请求,该字段也可以是星号*,表示所有的域名都可以请求
Access-Control-Allow-Origin: *
其他相关字段分别为:
- Access-Control-Allow-Methods:该字段必须,是一个以逗号分割的字符串,注意该字段返回的是服务器所有支持的方法而不只是请求源需要用的方法,这样做事为了避免多次“预检”
- Access-Control-Allow-Headers:如果浏览器的请求包括Access-Control-Request-Headers,那么该字段就是必须的。是一个以逗号分割的字符串,注意该字段返回的也是服务器所有支持的请求头信息字段,不限于浏览器在“预检”中请求的字段,这样的目的也是避免多次“预检”
- Access-Control-Allow-Credentials:该字段与简单请求时的用法相同,是否允许发送cookie,只能是true表示发送,不允许直接删除该字段即可
- Access-Control-Max-Age:该字段可选,用来表示此次“预检”的有效期,单位为秒,比如Access-Control-Max-Age: 1728000,表示浏览器在20天(1728000秒)内不需要再次进行“预检”,可以直接发送AJAX请求
如果服务器通过了“预检”,允许跨域,那么浏览器的每次请求都会自动加一个Origin字段,服务器的响应也会加一个Access-Control-Allow-Origin字段,该字段是必须的
也就是说如果通过了“预检”,那么之后的请求就和简单请求一样了
CORS与JSONP的使用目的相同,都是为了解决跨域,但是CORS比JSONP强大
JSONP只支持GET,而CORS支持所有HTTP请求方法
JSONP的优势在于支持老式浏览器,以及兼容不支持CORS请求的浏览器
第一次写博客,里面的内容都是我根据自己的理解以及参考阮大佬的文章总结的,如果有什么地方错误希望大家多多包涵,指正一下