同源策略
之所以会出现跨域问题,其实是由于浏览器同源策略的存在。同源策略是浏览器最基本最核心的功能,是一种安全策略。同源策略是浏览器行为,是为了保护本地数据不被获取回来的数据污染,因此拦截时机是在服务端返回时,即请求已经发出,服务器已经处理,只是在响应返回时,浏览器因为请求跨域而拒绝处理此次回来的数据。
跨域
说到跨域,我们先了解一下同域(同源),我们在控制台打印一下window.location.origin,可以看到origin由三个部分组成:origin = protocol+hostname+port,所以同域代表protocol、hostname、port都要相同,那么跨域就是这三个只要其中一个不同就带表跨域。以下面绿色行对比举例:
| protocol | hostname | port | 是否跨域 |
|---|---|---|---|
| http | www.a.com | 80 | |
| https | www.a.com | 80 | Y |
| http | yyy.a.comm | 80 | Y |
| http | www.a.com | 90 | Y |
如何解决
1、JSONP
<script>var script = document.createElement('script');script.type = 'text/javascript';// 传参一个回调函数名给后端,方便后端返回时执行这个在前端定义的回调函数script.src = 'http://www.domain.com/path?callback=handleCallback';document.head.appendChild(script);// 回调执行函数function handleCallback(res) {alert(JSON.stringify(res));}</script>
2、document.domain + iframe
此方案仅限主域相同,子域不同的情况,设置两个页面的document.domain = 主域,举例:
父窗口:http://parent.domain.com/page.html
<iframe id="iframe" src="http://child.domain.com/page.html"></iframe><script>document.domain = "domain.com"const parentName = "wstreet7"const childWindow = document.getElementById('iframe').contentWindowconst { childName } = childWindow</script>
子窗口:http://child.domain.com/page.html
<script>document.domain = "domain.com"const { parentName } = window.parentconsole.log(parentName) // wstreet7const childName = 'child'</script>
3、location.hash + iframe
页面通信一般是双向通信,A页面可以获取B页面的数据,B页面也可以获取A页面的数据使用location.hash + iframe达到两个页面通信是需要一个代理C页面的
A页面:www.domain1.com/a.html
<iframe id="iframe-b" src="http://www.domain2.com/b.html" style="display:none;"></iframe><script>var iframe = document.getElementById('iframe-b');// 向B.html传hash值setTimeout(function() {iframe.src = iframe.src + '#user=wstreet';}, 1000);// 开放给同域c.html的回调方法function onCallback(res) {alert('data from c.html ---> ' + res);}</script>
B页面:www.domain2.com/b.html
<iframe id="iframe-c" src="http://www.domain1.com/c.html" style="display:none;"></iframe><script>var iframe = document.getElementById('iframe-c');// 监听a.html传来的hash值,再传给c.htmlwindow.onhashchange = function () {const user = location.hash.replace('#user=', '');// 向C页面传值,C页面再传回给A页面iframe.src = iframe.src + '#cAge=26'};</script>
C页面:http://www.domain1.com/c.html
<script>// 监听b.html传来的hash值window.onhashchange = function () {// 再通过操作同域a.html的js回调,将结果传回window.parent.parent.onCallback('age: ' + location.hash.replace('#cAge=', ''));};</script>
4、window.name + iframe
此方法利用了window.name在location改变之后数据依然不会改变的特性来实现跨域
A页面:http//domain1.com/a.html
const proxy = (url, callback) => {let state = 0const iframe = document.createElement('iframe')// 会调用2次onloadiframe.src = urliframe.onload = () => {if (state === 0) {// 第1次,加载跨域b页面成功后,设置location,切到同域代理页面// 只有同域情况下,才能获取iframe.contentWindowiframe.contentWindow.location = 'http://www.domain1.com/proxy.html'state = 1} else if (state === 1) {// 第2次加载同域代理页面成功后读取iframe.namecallback(iframe.contentWindow.name)// 获取到数据后销毁iframedestoryFrame()}}document.body.appendChild(iframe);function destoryFrame() {iframe.contentWindow.document.write('');iframe.contentWindow.close();document.body.removeChild(iframe);}}proxy('http://www.domain2.b.html', (data) => {console.log(data)})
B页面:http://www.domain2.com/b.html
<script>window.name = 'wstreet'</script>
proxy页面:http://www.domain1.com/proxy.html(空白页面)
总结: 1、iframe先加载跨域页面及,将需要获取的数据保存在跨域页面的windo.name; 2、改变iframe的地址到同域代理页面,这时候可以读取iframe.contentWindow
5、postMessage
postMessage方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递,postMessage可以解决以下问题:
1)、页面和其他打开的新窗口的数据传递
2)、多窗口之间消息传递
3)、页面和嵌套的iframe之间的消息传递
4)、以上三个场景跨域数据传递
用法:postMessage(data,origin)接受两个参数:
(1)data:HTML5规范中规定该参数可以是JavaScript任意基本类型数据或者可复制的对象。然而并不是所有浏览器都支持,有些浏览器只支持字符串,所以我们在传递参数的时候需要使用JSON.stringify()方法对对象参数序列化。
(2)origin:字符串类型,指明目标窗口的源,协议+主机+端口号[+URL],URL会被忽略,所以可以不写,这个参数是为了安全考虑,postMessage只会将message传给指定窗口。也可以将origin设置成“*”,这样可以传递给任意窗口。如果要指定接收的窗口要和当前窗口同源的话,可以将origin设置成“/”。
A页面:http://www.domain1.com/a.html
<iframe id="iframe" src="http://www.domain2.com/b.html" frameborder="0"></iframe><script>const iframe = document.querySelector('#iframe')iframe.onload = () => {iframe.contentWindow.postMessage('wstreet', 'http://www.domain2.com')}window.addEventListener('message', e => {console.log(e.data)})</script>
B页面:http://www.domain2.com/b.html
<script>window.addEventListener('message', e => {const data = e.datawindow.parent.postMessage(data, 'http://www.domain1.com')})</script>
6、CORS(跨域资源共享)
需要服务端设置一些请求头
// 允许跨域访问的域名response.setHeader("Access-Control-Allow-Origin", "http://www.domain1.com");// 允许前端带认证cookie,若此项开启,上面的Access-Control-Allow-Origin不能设置成"*"response.setHeader("Access-Control-Allow-Credentials", "true");// options预检时,后端需要设置的两个常用自定义头response.setHeader("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");
如果需要携带cookie,前端需要设置
xhr.withCredentials = true;
7、Nginx代理跨域
同源策略只是浏览器的安全策略,对HTTP不造成限制,当请求发出时,可以在中间使用Nginx做一下跳板,将相关内容做一下替换。
server {listen 81;server_name www.domain1.com;location / {proxy_pass http://www.domain2.com:8080; #反向代理proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名index index.html index.htm;# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*add_header Access-Control-Allow-Credentials true;}}
8、WebSocket协议跨域
WebSocket协议是HTML5一种新的协议,它实现了浏览器与服务器双工通信,同时允许跨域通信。
使用Socket.io演示:
(1)前端代码:
//<div>user input:<input type="text"></div><script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script><script>var socket = io('http://www.domain2.com:8080');// 连接成功处理socket.on('connect', function() {// 监听服务端消息socket.on('message', function(msg) {console.log('data from server: ---> ' + msg);});// 监听服务端关闭socket.on('disconnect', function() {console.log('Server socket has closed.');});});document.getElementsByTagName('input')[0].onblur = function() {socket.send(this.value);};</script>
(2)Nodejs socket后台
var http = require('http');var socket = require('socket.io');// 启http服务var server = http.createServer(function(req, res) {res.writeHead(200, {'Content-type': 'text/html'});res.end();});server.listen('8080');console.log('Server is running at port 8080...');// 监听socket连接socket.listen(server).on('connection', function(client) {// 接收信息client.on('message', function(msg) {client.send('hello:' + msg);console.log('data from client: ---> ' + msg);});// 断开处理client.on('disconnect', function() {console.log('Client socket has closed.');});});
