一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第24天,点击查看活动详情。
因为浏览器出于安全考虑,有同源策略。也就是说,如果协议、域名或者端口有一个不同就是跨域,Ajax
请求会失败。
解决跨域有很多种方法,其本质就是搞定同源策略。常用的有 jsonp
、iframe
、cors
、img
、HTML5 postMessage
等等。其中还有用到 html
标签进行跨域的原理就是 html
不受同源策略影响。但只是接受 Get
的请求方式,这个需要注意。
我们知道同源策略并不是禁止跨域请求,而是在请求后拦截了请求的回应。但是引用图片、css、js 文件等资源的时候就允许跨域。
1. JSONP[利用script标签]
JSONP
的原理很简单,就是利用 <script>
标签没有跨域限制的漏洞。通过 <script>
标签指向一个需要访问的url
并提供一个回调函数拼在url
中来接收数据当需要通讯时。这种方式需要后端接口根据实际情况配合,约定好回调函数的函数名。
<!-- 接口传参并指定回调执行函数名为jsonp -->
<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script>
<script>
// 回调函数
function jsonp(data) {
console.log(data)
}
</script>
script跨域的优缺点
- 优点:可以直接返回
json
格式的数据,方便处理 - 缺点:只接受
GET
请求方式
2. postMessage
H5
中新增的postMessage()
方法,可以用来做跨域通信。
这种方式通常用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另一个页面判断来源并接收消息。
// 发送消息端
window.parent.postMessage('message', 'http://blog.poetries.com');
// 接收消息端
var mc = new MessageChannel();
mc.addEventListener('message', (event) => {
var origin = event.origin || event.originalEvent.origin;
if (origin === 'http://blog.poetries.com') {
console.log('验证通过')
}
});
场景示例:窗口 A (http:A.com
)向跨域的窗口 B (http:B.com
)发送信息。步骤如下
// 窗口A(http:A.com)向跨域的窗口B(http:B.com)发送信息
Bwindow.postMessage('data', 'http://B.com'); //这里强调的是B窗口里的window对象
// 在窗口B中监听 message 事件
Awindow.addEventListener('message', function (event) { //这里强调的是A窗口里的window对象
console.log(event.origin); //获取 :url。这里指:http://A.com
console.log(event.source); //获取:A window对象
console.log(event.data); //获取传过来的数据
}, false);
3. document.domain + iframe
- 此方案仅限主域相同,子域不同的跨域应用场景。比如
a.test.com
和b.test.com
适用于该方式。 - 只需要给页面添加
document.domain = 'test.com'
表示二级域名都相同就可以实现跨域
具体示例操作
- 父窗口:
http://www.test.com/a.html
<iframe id="iframe" src="http://child.test.com/b.html"></iframe>
<script>
document.domain = 'test.com';
var user = 'admin';
</script>
- 子窗口:
http://child.test.com/b.html
<iframe id="iframe" src="http://child.test.com/b.html"></iframe>
<script>
document.domain = 'test.com';
var user = 'admin';
</script>
优缺点
-
优点:跨域完毕之后
DOM
操作和互相之间的JavaScript
调用都是没有问题的 -
缺点:
1.若结果要以
URL
参数传递,这就意味着在结果数据量很大的时候需要分割传递,比较麻烦。2.还有一个是
iframe
本身带来的,母页面和iframe
本身的交互本身就有安全性限制。
4. Hash
url
的#
后面的内容就叫Hash
。Hash的改变,页面不会刷新。这就是用Hash
做跨域通信的基本原理。
补充:
url
的?
后面的内容叫Search
。Search
的改变,会导致页面刷新,因此不能做跨域通信。
场景举例:页面 A
通过iframe
或frame
嵌入了跨域的页面 B
后,A
页面想给B
页面发消息,如何操作?
- 在
A
页面
// A中伪代码
var B = document.getElementsByTagName('iframe');
B.src = B.src + '#' + 'jsonString'; //我们可以把JS 对象,通过 JSON.stringify()方法转成 json字符串,发给 B
- 在
B
页面
// B中的伪代码
window.onhashchange = function () { //通过onhashchange方法监听,url中的 hash 是否发生变化
var data = window.location.hash;
};
5. webSocket
WebSocket
:不受同源策略的限制,支持跨域。
WebSocket
的用法如下:
var ws = new WebSocket('wss://echo.websocket.org'); //创建WebSocket的对象。参数可以是 ws 或 wss,后者表示加密。
//把请求发出去
ws.onopen = function (evt) {
console.log('Connection open ...');
ws.send('Hello WebSockets!');
};
//对方发消息过来时,我接收
ws.onmessage = function (evt) {
console.log('Received Message: ', evt.data);
ws.close();
};
//关闭连接
ws.onclose = function (evt) {
console.log('Connection closed.');
};
6. CORS
CORS
:不受同源策略的限制,支持跨域。一种新的通信协议标准。可以理解成是:同时支持同源和跨域的Ajax。
CORS
需要浏览器和后端同时支持- 浏览器会自动进行
CORS
通信,实现CORS
通信的关键是后端。只要后端实现了CORS
,就实现了跨域。 - 后端在头部信息里面设置允许跨域访问的安全域名:服务端设置
Access-Control-Allow-Origin
就可以开启CORS
。 该属性表示哪些域名可以访问资源,如果设置通配符*
则表示所有网站都可以访问资源
fetch
是一个比较新的API
,用来实现CORS
通信。用法如下:
// url(必选),options(可选)
fetch('/some/url/', {
method: 'get',
}).then(function (response) { //类似于 ES6中的promise
// dosomething
}).catch(function (err) {
// 出错了,等价于 then 的第二个参数,但这样更好用更直观
});
CORS为什么支持跨域的通信?因为跨域时,浏览器会拦截Ajax
请求,并在http
头中加Origin
。
7. img
new Image
, 设src 的时候,图片需要设置Cors
cors需要后台配合设置HTTP响应头,如果请求不是简单请求,浏览器会先发送option预检请求,后端需要响应option请求,然后浏览器才会发送正式请求,cors通过白名单的形式允许指定的域发送请求
简单请求的情况:
-
请求method为:get,post,head
-
content-type:三种表单自带的content-type
text/plain
multipart/form-data
application/x-www-form-urlencoded
-
没有自定义的HTTP header
以上情况有任意一点不满足,就不算简单请求。比如我们常发起的请求的content-type
为application/json
就不是简单请求。
优缺点
- 优点:可以访问任何
url
,一般用来进行点击追踪,做页面分析常用的方法 - 缺点:不能访问响应文本,只能监听是否响应
8. webpack中进行反向代理
在react
的umi脚手架或vue
的vue-cli的框架中都可以通过proxy
来配置
其本质是配置在了webapck
中devServer
中的proxy
中,提供了反向代理能力
devServer: {
proxy: {
'/api': {
target: 'http://www.example.com', // your target host
changeOrigin: true, // needed for virtual hosted sites
pathRewrite: {
'^/api': '' // rewrite path
}
},
}
}
proxy
工作原理实质上是利用http-proxy-middleware
这个http
代理中间件,实现请求转发给其他服务器
在webpack
内部需要配合 http-proxy-middleware
插件对 api
请求地址进行代理
const express = require('express');
const proxy = require('http-proxy-middleware');
// proxy api requests
const exampleProxy = proxy(options); // 这里的 options 就是 webpack 里面的 proxy 选项对应的每个选项
// mount `exampleProxy` in web server
const app = express();
app.use('/api', exampleProxy);
app.listen(3000);
然后再用 nginx
把允许跨域的源地址添加到报头里面即可。
9. nginx代理跨域
nginx
的CORS
配置,大致如下:
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'DNT, X-Mx-ReqToken, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 200;
}
}
10. 使用nodejs中间件代理
可以自己写一段nodejs同时启一个服务端做中间层
此时的环境中(如本地开发时)本地的前端页面发起ajax
请求到本地的node服务端是不跨域的,此时在node中进行一个转发,node服务端到真实服务端的访问也是不跨域的(同源策略是浏览器拦截了ajax
的相应),这样就可以绕过跨域。
实现与webpack的proxy解决跨域原理的相同。