前言

生活中,经常会遇到这样一种情况,如果有陌生人找你问路,你可能会尽力帮助对方,而如果陌生人找你借钱,那十有八九你是不会借给他的,除非你们之间有人担保,或者他能证明自己不是骗子。前端也存在这种情况,当别人请求你的js或者图片时,可能你就之间给对方了,毕竟加载页面时都要用到,而别人想直接调用你的后台,那就需要你好好掂量一下了。

跨域

基础知识

上面那种现象就是跨域,想要理解跨域,首先要了解下面几个知识点。

  • 域名

域名是url的一部分,可以通过document.domain查看当前网页的域名,比如当前页面的域名就是www.yuque.com。

  • 协议

这里的协议指的是url最前面的一部分(应用层协议),代表访问资源的方式,比如常见的http,https等协议。

  • 端口

端口可以认为是设备与外界通讯交流的出口。一般的协议都有默认的端口,比如http的80,https的443等。

  • 同源策略(Same-origin Policy)

同源指的就是上面三个都相同的两个网页,听名字也能理解,大家同出一源,一家人不说两家话,想要什么自己拿。

下面的网站同源吗?

  1. http://www.jianshu.comhttp://jianshu.com
    不同源,因为域名不同
  2. http://www.bilibili.tvhttp://www.bilibili.com
    不同源,因为域名不同
  3. http://localhost:3000http://localhost:3001
    不同源,因为端口不同
  4. http://qq.comhttps://qq.com
    不同源,因为协议不同
  5. https://www.pixiv.nethttps://www.pixiv.net/manage/illusts/
    同源,因为域名协议端口都相同。

不同源会怎么样?

对于js脚本有限制。
主要表现在3点
(1) 无法用js读取非同源的Cookie、LocalStorage 和 IndexDB 。(毕竟你不可能随便把身份证给陌生人)
(2) 无法用js获取非同源的DOM 。(也不可能让陌生人住进你家)
(3) 无法用js发送非同源的AJAX请求 。更准确的说,js可以向非同源的服务器发请求,服务器会正常响应请求,但是返回的数据会被浏览器拦截。(也不可能让人拿着摄像机跟着你去取款机取钱)

是谁来执行同源策略的?

同源策略是由浏览器来执行,所有的限制都是浏览器的作用。这是浏览器为了保护用户的数据安全而采取的策略。

同源策略如何保护用户数据安全?没有同源策略会怎么样?

同源策略对于js的限制有3点,我们一点一点来说。

(1) 无法用js读取非同源的Cookie、LocalStorage 和 IndexDB 无法读取。

这条很好理解。
为了防止恶意网站通过js获取用户其他网站的cookie。

(2) 无法用js获取非同源的DOM 。

如果没有这一条,恶意网站可以通过iframe打开银行页面,可以获取dom就相当于可以获取整个银行页面的信息。

(3) 无法用js发送非同源的AJAX请求 。

这一条我一开始也没搞懂为什么。如果没有这一条会有什么危害?
看了这篇文章后我终于懂了https://blog.csdn.net/hcrw01/article/details/84289109
我把这部分复制过来放在这里。

假设有一个黑客叫做小黑,他从网上抓取了一堆美女图做了一个网站,每日访问量爆表。 为了维护网站运行,小黑挂了一张收款码,觉得网站不错的可以适当资助一点,可是无奈伸手党太多,小黑的网站入不敷出。 于是他非常生气的在网页中写了一段js代码,使用ajax向淘宝发起登陆请求,因为很多数人都访问过淘宝,所以电脑中存有淘宝的cookie,不需要输入账号密码直接就自动登录了,然后小黑在ajax回调函数中解析了淘宝返回的数据,得到了很多人的隐私信息,转手一卖,小黑的网站终于盈利了。 如果跨域也可以发送AJAX请求的话,小黑就真的获取到了用户的隐私并成功获利了!!!

如何跨域请求

跨域一般有几种做法:

  1. CORS
  2. JSONP
  3. document.domain + iframe跨域
  4. location.hash + iframe
  5. window.name + iframe跨域
  6. postMessage跨域
  7. Nginx代理跨域
  8. Nodejs中间件代理跨域
  9. WebSocket协议跨域
  10. H5资源文件crossOrigin属性跨域

TODO

CORS

具体内容请参考:https://www.cnblogs.com/loveis715/p/4592246.html
实际操作主要是通过服务端的Access-Control-Allow-Origin头来实现,这个头会返回一个站点列表,来告诉浏览器,哪些域的跨域请求是合法的。
 现在我们就来看一个通过CORS来进行跨域访问的简单示例。假设ambergarden.com想从一个公有数据平台public-data.com中返回一些数据,那么在页面逻辑中,其可以通过下面的代码向public-data.com发送数据请求:

  1. function retrieveData() {
  2. var request = new XMLHttpRequest();
  3. request.open('GET', 'http://public-data.com/someData', true);
  4. request.onreadystatechange = handler;
  5. request.send();
  6. }

在运行这段代码的之后,浏览器会向服务发送如下的请求:

  1. GET /someData/ HTTP/1.1
  2. Host: public-data.com
  3. ......
  4. Referer: http://ambergarden.com/somePage.html
  5. Origin: http://ambergarden.com

而一个支持CORS协议的服务可能会给出下面的响应:

HTTP/1.1 200 OK
Access-Control-Allow-Origin: http://ambergarden.com
Content-Type: application/xml
......
[Payload Here]

注意,这里我们前端的域是http://ambergarden.com而后端的域是http://public-data.com,通过后端设置Access-Control-Allow-Origin完成了跨域请求。

JSONP

具体内容参考:https://blog.csdn.net/hansexploration/article/details/80314948
我们在平时的开发中可以发现,js、css、iframe等带有src属性的标签,都拥有跨域的能力,css里一般做不了什么特别的,但是js和iframe就值得我们大书特书了。这里说的JSONP就用到了JS,我们可以在请求JS时传入一定参数,然后请求的实际不是一个js,而是一个后端接口,只是这个后端接口返回给你一个符合js语法的字符串,于是你的浏览器就解析并执行返回的这段js代码了。当然你可以只传一个JSON对象,但一般来说你请求这个数据总是想做点什么,怎么办呢,就是使用一个自己和服务器都知道的回调函数(当然你也可以本地存在多个回调并根据情况告诉服务器该使用哪个回调)。
下面是一个简单的例子:


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
    <title></title>
    <script type="text/javascript">
    // 得到航班信息查询结果后的回调函数
    var flightHandler = function(data){
        alert('你查询的航班结果是:票价 ' + data.price + ' 元,' + '余票 ' + data.tickets + ' 张。');
    };
    // 提供jsonp服务的url地址(不管是什么类型的地址,最终生成的返回值都是一段javascript代码)
    var url = "http://flightQuery.com/jsonp/flightResult?code=CA1998&callback=flightHandler";
    // 创建script标签,设置其属性
    var script = document.createElement('script');
    script.setAttribute('src', url);
    // 把script标签加入head,此时调用开始
    document.getElementsByTagName('head')[0].appendChild(script); 
    </script>
</head>
<body>

</body>

我们可以看到,我们创建了一个script标签,并设置他的src为一个跨域的请求,带上了查询参数code,并告诉服务端回调函数是flightHandler,然后将script标签加入head中,浏览器就会自动去请求数据了。请求完成后会自动调用flightHandler方法。
我们常用的JQuery也提供了jsonp调用的封装,和ajax共用一套API,只是参数略微变了点,不过ajax和jsonp完全是两个不同的东西,不要混淆了。

document.domain + iframe跨域

此方案仅限主域相同,子域不同的跨域应用场景。
实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。
1.)父窗口:(http://www.domain.com/a.html))

<iframe id="iframe" src="http://child.domain.com/b.html"></iframe>
<script>
    document.domain = 'domain.com';
    var user = 'admin';
</script>

2.)子窗口:(http://child.domain.com/b.html))

<script>
    document.domain = 'domain.com';
    // 获取父窗口中变量
    alert('get js data from parent ---> ' + window.parent.user);
</script>

为什么不能实现主域不同的跨域?这是因为document.domain在修改上有限制,你只能修改成当前域名或者当前域名的基础域名。看完下面的例子你就明白了:
image.png

location.hash + iframe

实现原理: a欲与b跨域相互通信,通过中间页c来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。
具体实现:A域:a.html -> B域:b.html -> A域:c.html,a与b不同域只能通过hash值单向通信,b与c也不同域也只能单向通信,但c与a同域,所以c可通过parent.parent访问a页面所有对象。
1.)a.html:(http://www.domain1.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>

2.)b.html:(http://www.domain2.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>

3.)c.html:(http://www.domain1.com/c.html))

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

上面可以看到,虽然b没法直接调用a的方法,但可以通过改变c的值间接调用a的方法,因为a和c是同源的。

window.name + iframe跨域

window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
1.)a.html:(http://www.domain1.com/a.html))

var proxy = function(url, callback) {
    var state = 0;
    var iframe = document.createElement('iframe');
    // 加载跨域页面
    iframe.src = url;
    // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name
    iframe.onload = function() {
        if (state === 1) {
            // 第2次onload(同域proxy页)成功后,读取同域window.name中数据
            callback(iframe.contentWindow.name);
            destoryFrame();
        } else if (state === 0) {
            // 第1次onload(跨域页)成功后,切换到同域代理页面
            iframe.contentWindow.location = 'http://www.domain1.com/proxy.html';
            state = 1;
        }
    };
    document.body.appendChild(iframe);
    // 获取数据以后销毁这个iframe,释放内存;这也保证了安全(不被其他域frame js访问)
    function destoryFrame() {
        iframe.contentWindow.document.write('');
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
    }
};
// 请求跨域b页面数据
proxy('http://www.domain2.com/b.html', function(data){
    alert(data);
});

2.)proxy.html:(http://www.domain1.com/proxy….)
中间代理页,与a.html同域,内容为空即可。
3.)b.html:(http://www.domain2.com/b.html))

<script>
    window.name = 'This is domain2 data!';
</script>

总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

postMessage跨域

postMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:
a.) 页面和其打开的新窗口的数据传递
b.) 多窗口之间消息传递
c.) 页面与嵌套的iframe消息传递
d.) 上面三个场景的跨域数据传递
用法:postMessage(data,origin)方法接受两个参数
data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
origin: 协议+主机+端口号,也可以设置为”*”,表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为”/“。
1.)a.html:(http://www.domain1.com/a.html))

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;"></iframe>
<script>       
    var iframe = document.getElementById('iframe');
    iframe.onload = function() {
        var data = {
            name: 'aym'
        };
        // 向domain2传送跨域数据
        iframe.contentWindow.postMessage(JSON.stringify(data), 'http://www.domain2.com');
    };
    // 接受domain2返回数据
    window.addEventListener('message', function(e) {
        alert('data from domain2 ---> ' + e.data);
    }, false);
</script>

2.)b.html:(http://www.domain2.com/b.html))

<script>
    // 接收domain1的数据
    window.addEventListener('message', function(e) {
        alert('data from domain1 ---> ' + e.data);
        var data = JSON.parse(e.data);
        if (data) {
            data.number = 16;
            // 处理后再发回domain1
            window.parent.postMessage(JSON.stringify(data), 'http://www.domain1.com');
        }
    }, false);
</script>

Nginx代理跨域

1、 nginx配置解决iconfont跨域

浏览器跨域访问js、css、img等常规静态资源被同源策略许可,但iconfont字体文件(eot|otf|ttf|woff|svg)例外,此时可在nginx的静态资源服务器中加入以下配置。

location / {
  add_header Access-Control-Allow-Origin *;
}

2、 nginx反向代理接口跨域

跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。
实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。
nginx具体配置:

#proxy服务器
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;
    }
}

1.) 前端代码示例:

var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();

2.) Nodejs后台示例:

var http = require('http');
var server = http.createServer();
var qs = require('querystring');
server.on('request', function(req, res) {
    var params = qs.parse(req.url.substring(2));
    // 向前台写cookie
    res.writeHead(200, {
        'Set-Cookie': 'l=a123456;Path=/;Domain=www.domain2.com;HttpOnly'   // HttpOnly:脚本无法读取
    });
    res.write(JSON.stringify(params));
    res.end();
});
server.listen('8080');
console.log('Server is running at port 8080...');

Nodejs中间件代理跨域

node中间件实现跨域代理,原理大致与nginx相同,都是通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

1、 非vue框架的跨域(2次跨域)

利用node + express + http-proxy-middleware搭建一个proxy服务器。
1.)前端代码示例:

var xhr = new XMLHttpRequest();
// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;
// 访问http-proxy-middleware代理服务器
xhr.open('get', 'http://www.domain1.com:3000/login?user=admin', true);
xhr.send();

2.)中间件服务器:

var express = require('express');
var proxy = require('http-proxy-middleware');
var app = express();
app.use('/', proxy({
    // 代理跨域目标接口
    target: 'http://www.domain2.com:8080',
    changeOrigin: true,
    // 修改响应头信息,实现跨域并允许带cookie
    onProxyRes: function(proxyRes, req, res) {
        res.header('Access-Control-Allow-Origin', 'http://www.domain1.com');
        res.header('Access-Control-Allow-Credentials', 'true');
    },
    // 修改响应信息中的cookie域名
    cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
}));
app.listen(3000);
console.log('Proxy server is listen at port 3000...');

3.)Nodejs后台同(六:nginx)

2、 vue框架的跨域(1次跨域)

利用node + webpack + webpack-dev-server代理接口跨域。在开发环境下,由于vue渲染服务和接口代理服务都是webpack-dev-server同一个,所以页面与代理接口之间不再跨域,无须设置headers跨域信息了。
webpack.config.js部分配置:

module.exports = {
    entry: {},
    module: {},
    ...
    devServer: {
        historyApiFallback: true,
        proxy: [{
            context: '/login',
            target: 'http://www.domain2.com:8080',  // 代理跨域目标接口
            changeOrigin: true,
            secure: false,  // 当代理某些https服务报错时用
            cookieDomainRewrite: 'www.domain1.com'  // 可以为false,表示不修改
        }],
        noInfo: true
    }
}

WebSocket协议跨域

WebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。
原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
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.'); 
    });
});

H5资源文件crossOrigin属性跨域

https://www.zhangxinxu.com/wordpress/2018/02/crossorigin-canvas-getimagedata-cors/
对于跨域的图片,只要能够在网页中正常显示出来,就可以使用canvas的drawImage() API绘制出来。但是如果你想更进一步,通过getImageData()方法获取图片的完整的像素信息,则多半会出错。

举例来说,使用下面代码获取github上的自己头像图片信息:
var canvas = document.createElement(‘canvas’);
var context = canvas.getContext(‘2d’);

var img = new Image();
img.onload = function () {
context.drawImage(this, 0, 0);
context.getImageData(0, 0, this.width, this.height);
};
img.src = ‘https://avatars3.githubusercontent.com/u/496048?s=120&v=4‘;’;
结果在Chrome浏览器下显示如下错误:

Uncaught DOMException: Failed to execute ‘getImageData’ on ‘CanvasRenderingContext2D’: The canvas has been tainted by cross-origin data.

前端跨域 - 图2
Firefox浏览器错误为:

SecurityError: The operation is insecure.

如果使用的是canvas.toDataURL()方法,则会报:

Failed to execute ‘toDataURL’ on ’HTMLCanvasElement’: Tainted canvased may not be exported

原因其实都是一样的,跨域导致。
那有没有什么办法可以解决这个问题呢?
可以试试crossOrigin属性。

HTML crossOrigin属性解决资源跨域问题
**在HTML5中,有些元素提供了支持CORS(Cross-Origin Resource Sharing)(跨域资源共享)的属性,这些元素包括<img><video><script>等,而提供的属性名就是crossOrigin属性。
因此,上面的跨域问题可以这么处理:

var canvas = document.createElement('canvas');
var context = canvas.getContext('2d');
var img = new Image();
img.crossOrigin = '';
img.onload = function () {
    context.drawImage(this, 0, 0);
    context.getImageData(0, 0, this.width, this.height);
};
img.src = 'https://avatars3.githubusercontent.com/u/496048?s=120&v=4';';

增加一个img.crossOrigin = ''即可,虽然JS代码这里设置的是空字符串,实际上起作用的属性值是anonymous
crossOrigin可以有下面两个值:

关键字 释义
anonymous 元素的跨域资源请求不需要凭证标志设置。
use-credentials 元素的跨域资源请求需要凭证标志设置,意味着该请求需要提供凭证。

其中,只要crossOrigin的属性值不是use-credentials,全部都会解析为anonymous,包括空字符串,包括类似'abc'这样的字符。
例如:

img.crossOrigin = 'abc';
console.log(img.crossOrigin);    // 结果是'anonymous'

前端跨域 - 图3
另外还有一点需要注意,那就是虽然没有crossOrigin属性,和设置crossOrigin="use-credentials"在默认情况下都会报跨域出错,但是性质上却不一样,两者有较大区别。
crossOrigin兼容性
IE11+(IE Edge),Safari,Chrome,Firefox浏览器均支持,IE9和IE10会报SecurityError安全错误,如下截图:
前端跨域 - 图4

crossOrigin属性为什么可以解决资源跨域问题?
**crossOrigin=anonymous相对于告诉对方服务器,你不需要带任何非匿名信息过来。例如cookie,因此,当前浏览器肯定是安全的。
就好比你要去别人家里拿一件衣服,crossOrigin=anonymous相对于告诉对方,我只要衣服,其他都不要。如果不说,可能对方在衣服里放个窃听器什么的,就不安全了,浏览器就会阻止。

IE10浏览器不支持crossOrigin怎么办?
**我们请求图片的时候,不是直接通过new Image(),而是借助ajax和URL.createObjectURL()方法曲线救国。
代码如下:

var xhr = new XMLHttpRequest();
xhr.onload = function () {
    var url = URL.createObjectURL(this.response);
    var img = new Image();
    img.onload = function () {
        // 此时你就可以使用canvas对img为所欲为了
        // ... code ...
        // 图片用完后记得释放内存
        URL.revokeObjectURL(url);
    };
    img.src = url;
};
xhr.open('GET', url, true);
xhr.responseType = 'blob';
xhr.send();

此方法不仅IE10浏览器OK,原本支持crossOrigin的诸位浏览器也是支持的。
也就多走一个ajax请求,还可以!
根据,根据实践发现,在IE浏览器下,如果请求的图片过大,几千像素那种,图片会加载失败,我猜是超过了blob尺寸限制。