所谓跨域,就是浏览器对于JavaScript的同源策略的限制。
浏览器认为跨域情况下的数据请求是不安全的。
那么同源策略是什么呢?
同源策略
同源保证指A网页设置的Cookie,B网页不能打开,除非这两个网页同源,同源指的是:
- 协议相同(protocol)
- 域名相同(host)
- 端口相同(port)
这三个中任意一个不同,网站间的数据请求与传输就构成了跨域调用,会受到同源策略的限制。
举例来说,http://www.example.com,`http://`就是协议,`www.example.com`就是域名,端口是`80`(默认端口可以省略)
同源策略限制了从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键安全机制。
解决跨域
虽然同源策略是出于安全考虑,但是在使用时会有很多麻烦,所以我们还是会想办法规避它的限制。
我们可以写一个简单的ajax请求作为请求失败的例子:
- html部分就只有一个按钮,点击按钮会向
http://localhost:9000
发送一个ajax请求
1 | <input type="submit" id="submit" value="提交" /> |
- 后端部分是一个简单的服务器,只要接收到请求,就回复一个字符串
1 | const http = require("http"); |
页面如下:
点击后我们可以在控制台看到报错信息:
那么解决方法有哪些呢?
JSONP
在HTML标签里,有一些标签比如img, script是不受跨域限制的,所以我们可以利用这一点绕过限制。
- 前端
1 | <button id='btn'>点我发送ajax请求</button> |
- 后端
1 | // 1. 引入必要模块 |
我们做了这样几件事:
我们在前端代码中写了一个
script
文件的内容,并且插入到body当中,这个文件会和其他script
文件一样被执行,这里如果写成平时嵌入的script文件的形式,应该是这样的(我们假设callback函数名是callback_1234):1
2
3
4
5<script src="http://localhost:9000?calback=callback_1234">
//现在已有一个定义好的全局函数,即callback_1234
//现在要执行的内容就是返回回来的data
${data}
</script>前端的请求发送到了
http://localhost:9000
,后端拿到的完整req.url
是http://localhost:9000/?callback=callback_1234
,我们用url
模块解析url,可以获取到query
是一个json数据1
{callback: callback_1234}
我们将获取到的
callback
数据保存为fnName
,这样方便我们加入我们想要返回给前端的数据,然后让前端依照这个函数执行我们先尝试只返回给前端一个字符串,我们希望前端能输出这个字符串:
1
2res.end(fnName + "('hello world')");
//实际返回给前端的内容就是callback_1234("hello world")前端收到了这个字符串
callback_1234("hello world")
,前面提到前端已经有这个callback_1234
的全局函数了,那么前端要执行的${data}
就是:1
2
3<script src="http://localhost:9000?calback=callback_1234">
callback_1234("hello world");
</script>即:
1
console.log("hello world");
我们再看一看浏览器的控制台
这样我们就实现了用jsonp的方法来解决跨域。
需要注意的是,response.end()
方法只能传输字符串或者Buffer,如果我们需要传输其他数据结构,还需要添加解析的步骤:
1 | <button id="btn">点我发ajax请求</button> |
1 | const http = require('http'); |
jsonp总结:
- 前端代码
- 创建script标签
- 添加全局函数
- 传递函数名到后端
- 后端代码
- 获取响应数据
- 返回函数名(“数据”)的字符串给前端script执行
特征:
- jsonp的请求方式是GET
- 没有兼容性问题
CORS(跨域资源共享)
CORS,跨域资源共享(Cross-Origin Resource Sharing), 是一个W3C标准。它允许浏览器向跨源服务器发出XMLHttpRequest
请求,从而克服了AJAX只能同源使用的限制。
完成CORS通信需要浏览器和服务器同时支持,目前浏览器都支持该功能,IE浏览器不能低于IE10。服务器则需要实现CORS接口。
CORS的两种请求
浏览器将CORS请求分为两种:简单请求和非简单请求。
简单请求
满足:
- 方法: HEAD / GET / POST
- HTTP头信息不超出以下几种字段
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type(只接受以下3个值)
- application/x-www-form-urlencoded
- multipart/form-data
- text/plain
通信条件:
- 服务器设置的
Access-Control-Allow-Origin
中包含CORS请求,这样有两种方式,一个是设置成CORS请求中的origin
,还有一种是设置成*
,即接受任意域名的请求; Access-Control-Allow-Credentials
设置一个布尔值,表示是否允许浏览器发送Cookie给服务器。设置成true
表示服务器明确允许浏览器把Cookie放在请求中一起发给服务器。事实上,这个值也只能设为true
,服务器如果不需要cookie,删除该字段即可;- CORS请求默认不发送Cookie和HTTP认证信息,所以服务器即使设置
Access-Control-Allow-Credentials: true
也不表示浏览器就会发送cookie,因为还需要开发者在AJAX请求中打开withCredentials
属性,将其设置为true
- 需要注意,如果要发送cookie,
Access-Control-Allow-Origin
就不能设置为*
,必须指定请求中的域名(如果所有域名都可以获取cookie,那也是挺危险的) - 同样地,cookie也会遵循同源策略,只会将设置为服务器域名的cookie发送给服务器,其他域名下的cookie不会发送
Access-Control-Expose-Headers
用来指定特定字段
1 | <button id="btn">点我发ajax请求</button> |
1 | const http = require('http'); |
非简单请求
非简单请求即不满足上面简单请求的两个要求,一般有三种情况:
- 请求方法是
GET
或POST
之外的方法,如PUT
或DELETE
- 请求头里包含自定义头
Content-Type
是application/json
非简单请求会在正式通信前请示服务器,发送一个OPTIONS预检请求(preflight),询问服务器当前页面所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有服务器许可之后,浏览器才会发出正式的
XMLHttpRequest
请求,否则就会报错。预检请求的请求方法是
OPTIONS
,表明这个请求是用来询问的。我们还是用上面的例子做演示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14<button id="btn">点我发ajax请求</button>
<script type="text/javascript">
let oBtn = document.getElementById("btn");
oBtn.onclick = function() {
let xhr = new XMLHttpRequest();
xhr.open("get", "http://localhost:9000", true);
xhr.send();
xhr.onreadystatechange = function() {
if(xhr.readyState === 4 && xhr.status === 200) {
console.log("succeed: " + xhr.responseText);
}
}
}
</script>1
2
3
4
5
6
7
8
9const http = require('http');
let server = http.createServer((req,res)=>{
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Credentials', true);
res.end("hello world");
});
server.listen(9000,()=>{
console.log("server running at 9000");
})第一个触发点(get/post方法之外)
1
2//xhr.open("get", "http://localhost:9000", true);
xhr.open("put", "http://localhost:9000", true);结果:
第二个触发点(加入自定义头)
1
2
3xhr.open("get", "http://localhost:9000", true);
xhr.setRequestHeader('my-token', 'abc');
xhr.send();结果:
第三个触发点(
Content-Type
是application/json
)1
2
3xhr.open("get", "http://localhost:9000", true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.send();结果:
我们可以从报错的信息里看到,预检请求的头信息中有两个重要的特殊字段:
Access-Control-Request-Methods
:用来列出CORS请求用到哪些HTTP方法Access-Control-Request-Headers
:指定CORS请求会额外发送的头字段信息
我们在例子中加入这两个设置:
1
2
3
4
5
6
7
8
9
10
11
12
13let oBtn = document.getElementById("btn");
oBtn.onclick = function() {
let xhr = new XMLHttpRequest();
xhr.open("get", "http://localhost:9000", true);
xhr.setRequestHeader('my-token', 'abc')
xhr.setRequestHeader('Content-Type', 'application/json')
xhr.send();
xhr.onreadystatechange = function() {
if(xhr.readyState === 4 && xhr.status === 200) {
console.log("succeed: " + xhr.responseText);
}
}
}1
2
3
4
5
6
7
8
9
10const http = require('http');
let server = http.createServer((req,res)=>{
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Credentials', true);
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'my-token, content-type')
res.end("hello world");
});
server.listen(9000,()=>{
console.log("server running at 9000");结果:
这时我们可以在NETWORK里看到两次请求:
预检请求
XMLHttpRequest
请求
- 请求方法是
总结:
- CORS需要在服务器端编写代码,浏览器端正常发送ajax请求
- 和JSONP相比,CORS支持所有类型的HTTP请求,JSONP的优势则在于支持老式浏览器,以及可以向不支持CORS的网站请求数据
代理
服务器本身不受浏览器同源策略的影响,所以我们可以通过代理来实现跨域数据请求。
核心思路:
跨域的本质是浏览器向服务器请求数据,浏览器和服务器不同源,导致请求被拒。那么如果我们能让服务器代为发送请求,再由服务器把数据返回给浏览器的话,就可以规避限制了。所以我们实现代理要做的就是:
- 浏览器将需要发送的请求告诉服务器(而不是自己发送请求)
- 服务器接收到浏览器告诉自己的请求要求并替浏览器发送请求
- 服务器获取到请求返回的数据,再回传给浏览器
配置nginx
我们首先需要配置nginx。
下载
1
brew install nginx
启动
1
nginx
nginx安装后,我们可以在浏览器访问,默认端口是8080,在http://localhost:8080
我们会看到这样的界面:
接着需要配置,我在mac上的配置文件路径是/usr/local/etc/nginx
,找到nginx.config文件。
1 | location /apis { |
配置好之后,我们可以开始写代理服务器的代码。这里我们拿豆瓣api来做例子。
豆瓣api接口:https://api.douban.com
正在热映:
/v2/movie/in_theaters
我们请求正在热映的电影信息就是输入以上两部分地址,直接在地址输入https://api.douban.com/v2/movie/in_theaters
,可以看到:
(有可能会是写成一堆的json数据,我安装了插件,所以信息美化了)
确认好地址无误后,我们在html页面里用XMLHttpRequest将地址发送给服务器。
1 | <button id="btn">点我发本服务器的node代理</button> |
1 | // 1. 引入模块 |
点击按钮会返回豆瓣数据:
我们可以解析一下这些步骤
登录的时候,直接到localhost:8888
页面,服务器解析到url是/
,于是把proxy.html页面的代码返回给浏览器,浏览器解析出来,呈现在页面上。
请求数据则是浏览器只把请求内容告诉服务器,服务器代为请求。这里可以把前端代码中的豆瓣api地址部分替换成任何其他我们想要请求的数据地址。
以上就是常见的解决跨域的三种方法:
- JSONP
- CORS
- 代理