从一道题目谈谈跨域

那个时代的摇滚让人热泪盈眶。

1.基本概念

跨域是浏览器限制的,在xhr请求中,跨域问题尤为常见。浏览器针对跨域请求的策略:一个域名的JS,在未经允许的情况下,是不得读取另外一个域名的内容的。跨域问题不能够单单通过前端进行解决。

关于跨域常见的误区:

误区一:是服务端阻止的你跨域。NO! 上面也提到的,跨域是浏览器应用同源策略所限制的。

2.例子1:没有同源策略限制的接口请求

这里考虑使用cookie作为登录态的处理方法。假如你登录了a.com的话,然后接着登录b.com的话。并且这个b.com背地里偷偷摸摸做了一些暗操作:执行a.com网站的某个操作;我们知道对于cookie来说,浏览器会将它自动放到请求头里面的,所以假设没有同源策略的限制的话,那么b网站在背后偷偷对网站a发起请求那么就是可能的。但这样的操作所带来的问题不言而喻,因此基于同源策略的跨域解决是很有必要的。

3.解决跨域的方案之使用JSONP技术

何谓JSONP技术?大意就是指使用script标签向被跨域的服务器发起请求。当服务器收到这个请求后,便返回一串js代码,前端页面在收到服务端所返回的数据之后便能够执行这个请求。为什么JSONP技术能够用来进行跨域呢?因为script标签就是不受浏览器的跨域限制哇。接下来举一个例子来使用JSONP技术来解决:在将会跨域的情况下,服务端根据客户端传过来的type返回对应的列表数据。

1
2
3
4
5
6
7
8
9
10
11
12
// 这是前端页面代码
<script type="text/javascript">
let cb = function (arr) {
if (!arr || Object.prototype.toString.call(arr) != "[object Array]") { return; }
arr.map(str => {
let p = document.createElement('p');
p.innerText = str;
document.body.appendChild(p);
});
}
</script>
<script type="text/javascript" src="https:abcdwillcors.com/jsonp?type=article&cb=cb"></script>

注意query参数,这在JSONP中告诉了后端自己改怎么处理,处理完了该怎么返回。接下来看看后端相关代码(使用express)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const express = require('express');
const router = express.Router();

let mockDbQuery = type => {
let result = [];
switch (type) {
case 'article':
result = ['article1', 'article2'];
break;
case 'video':
result = ['video1', 'video2'];
break;
default:
result = ['article1', 'video1'];
}
return result;
}

router.get('/', function (req, res, next) {
let query = req.query;
res.cookie('sessionid', 'mock one');
let result = mockDbQuery(query.type || '');
let response = `${query.cb}(${JSON.stringify(result)})`; // 客户端接受到的应该是未经解释过的代码
res.send(response);
});

module.exports = router;

上面的例子便是使用JSONP技术来完成跨域功能处理。运行之后,前端将能够渲染出后端所返回的列表数据。

4.通过表单Form来解决POST请求跨域问题

对于上面的jsonp请求,很明显它支持GET请求的形式。尽管也能够携带有足够的参数,但是是满足不了POST请求的。为什么Form表单也没有被浏览器现在跨域?因为该种方式下,发起跨域的页面是无法获取新页面的内容的。而xhr确是有能力读取新页面返回的内容的。下面是一个使用form进行POST请求解决跨域的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 这是前端页面的代码
<script type="text/javascript">
let mockPost = (url, data) => {
let iframe = document.createElement('iframe');
let form = document.createElement('form');
let input = document.createElement('input');
iframe.style.display = 'none';
form.style.display = 'none';
iframe.name = 'mockPost';
form.action = url;
form.method = 'post';
form.target = iframe.name; // 避免在新页面中打开或者本页面刷新
document.body.appendChild(iframe);
for (let key in data) {
input.name = key;
input.value = data[key];
form.appendChild(input.cloneNode());
}
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
}

mockPost('http://apiwillcors/apiPost', { name: '3h', sex: 'man' });
</script>

接下来是后端部分的代码:

1
2
3
4
5
6
// 接着上个jsonp中的express例子
router.post('/apiPost', function (req, res, next) {
let body = req.body;
console.log('body is', body); // 打印出 { name: '3h', sex: 'man' }
res.send('done');
});

5.使用CORS协议处理跨域问题

CORS基本介绍:CORS的全程叫做cross origin resource sharing,翻译过来也就是叫做跨域资源共享。CORS协议用来避开浏览器的同源策略,是JSONP协议的现代版本,CORS能够更好的支持各种HTTP方法以及XMLHttpRequest。

CORS相比JSONP,具有下面这些特点:

1.CORS支持所有类型的HTTP方法,能够搭配XMLHttpRequest进行使用,能够更好的处理服务器的响应结果。

2.相比JSONP,CORS兼容性可能会没那么好。

CORS协议是被浏览器所使用的,所以不要奇怪为什么在postman里面是不受CORS限制的。CORS的实现思路是使用自定义的HTTP请求头来携带双方所规定好的请求头信息,这部分请求头信息被用于双方进行相互了解。如果浏览器支持CORS协议的话,但是双方的请求头信息不符合所约定的规则的话,那么也会出现跨域问题。

在CORS协议中,请求分为两种,分别是简单请求和非简单请求。满足下面这些限制条件的请求就属于简单请求:

(1).请求方法为GET,POST,HEAD中的一种;(2).HTTP头信息只能有Accept,Accept-Language,Content-Language,Last-Event-ID,Content-Type,而且Content-Type的值只能是application/x-www-form-urlencoded,multiple/form-data,text/plain。

对于简单请求来说,后端需要设置Access-Control-Origin响应头的值为*,如下所示(使用express举例):

1
2
3
4
5
6
7
8
9
10
11
12
// 接着上面的express例子
router.get('/cors', function (req, res, next) {
res.set('Access-Control-Allow-Origin', '*');
res.cookie('tokenId', 2);
res.send('fine');
});

// 下面是客户端的代码
fetch('localhost:port/jsonp/cors')
.then(res => {
console.log('res is', res);
});

很显然这个请求属于一个简单请求,并且我们的服务端还设置了Access-Control-Allow-Origin的值为*,意思就是告诉浏览器所有的Origin都能够访问我。因此此时即便是跨域了,但是也还是能够获取到数据的。

思考问题一:如果把Access-Control-Allow-Origin的值设置为某个具体的域名abc.com的话,并且发起跨域请求的网站的Origin并不是abc.com的话会发生什么情况?答案是会发生跨域,但是此时浏览器是能够收到服务器的正确响应结果的,只是不能通过js获取到返回值而已。把Access-Control-Allow-Origin值给取消所带来的结果也是一样。

接下来考虑对于非简单请求的CORS处理情况:

对于非简单请求来说,后端设置的请求头里面必不可少的信息是Access-Control-Allow-Origin(只要涉及到CORS,那个这个响应头信息必不可少),Access-Control-Request-Method,Access-Control-Allow-Headers这几个字段。是不是后端设置这个请求支持了非简单请求,那么前端就必须使用非简单请求方式发起呢?答案是不是的,你仍旧可以使用简单请求方式向这个接口发起请求,至于能否拿到正确的数据这就需要看后端的处理方式的。

下面看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 注意这里使用的是all,不支持采用这种方法,仅仅作为演示使用
router.all('corsm', function (req, res, next) {
res.set('Access-Control-Allow-Origin', '*');
res.set('Access-Control-Allow-Method', 'PUT,POST,GET,DELETE,OPTIONS');
res.set('Access-Control-Allow-Headers', 'Origin,Accept,t');
res.send('fine');
});

// 前端代码
fetch('http://someCom.com/jsonp/corsm', {
headers: {
t: 'test'
}
}).then(res => {
console.log('res is', res);
})

稍微解释一下上面这个例子:为什么服务端是all?这里主要是为了省事,对于非简单请求来说,首先浏览器会先发送一个OPTIONS请求,这叫做预检请求,只有等预检请求通过了才会接着发送后面的正式请求;而这里使用all只是为了省事处理,不推荐。接着在说下前端代码,为了表示这是个非简单请求,这里显式的加上了请求头t来表示这不是一个简单请求。如果把t这个自定义请求头给去掉的话,那么就是走的简单请求了,此时仍旧能够正常拿到响应数据。

6.使用代理来解决跨域问题

我们知道跨域是浏览器所导致的情况,因此如果需要解决这个问题的话,那么我们可以引入中间件来进行请求转发。这就要求中间件和前端网页是在同源下面的,而后端接口api不同源没关系,我们使用中间件将请求转发到真正的后端接口上面,此时就巧妙的避免了跨域问题。这种解决方法很普遍采用,并且这个中间件大多数都是NGINX。

我们可以看看下面这个配置:

1
2
3
4
5
6
7
location / {
proxy_pass http://127.0.0.1:5050/;
}

location /someapi/somePath/ {
proxy_pass http://127.0.0.1:20000/somePath/;
}

按照上面这个配置,当你访问这个域名,经过若干步骤后到达了服务器后,默认被转发到了服务器自身的5050端口上面,这个端口被中间件给监听着,它负责静态文件分发,当你的前端页面发起一个api请求后,此时若是路由匹配上了nginx所设置的/someapi/somePath/的话,那么会被转接给本机20000/somePath上面。在这个例子中,使用两个中间件,第一个便是nginx转发api请求,第二个便是5050端口上的中间件负责静态文件服务。可以整合为一个中间件,接口分发和静态文件分发都可以交给nginx承担。

7.postMessage解决跨域问题

8.canvas中的跨域问题

我们知道对于img标签来说,当然是不存在跨域问题的,比如你可以像下面这样使用而无需担心跨域问题:

1
2
3
4
5
let img = new Image();
img.onload = function () {
console.log('done');
}
img.src = 'http://timgsa.baidu.com/timg?image';

但是如果要是把这张图用于进行canvas绘制的话,并且调用canvasInstance.getImageData方法的话,那么便会出现跨域问题,如下所示:

1
2
3
4
5
6
7
8
9
// 部分报错原因:The canvas has been tainted by cross-origin data.
let canvas = document.createElement('canvas');
let context = canvas.getContext('2d');
let 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%27;';

为什么把图片绘制出来就没问题,调用getImageData或者toDataURL方法就会报跨域问题呢?因为此时会泄露信息。那么要怎么解决这个问题呢?告诉浏览器我请求别的域的图片的时候不用携带那个域名的敏感性信息即可,具体就是使用img.crossOrigin值即可。它有两个值可选,分别是’anonymous’和’use-credentials’这两个,第二个就表明我会携带敏感信息。需要注意的是,利用js对crossOrigin属性进行赋值的话,那么只要不是’use-credentials’那么就是表明是’anonymous’。所以对于上面这个问题,使用下面方式便可以解决:

1
2
3
4
5
6
7
8
9
10
let img = new Image();
img.onload = function () {
let canvas = document.createElement('canvas');
let context = canvas.getContext('2d');
context.drawImage(this, 0, 0); // 调用canvas绘制图片
context.getImageData(0, 0, this.width, this.height); // 此时能够正常获取到图片信息,不受干扰
document.body.appendChild(canvas);
};
img.crossOrigin = 'anonymous';
img.src = 'https://avatars3.githubusercontent.com/u/496048?s=120&v=4%27;';

上面这招算银弹吗?已经能够满足大部分使用场景了,但是对于ie10浏览器以下还是会有兼容性问题,所以该怎么解决呢?答案是利用ajax和URL.createObjectURL()方法曲线救国。

具体做法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let xhr = new XMLHttpRequest();
xhr.onload = function () {
let url = URL.createObjectURL(this.response);
let img = new Image();
img.onload = function () {
let canvas = document.createElement('canvas');
let context = canvas.getContext('2d');
context.drawImage(this, 0, 0);
context.getImageData(0, 0 , this.width, this.height);
document.body.appendChild(canvas);
};
img.src = url;
};
xhr.open('GET', 'https://avatars3.githubusercontent.com/u/496048?s=120&v=4%27;', true);
xhr.responseType = 'blob';
xhr.send();

这种方法的缺点就是会多一个请求,但是兼容性挺好。

9.问题

为什么通常在发送数据埋点请求的时候使用的是 1x1 像素的透明 gif 图片?

用来实现跨域,而且兼容性很好。如何用它来实现跨域?