Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JavaScript浅析 -- 同源策略和跨域 #18

Open
luoshaoxiong opened this issue Oct 2, 2018 · 0 comments
Open

JavaScript浅析 -- 同源策略和跨域 #18

luoshaoxiong opened this issue Oct 2, 2018 · 0 comments

Comments

@luoshaoxiong
Copy link
Owner

一、同源策略

什么是浏览器的同源策略?浏览器出于安全考虑,只允许相同域下的资源进行交互,不同源下的脚本在没有明确授权的情况下,不能读写对方的资源,这就是同源策略。

而所谓的同源,指的是协议、域名、端口号相同。举个例子:http://github.com:80,其中http://就是协议,而github.com就是域名,80是端口(默认端口可以省略)。只有这三个完全相同才算同源,任何一个不同都不算。假设当前的网址是http://www.a.com:80/a/index.js,同源情况如下:

  • https://www.a.com:80 不同源(协议不同)
  • http://www.a.com:90 不同源(端口号不同)
  • http://a.com:80 不同源(域名不同)
  • http://abc.a.com:80 不同源(域名不同)
  • http://www.b.com:80 不同源(域名不同)
  • http://192.110.110.110:80 不同源(192.110.110.110www.a.com对应的ip也认为域名不同)
  • http://www.a.com:80/b/index.js 同源(只要前部分相同,后面不同文件夹也可以)

如果两个网站不同源,则交互会受到限制,具体如下:

  • 不能共享cookie、storage和IndexedDB。
  • 不能互相操作dom。
  • 不能向非同源地址发送 AJAX 请求(可发但浏览器会拒绝接受响应)。

而为啥要有这个策略限制呢?试想一下,在一个窗口刚登录过银行网站http://www.bank.com,然后又切换到另一个页面http://hacker.com,如果这个与银行非同源页面能够访问到银行页面的cookie,而cookie里却保存着银行的账号和密码,这是一件非常危险的事情。

二、跨域方案

虽然这个同源策略是为了安全而诞生的。但有时候开发我们却需要有一些跨域请求或操作。比如使用第三方服务而需要请求第三方服务器,这就是所谓的跨域。

那如何避开浏览器的同源策略实现跨域呢?此处主要整理了八种方案,主要分为三类:

  • AJAX 请求的跨域
  • iframe 的跨域(又可分为主域相同和主域不同)
  • 通过服务器代理实现跨域

(一)AJAX 请求的跨域

1. JSONP

JSONP是跨域中非常常见的一种形式,他支持所有老版的浏览器。其主要是利用页面上请求<script>脚本不受同源策略限制的特性,来实现跨域。主要步骤如下:

  • 创建一个接收后处理数据的回调函数,并在被请求的url后面增加callback=funcName
  • 创建一个<script>元素,src指定为上面增加了callback的url。
  • 发起脚本请求,服务器返回数据后会自动执行script脚本从而执行了回调函数。

(1) 原生版本

// 前端页面代码
<script>
   // 服务器收到请求后会将数据放在回调函数的参数位置返回
    function jsonpCallback(data) {
        console.log(data.msg); // 作为参数的JSON数据被视为js对象而非字符串,不需要JSON.parse
    }
</script>
<script src="http://127.0.0.1:8080?callback=jsonpCallback"></script>
// 后端代码,node版本
const url = require('url');
const http = require('http');

// 启动http服务	
http.createServer((req, res) => {
	const data = { msg: 'success' };
	const callback = url.parse(req.url, true).query.callback; // 解析url取函数名
	res.writeHead(200); // 返回成功的状态码200
	res.end(`${callback}(${JSON.stringify(data)})`); // 向前端返回数据
}).listen(8080, '127.0.0.1'); // 监听来自127.0.0.1:8080端口的请求

(2)jquery的ajax版本

// 前端代码
$.ajax({
    url: 'http://127.0.0.1:8080',
    type: 'get',
    dataType: 'jsonp',  // 请求方式为jsonp
    jsonpCallback: "jsonpCallback",    // 回调函数名
    data: {}
});

JSONP的优点和缺点如下:

优点:
(1)支持所有浏览器即使是旧版浏览器。
(2)可用于k向一些不支持 CORS 的网站请求数据。
(3)不需要 XMLHttpRequest 或 ActiveX 的支持,所以不受 XMLHttpRequest 同源策略限制;请求完毕后自动调用 callback 并回传结果。

缺点:
(1)只支持get请求。
(2)无法捕获请求时的连接异常,只能通过超时进行处理。
(3)无法解决iframe页面之间的数据通信问题。

2. CORS

所谓CORS,全称Cross-Origin Resource Sharing跨域资源共享,是一个W3C标准,专门用于解决 AJAX 的跨域问题。它允许浏览器向跨源服务器发出 XMLHttpRequest 请求(如果没有增加 CORS 支持则不能向非同源发送 AJAX 请求,会被浏览器拦截)。

CORS 需要浏览器和服务器同时支持才生效,前端不需要做任何操作。当浏览器发现发送的请求是跨域请求时,会自动在请求头加上Origin字段表明当前的域(协议+域名+端口号),非简单请求还会先发送一次额外请求进行预检,但这都是浏览器自动完成的,用户和前端并不需要增加操作。而请求之后服务器也会返回一个http响应,如果返回的响应中带有Access-Control-Allow-Origin,并与上面请求头的Origin相匹配的话,那么即允许跨域;否则抛出错误被 XMLHttpRequest 的onerror回调函数捕获。注意,这种错误状态码可能是200,所以不能通过状态码去判断。

所以,可以见得,实现CORS的关键是后端要增加相应的字段。具体见下面的例子(还可以返回更多的头信息如Access-Control-Allow-Credentials等,此处只写了关键的一步):

// 服务端代码,node版本
const http = require('http');

// 启动http服务	
http.createServer((req, res) => {
	res.writeHead(200, {
		'Access-Control-Allow-Origin': '*', // 也可以直接写允许请求的域如http://www.baidu.com;*表示所有的域都可以请求,适合公开接口
		'Content-Type': 'text/html;charset=utf-8',
	});
	res.end();
}).listen(8080, '127.0.0.1'); // 监听来自127.0.0.1:8080端口的请求

CORS 的优缺点如下:

优点:
(1)比 JSONP 更强大,支持所有类型的 HTTP 请求。
(2)是W3C标准,大部分浏览器自动完成,只需服务器增加些许字段,非常方便。

缺点:仅支持 IE 10 以上等新版浏览器。

3. WebSocket

WebSocket 是HTML5一种新的协议。它实现了浏览器与服务器的双向通信,同时不受同源策略限制,允许跨域通讯,使用ws://(非加密)和wss://(加密)作为协议前缀。

// 前端代码,websocket版本
var ws = new WebSocket('ws://http://127.0.0.1:8080/websocket/chat'); // 创建连接,安全连接用wss

// 建立连接时调用
ws.onopen = function() {
    console.log('Connection open ...');
    ws.send('Hello WebSocket!'); // 发送消息给服务端 
}

// 接收服务器发送过来的消息
ws.onmessage = function() {
    var data = msg.data;
    if(typeof data === String) {
        console.log(data);
    }
    if(data instanceof ArrayBuffer) {
        // 处理ArrayBuffer逻辑...
    }
    ws.close(); // 关闭连接
}

// 关闭时调用
ws.onclose = function() {}

// 错误处理
ws.onerror = function(err) {}

一般使用WebSocket,我偏向使用socket.io。后者对前者的API进行了封装,使其更易用。前者只支持 IE 10 以上等新版浏览器,而后者兼容了旧版浏览器。所以此处增加个socket.io的例子。

// 前端代码,socket.io版本
<script src="./socket.io.js"></script>
<script>
var socket = io('http://127.0.0.1:8080');

// 连接成功处理
socket.on('connect', function() {
    // 监听接收服务端消息
    socket.on('message', function(data){});

    // 服务端关闭调用
    socket.on('disconnect', function(){});
});

socket.send('Hello WebSocket!'); // 发送消息给服务端
</script>
// 后端代码,node + socket.io版本
var http = require('http');
var socket = require('socket.io');

var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});
server.listen('8080');

// 监听连接
socket.listen(server).on('connection', function(client) {
    // 接收信息
    client.on('message', function(data) {
        client.send(data);
    });

    // 断开处理
    client.on('disconnect', function(){});
});

优点:
(1)不受同源策略的限制,支持与任意服务端通信。
(2)可以进行双向通信,服务端也可以主动给客户端推送消息。(传统的 HTTP 只允许客户端发起请求,只能用轮询来了解服务端是否更新信息,效率低浪费资源。)

(二)iframe实现跨域

iframe的跨域实现,主要有document.domain、location.hash、window.name、postMessage四种方案,下面我们一一解析。

1. document.domain

这种方法适合一级域名相同,二级域名不同的情况下使用。域名相关见如下:

  • 顶级域名:
    .com .net .edu .gov 等属于通用顶级域名
    .com.cn .net.cn .edu.cn 等属于带有国家地区代码顶级域名,而不是所谓的一级域名
  • 一级域名(又叫主域名)就是最靠近顶级域名左侧的字段,下面均属于一级域名:
    baidu.com qq.com
    baidu.com.cn qq.com.cn
  • 二级域名,即最靠近二级域名左侧的字段,二级及以上级别域名称为子域名:
    www.baidu.com www.qq.com
    www.baidu.com.cn www.qq.com.cn
  • 再接下来从右向左便可依次有三级域名、四级域名、五级域名等,依次类推即可。
// 主页面域名http://parent.main.com
<script>
    document.domain = 'main.com'; // 将两个窗口的域名都设置为一级域名
    let iframe = document.getElementsByTagName('iframe')[0];
    let data = iframe.contentWindow.data; // 获取子窗口里的data数据
</script>
// 子窗口域名http://child.main.com
<script>
    document.domain = 'main.com'; // 将子窗口的域名设置为一级域名
    let data = window.parent.data; // 获取父窗口里的data数据
</script>

缺点:只支持主域名相同的父子窗口通信。

2. location.hash

location.hash获取的是当前地址栏url的片段识别符,即http://example.com/x.html#fragment#及后面部分#fragment。由于单纯改变片段识别符并不会导致页面刷新,所以我们可以利用这个特性来让父子窗口互相传值。

// 父窗口修改子窗口的hash
var src = childURL + '#' + data;
document.getElementById('childIFrame').src = src;
// 父窗口监听hash改变
window.onhashchange = function() {
  var data = window.location.hash; // 获取子窗口传过来的数据
}

如果两个窗口不在同一个域下, IE、Chrome 不允许子窗口修改 parent.location.hash 的值,所以要借助于一个和父窗口同域的页面来实现修改 hash 值。

// 子窗口监听hash改变
window.onhashchange = function() {
  var data = window.location.hash; // 获取父窗口传过来的数据
}
// 子窗口也可以修改父窗口的hash,但需要一个与父窗口同域的代理窗口来修改hash值
let iframe = document.createElement('iframe');
iframe.src = parentURL;
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.onload = function() {
    parent.parent.location.href = parentURL + '#' + data;
}
// 同域可以直接用下面的写法
// parent.location.href = parentURL + '#' + data;

缺点:会改变url上面的#后的值,数据直接暴露在url上。

3. window.name

window.name是窗口的名字,每个子窗口也有自己的windowwindow.name。只能保存字符串,如果写入的值不是字符串,会自动转成字符串。其特殊之处在于只要窗口不关闭,这个属性便不会消失,且储存容量可高达几MB。如果加载了a.com之后写入window.name,窗口不关闭重新加载b.com,此时window.name还是a.com写入的值;窗口关闭window.name清除。

利用这个特性,我们可以在当前域下创建一个目标域的子窗口,目标域的数据放在window.name,加载完成后再让子窗口跳到一个父域相同的空白代理页(window.name还是不变),获取这个代理页的window.name赋值给父域即可。注意,一定要有与父域同域的代理页,否则父域是无法直接获取子窗口的window.name的;另外重新跳转页面会触发onload,要注意避免死循环,可以加个loaded标记。

// 当前页面,即http://parent.com
<script>
    let isIFrameLoaded = false, 
        data = '',
        iframe = document.createElement('iframe');
    iframe.style.display = 'none';
    iframe.src = 'http://target.com';
    document.body.appendChild(iframe);

    iframe.onload = function() {
        if(!isIFrameLoaded) { // 首次进入读取到window.name,然后刷新到代理页面window.name不变
            isIFrameLoaded = true; // 下面刷新会再次进入onload,此处标记为已完成避免死循环
            iframe.contentWindow.location = 'http://parent.com/proxy.html';
        } else {
            data = iframe.contentWindow.name; // 获取到目标域下的数据
            // 清除iframe
            iframe.contentWindow.document.write('');
            iframe.contentWindow.close();
            document.body.removeChild(iframe);
        }
    };
</script>
// 目标数据页面,即http://target.com,将想要提供的数据保存在window.name即可
<script>
    // 注意只能是字符串,若内容有引号根据需要可能要进行转义处理
    window.name = JSON.stringify({code: 0, result: {name: 'Peter', age: 18}});
</script>

缺点:
(1)比较繁琐,需要增加代理页面,还要避免重复循环等。
(2)目标数据要放在window.name,格式只能是字符串。
(3)缺少请求源控制,任何页面都可以按同样的方式访问到目标页面的数据。

4. window.postMessage

可以无论hash还是window.name都是利用一些特性来绕个弯达到目的,均属破解。为了解决该问题,HTML5 引入了一个新API跨文档通信 API(Cross-document messaging),无论两个窗口页面是否同源,都可以通过调用window.postMessage(content, target)来进行通信。其中target为协议域名端口号,可以设置“ * ”代表向全部窗口发送,也可以指定“ / ”代表当前域。父子窗口通过监听message事件可以获得来源方的这些信息event.origin(源网址)event.data(携带的数据)event.source(发送消息方的窗口)

// 父窗口,http://origin.com
let iframe = document.createElement('iframe');
let targetURL = 'http://target.com';
iframe.src = targetURL;
iframe.style.display = 'none';
document.body.appendChild(iframe);
iframe.onload = function() {
    // 引用子窗口触发其message事件
    iframe.contentWindow.postMessage('Hello target', targetURL);
}
// 监听当前窗口的message改变来获取数据
window.addEventListener('message', function(event) {
    console.log(event.data); // 获取目标页面的数据 ‘Hello origin’
});
// 子窗口或目标页面,http://target.com
window.addEventListener('message', receiveMessage);
function receiveMessage(event) {
    // 检验数据请求方是否为自己的网址
    if (event.origin !== 'http://origin.com') return;

    if (event.data === 'Hello World') {
        // 引用父窗口触发父窗口的message事件
        event.source.postMessage('Hello origin', event.origin);
    } else {
        console.log(event.data);
    }
}

优点:
(1)多个窗口(嵌套与否均可)之间可以进行跨域通信和操作window属性等,非常强大,也让跨域存储localStorage成为了可能。
(2)可以对来源进行校验,控制是否有权访问。

(三)服务器代理

最后一类实现跨域的方法是通过架设代理服务器来实现跨域。即**先请求同源服务器,再由同源服务器请求外部服务器。**由于请求的是同源服务器,所以不受浏览器同源策略限制,而服务器之间的请求也没有同源策略这一说,所以以此达到跨域目的。由于对服务器方面了解并不是很深,此处就不做展开,有兴趣可以自行了解下。

至此,八种跨域的方式终于讲完了(写了好久...吃掉每个小点再讲明白真不容易,可能还有图片ping啥的,先躺倒休息会...),以上全部都是简单案例可根据需求进行优化扩充,如有错漏,欢迎指出!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant