谈谈跨域

详解前端跨域的概念和解决方案。

何为跨域,为何跨域?

何为跨域

一言以蔽之:

只要协议、域名、端口有任何一个不同,都被当作是不同的域,之间的请求就是跨域操作。

比如http与https,80端口与81端口,a.com与b.com以上三个情况的各种组合都是不同域的,互相之前的请求就是跨域请求

浏览器同源策略

那所谓的跨域是怎么产生的呢?那么就要提到同源策略了。浏览器为了安全考虑就制定了一个同源策略,同源策略又分为以下几种

  1. DOM同源策略:禁止对不同源页面DOM进行操作。这里主要场景是iframe跨域的情况,不同域名的iframe是限制互相访问的。
  2. XmlHttpRequest同源策略:禁止使用XHR对象向不同源的服务器地址发起HTTP请求。就是所谓的ajax请求,生活中这个场景较多见。

这里主要介绍ajax跨域的情况。

另外,img,script,link,iframe等标签的src都是不受同源策略约束的,可以自由引用不同源的内容。
想想要是没有同源策略,a网站上的js就可以操作b网站上的DOM并且可以发起请求,这将是一个灾难!所以一切为了安全…

同源策略更详细的介绍:

为何跨域

总有这样那样的原因使我们想要请求别的网站的数据或者和别的网页交互。这个原因可以是

  1. 前后端分离开发,前端用了webpack-dev-server必然有个和后端不同的端口…
  2. 想要获取其他网站的内容比如教务处请求课表啊成绩啊之类的

跨域解决方案

首先明确一点对于端口和协议的不同,只能通过后台来解决。

既然提到解决方案当然要上代码,但是作为demo代码比较简略。后端使用nodejs的Express,浏览器用chrome。
本地服务器模拟不同域名最简单粗暴的方式是改host。。。文件位于C:\Windows\System32\drivers\etc
右键hosts用记事本打开,里面长这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
#
# For example:
#
# 102.54.94.97 rhino.acme.com # source server
# 38.25.63.10 x.acme.com # x client host

# localhost name resolution is handled within DNS itself.
# 127.0.0.1 localhost
# ::1 localhost

把测试的几个域名指向本地,也就是在这个文件的最后面加上以下几行内容

1
2
3
127.0.0.1 www.aaa.com
127.0.0.1 www.bbb.com
127.0.0.1 aaa.com

简单解释就是浏览器输入后面的三个网站时会默认打开本地的服务器(输网址记得加上http://,不然浏览器可能默认会加上www…)。测试完之后删了就好了,别的没什么影响。
需要注意的是如果有ss或者ssr记得关了它,chrome走代理的话就白改了,看不懂这句话请跳过。

然后我们需要知道的是,所谓的跨域是不管你这个域名对应的ip是啥,只看域名,对不上就跨域(就是个字符串处理),所以可以看到我们改了host文件吧三个域名都对应到了本地服务器来实现多个域名。
代码在这里,当然本文中我也会贴代码。。。

host的不同源环境配置完成,现在我们要打代码了,就是发几个跨域请求…
新建个文件夹crossorigin,命令行进去输入npm init -y初始化项目,然后yarn add express安装依赖
新建src文件夹,然后建立相应文件并写代码。

src/server1.js

1
2
3
4
5
6
7
8
9
10
11
const express = require('express');
const app = express();
const path = require('path');

app.use(express.static(path.join(__dirname,'./public')))

app.listen(8080)

app.get('/iv1/hello',(req,res,next)=>{
res.send('hello server 1')
})

src/server2.js

1
2
3
4
5
6
7
8
9
const express = require('express');
const app = express();
const path = require('path');

app.listen(8081)

app.get('/iv1/hello',(req,res,next)=>{
res.send('hello server 2')
})

src/public/index.html

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
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>跨域</title>
</head>

<body>
<button onclick="ajax1('http://www.aaa.com:8080/iv1/hello')">www.aaa.com:8080</button>
<button onclick="ajax1('http://aaa.com:8080/iv1/hello')">aaa.com:8080</button>
<button onclick="ajax1('http://www.bbb.com:8080/iv1/hello')">www.bbb.com:8080</button>
<button onclick="ajax1('http://www.aaa.com:8081/iv1/hello')">www.aaa.com:8081</button>
<button onclick="ajax1('http://127.0.0.1:8080/iv1/hello')">127.0.0.1:8080</button>
</body>
<script>
function ajax(url) {
let xhr = new XMLHttpRequest()
xhr.open('GET', url)
xhr.onload = e => {
if (xhr.status === 200) {
console.log(xhr.response)
} else {
console.log(xhr.status)
}
}
xhr.send()
}

function ajax1(url) {
fetch(url)
.then(res => res.text())
.then(data => {
console.log(data)
})
}
</script>

</html>

代码很简单,js部分开了两个服务器监听8080,8081端口,并用Express的static设置了静态目录,(就是说访问localhost:8080就是显示public/index.html)然后后端挂了个/iv1/hello的get接口,返回字符串‘hello server 1’,‘hello server 2’,html的代码就是放了几个按钮用于发起ajax请求,这里写了xhr和fetch两个方式,这是目前主流两种ajax方式。好了,在package.json的scripts里面加两个命令”server1”: “node ./src/server1.js”,”server2”: “node ./src/server2.js”,并执行npm run server1,npm run server2。访问http://www.aaa.com:8080/ 这个域名等效于localhost:8080 。因为我们host做了映射直接访问127.0.0.1了。打开控制台看console界面,然后依次点击5个按钮,可以看到,除了点第一个按钮出来了结果,其他几个都是报错说我们跨域了。那么,我们的旅程现在开始。

CORS

可以看到主要的报错信息是这样滴,上面那个是fetch的报错信息,下面是xhr的报错信息。

Failed to load http://aaa.com:8080/iv1/hello: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://www.aaa.com:8080' is therefore not allowed access. If an opaque response serves your needs, set the request’s mode to ‘no-cors’ to fetch the resource with CORS disabled.

 

Failed to load http://aaa.com:8080/iv1/hello: No ‘Access-Control-Allow-Origin’ header is present on the requested resource. Origin ‘http://www.aaa.com:8080' is therefore not allowed access.

划重点Origin 'http://www.aaa.com:8080' is therefore not allowed access.是说 “http://www.aaa.com:8080” 这个源不被允许访问“http://aaa.com:8080/iv1/hello”。 那么No 'Access-Control-Allow-Origin' header这是啥呢?它是说服务器上没有设置过这个头字段(header)。这也就引出了CORS的概念。CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing)阮一峰的这篇文章把CORS的细节讲的十分透彻了,我这里就不赘述,我就简单提一下代码怎么写,同时你也会明白怎么就莫名其妙的引出CORS了。

说出来你可能不信,代码十分简单。。。

在src/server1.js的app.get之前加上以下代码

1
2
3
4
app.use((req,res,next)=>{
res.setHeader('Access-Control-Allow-Origin','*')
next()
})

是不是看到了熟悉的’Access-Control-Allow-Origin’。其实这就是说web官方看你们那么想跨域,就制定了一个标准,要是后端的响应头设置了’Access-Control-Allow-Origin’这个字段,那么就允许你们跨域请求这个接口。
当然,一般情况一行代码不够用,emmmm…一行不够就三行喽。

1
2
3
4
5
6
app.use(function (req, res, next) {
res.setHeader("Access-Control-Allow-Origin", "*"); //允许哪些url可以跨域请求到本域,*表示全部
res.setHeader("Access-Control-Allow-Methods", "GET, POST"); //允许的请求方法,一般是GET,POST,PUT,DELETE,OPTIONS
res.setHeader("Access-Control-Allow-Headers", "x-requested-with,content-type,Token"); //允许哪些请求头可以跨域
next();
});

注释很详细啦~,一句话总结:CORS是为了跨域制定的标准,只要后端设置了响应头,符合要求的请求就可以跨域请求成功。
总结完加个ps:CORS需要浏览器本身支持,但是现代浏览器都是支持的,所以就提一下。(IE已被现代浏览器除名…)

pps:CORS默认不会带cookie,要带cookie的话js和服务端都要做额外设置。。。
对于客户端,我们还是正常使用xhr对象发送ajax请求,但是要设置:

1
xhr.withCredentials = true;

对于服务器端,需要在 response header中设置如下两个字段(注意第一个必须是具体域名,不能是*):

1
2
Access-Control-Allow-Origin: http://www.yourhost.com
Access-Control-Allow-Credentials:true

jsonp

相对于CORS有标准的支持,jsonp可以说只是一个小技巧。只不过这个技巧用多了就发现挺顺手然后大家都用了。。。
前文提到scrip标签的src引用的内容不受同源策略限制,那我们就手动创建一个script标签发起一个get请求,并且把某个函数名f作为参数传过去,并在全局定义一个f函数。后端返回一串字符串,字符串的形式是函数调用”f(data)”,script标签请求完后直接执行js就会执行f(data),然后就完成了跨域数据请求。

好吧,是有点绕。。。我们实践下,这次打开server2.js,之前CORS的代码写到了server1,server2是无法跨域的。

src/server2.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const express = require('express');
const app = express();
const path = require('path');

app.listen(8081)

app.get('/iv1/hello', (req, res, next) => {
res.send('hello server 2')
})

app.get('/iv1/jsonp', (req, res, next) => {
const funName = req.query.funName
const data = {
a: 1,
b: [1, 2]
}
res.send(`${funName}(${JSON.stringify(data)})`)
})

public/index.html最后在加两个按钮,然后script里面加个函数。(第一个按钮是测试跨域的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
...
<body>
...
<button onclick="ajax('http://www.bbb.com:8081/iv1/hello')">get www.bbb.com:8081</button>
<button onclick="jsonp()">jsonp www.bbb.com:8081</button>
</body>
<script>
...
function jsonp() {
const url = 'http://www.bbb.com:8081/iv1/jsonp?funName=logData';
window.logData = function (data) {
console.log(data)
}
const script = document.createElement('script');
script.setAttribute('src', url);
document.querySelector('body').appendChild(script);
}
</script>

</html>

然后访问http://www.aaa.com:8080/,点击最后两个请求,可以看到第一个被浏览器拦截说是跨域了,第二个正确log出了数据。

jsonp大概就是这样,人为伪造一个script标签发起请求并返回函数包裹数据的形式,前端拿到返回值直接当做js代码执行了。jsonp在CORS出现之前可是顶了半边天。。。但是它的限制也很大,首先最致命的事只支持get请求,毕竟本来就只是script标签的src属性发起的请求。。。然后就是后端要为jsonp类型的请求专门写一个接口。因为正常接口直接返回数据的话jsonp的接口需要返回函数名包裹数据的形式。

后端代理

这个方法就比较简单粗暴了。首先明确一点是一个简单请求虽然跨域了但是他是真的到达后端的,跨域的报错是浏览器拦截了那个请求的返回。对于非简单请求,浏览器会发一个方法为OPTIONS的请求,主要是去问问服务器是否允许跨域,不允许的话浏览器就报错了,允许的话浏览器就会发起正式的请求。可以看到,这一切都是浏览器的锅。。。所有的跨域都是浏览器的规则,那么就意味着我服务端发送的请求时可以得到正常响应的,于是就有了如下骚操作:host a的网页请求host a的某个接口,那个接口去请求host b的某个接口,并把请求的结果告诉host a的网页(浏览器)。对浏览器来说一切都是和自己家的后端交互,至于跨域请求那就让后端帮我们去请求并把结果告诉我就好啦,这就是所谓的代理。。。

上代码

1
2
3
4
<body>
...
<button onclick="ajax('http://www.aaa.com:8080/iv1/proxy')">www.aaa.com:8080后端代理www.bbb.com:8081</button>
</body>

src/server1.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
const http = require('http');
...
app.get('/iv1/proxy', (req, res, next) => {
http.get('http://www.bbb.com:8081/iv1/hello', function (_req, _res) {
let data = '';
_req.on('data', function (chunk) {
data += chunk;
});
_req.on('end', function () {
res.send(data);
});
});
})

可以看到html部分就是多了个按钮向自己的后端的代理接口发了个请求,后端首先引入了http库(nodejs自带)然后用http向目标url发了个get请求并在请求完成后把数据返回给前端。点击最后一个按钮可以看到数据成功打印在了控制台。

这个后端代理的好处就是完全不管啥同源策略什么的,暴力请求,请求玩数据扔给前端就行,emmmmm本质上就是个爬虫…