什么是跨域?跨域请求资源的方法有哪些?
一、什么是跨域
由于浏览器的同源策略,当发送请求 URL 的协议、域名、端口三者之间任意一个与当前页面 URL 不同即为跨域。跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。
当前页面 url | 被请求页面 url | 是否跨域 | 原因 |
---|---|---|---|
http://www.test.com/ | http://www.test.com/index.html | 否 | 同源 |
http://www.test.com/ | http://www.test.com/index.html | 跨域 | 协议不同(http&https) |
http://www.test.com/ | http://www.baidu.com/ | 跨域 | 主域名不同 |
http://www.test.com/ | http://blog.test.com/ | 跨域 | 子域名不同 |
http://www.test.com:8080/ | http://www.test.com:80/ | 跨域 | 端口号不同 |
http://www.test.com/ | http://192.168.4.12/ | 跨域 | 域名和域名对应 ip |
二、同源策略限制的内容
- 无法读取非同源网页的 Cookie、LocalStorage 和 IndexedDB
- 无法接触非同源网页的 DOM
- 无法向非同源地址发送 AJAX 请求
三、跨域解决方法
(1)JSONP
简单适用,兼容性好,缺点是只支持 get 请求,不支持 post 请求。
原理:ajax 请求受同源策略影响,不允许进行跨域请求,而 script 标签 src 属性中的链接却可以访问跨域的 JS 脚本,网页通过添加一个<script>
元素,向服务器请求 JSON 数据,服务器收到请求后,将数据放在一个指定名字的回调函数的参数位置传回来。
// index.html
function jsonp ({ url, params, callback }) {
return new Promise((resolve, reject) => {
let script = document.createElement('script');
window[callback] = function(data) {
resolve(data);
document.body.removeChild(script);
}
params = { ...params, callback }; // name=xx&callback=show
let arrs = [];
for (let key in params) {
arrs.push(`${key}=${params[key]}`);
}
//创建一个 <script> 标签,把那个跨域的 API 数据接口地址,赋值给 script 的 src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)
script.src = `${url}?${arrs.join('&')}`;
document.body.appendChild(script);
})
}
jsonp({
url: 'http://localhost:3000/login',
params: { name: 'Ray' },
callback: 'show'
}).then(data => {
console.log(data)
})
//上面这段代码相当于向 http://localhost:3000/login?name=Ray&callback=show 这个地址请求数据
(2)后端在服务器上设置 CORS
服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。
在发送请求时,分为两种情况,简单请求和复杂请求。
简单请求:
只要同时满足以下两大条件,就属于简单请求。
条件1:使用GET
、HEAD
、POST
方法之一。
条件2:Content-Type 的值是 text/plain、multipart/form-data、application/x-www-form-urlencoded 三者之一。
复杂请求:
除了简单请求,就是复杂请求。
复杂请求的 CORS 请求,会在正式通信之前,增加一次 HTTP 查询请求,称为”预检”请求,该请求是 option
方法的,通过该请求来知道服务端是否允许跨域请求。
例子:
vue-shop
后端用node.js
写的,在app.js
文件中设置跨域和相应数据格式。
app.all('/api/*', function (req, res, next) {
res.header('Access-Control-Allow-Origin', '*')
res.header('Access-Control-Allow-Headers', 'X-Requested-With, mytoken')
res.header('Access-Control-Allow-Headers', 'X-Requested-With, Authorization')
res.setHeader('Content-Type', 'application/json;charset=utf-8')
res.header('Access-Control-Allow-Headers', 'Content-Type,Content-Length, Authorization, Accept,X-Requested-With')
res.header('Access-Control-Allow-Methods', 'PUT,POST,GET,DELETE,OPTIONS') // 允许什么方法访问
res.header('X-Powered-By', ' 3.2.1')
if (req.method == 'OPTIONS') res.send(200)
/* 让 options 请求快速返回 */ else next()
})
(3)Nginx 反向代理
使用Nginx
反向代理实现跨域,是最简单的跨域方式。只需要修改 Nginx 的配置即可解决跨域问题,支持所有浏览器,支持 session,不需要修改任何代码,不会影响服务器性能。
// 修改 nginx.conf
server {
listen 80;
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;
}
# 静态资源
location /vue-shop/ {
root html;
index index.html index.htm;
try_files $uri $uri/ /vue-shop/index.html;
}
}
(4)Node.js 中间件代理
实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。
代理服务器,需要做以下几个步骤:
- 接收客户端请求
- 将请求转发给服务器
- 拿到服务器响应数据
- 将响应转发给客户端
//本地文件 index.html,通过代理服务器 http://localhost:3000 向目标服务器 http://localhost:4000 请求数据。
// index.html(http://127.0.0.1:5500)
<script src="https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script>
<script>
$.ajax({
url: 'http://localhost:3000',
type: 'post',
data: { name: 'xiamen', password: '123456' },
contentType: 'application/json;charset=utf-8',
success: function (result) {
console.log(result) // {"title":"fontend","password":"123456"}
},
error: function (msg) {
console.log(msg)
}
})
</script>
// server1.js 代理服务器(http://localhost:3000)
const http = require('http')
// 第一步:接受客户端请求
const server = http.createServer((request, response) => {
// 代理服务器,直接和浏览器直接交互,需要设置 CORS 的首部字段
response.writeHead(200, {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': '*',
'Access-Control-Allow-Headers': 'Content-Type'
})
// 第二步:将请求转发给服务器
const proxyRequest = http.request(
{
host: '127.0.0.1',
port: 4000,
url: '/',
method: request.method,
headers: request.headers
},
serverResponse => {
// 第三步:收到服务器的响应
var body = ''
serverResponse.on('data', chunk => { body += chunk })
serverResponse.on('end', () => {
console.log('The data is ' + body)
// 第四步:将响应结果转发给浏览器
response.end(body)
})
}
).end()
})
server.listen(3000, () => {
console.log('The proxyServer is running at http://localhost:3000')
})
// server2.js(http://localhost:4000)
const http = require('http')
const data = { title: 'fontend', password: '123456' }
const server = http.createServer((request, response) => {
if (request.url === '/') response.end(JSON.stringify(data))
})
server.listen(4000, () => {
console.log('The server is running at http://localhost:4000')
})
(5)postMessage
postMessage
是 HTML5 XMLHttpRequest Level 2 中的 API,且是为数不多可以跨域操作的 window 属性之一,它可用于解决以下方面的问题:
- 页面和其打开的新窗口的数据传递
- 多窗口之间消息传递
- 页面与嵌套的iframe消息传递
- 上面三个场景的跨域数据传递
postMessage()
方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
otherWindow.postMessage(message, targetOrigin, [transfer]);
参数解释:
message
:将要发送到其他 window 的数据。
targetOrigin
:通过窗口的origin
属性来指定哪些窗口能接收到消息事件,其值可以是字符串“*”(表示无限制)或者一个 URL。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin
提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。
transfer(可选)
:是一串和message
同时传递的Transferable
对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。
例子:
调用postMessage
方法实现父窗口http://test1.com
向子窗口http://test2.com
发消息(子窗口同样可以通过该方法发送消息给父窗口)。
// 父窗口打开一个子窗口
var openWindow = window.open('http://test2.com', 'title');
// 父窗口向子窗口发消息(第一个参数代表发送的内容,第二个参数代表接收消息窗口的 url)
openWindow.postMessage('Nice to meet you!', 'http://test2.com');
// 子窗口监听 message 消息
window.addEventListener('message', function (e) {
console.log(e.source); // e.source 发送消息的窗口
console.log(e.origin); // e.origin 消息发向的网址
console.log(e.data); // e.data 发送的消息
}, false);
(6)WebSocket 协议
WebSocket
是 HTML5 的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket
和 HTTP 都是应用层协议,都基于 TCP 协议。但是WebSocket
是一种双向通信协议,在建立连接之后,WebSocket
的server
与client
都能主动向对方发送或接收数据。同时,WebSocket
在建立连接时需要借助 HTTP 协议,连接建立好了之后client
与server
之间的双向通信就与 HTTP 无关了。
原生 WebSocket API 使用起来不太方便,我们使用Socket.io
,它很好地封装了WebSocket
接口,提供了更简单、灵活的接口,也对不支持WebSocket
的浏览器提供了向下兼容。
//本地文件 socket.html 向 localhost:3000 发送数据和接收数据
<script>
let socket = new WebSocket('ws://localhost:3000');
socket.onopen = function () {
socket.send('to server'); //向服务器发送数据
}
socket.onmessage = function (e) {
console.log(e.data); //接收服务器返回的数据
}
</script>
// server.js
let express = require('express');
let app = express();
let WebSocket = require('ws'); //记得安装 ws
let wss = new WebSocket.Server({ port: 3000 });
wss.on('connection', function (ws) {
ws.on('message', function (data) {
console.log(data);
ws.send('success')
});
})
(7)window.name + iframe
window.name
属性的独特之处:name 值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。
//其中 a.html 和 b.html 是同域的,都是 http://localhost:3000; 而 c.html 是 http://localhost:4000
// a.html(http://localhost:3000/a.html)
<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
<script>
let first = true;
// onload 事件会触发 2 次,第 1 次加载跨域页,并留存数据于 window.name
function load () {
if (first) {
// 第 1 次 onload(跨域页)成功后,切换到同域代理页面
let iframe = document.getElementById('iframe');
iframe.src = 'http://localhost:3000/b.html';
first = false;
} else {
// 第 2 次 onload(同域b.html页)成功后,读取同域 window.name中数据
console.log(iframe.contentWindow.name);
}
}
</script>
#b.html 为中间代理页,与 a.html 同域,内容为空。
// c.html(http://localhost:4000/c.html)
<script>
window.name = 'Ray'
</script>
总结:通过iframe
的src
属性由外域转向本地域,跨域数据即由iframe
的window.name
从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
(8)location.hash + iframe
实现原理: a.html 欲与 c.html 跨域相互通信,通过中间页 b.html 来实现。 三个页面,不同域之间利用iframe
的location.hash
传值,相同域之间直接 JS 访问来通信。
具体实现步骤:一开始 a.html 给 c.html 传一个 hash 值,然后 c.html 收到 hash 值后,再把 hash 值传递给 b.html,最后 b.html 将结果放到 a.html 的 hash 值中。 同样的,a.html 和 b.html 是同域的,都是http://localhost:3000
;而 c.html 是http://localhost:4000
// a.html
<iframe src="http://localhost:4000/c.html#msg1"></iframe>
<script>
window.onhashchange = function () { //检测 hash 的变化
console.log(location.hash);
}
</script>
// b.html
<script>
window.parent.parent.location.hash = location.hash
//b.html 将结果放到 a.html 的 hash 值中,b.html 可通过 parent.parent 访问 a.html 页面
</script>
// c.html
console.log(location.hash);
let iframe = document.createElement('iframe');
iframe.src = 'http://localhost:3000/b.html#msg2';
document.body.appendChild(iframe);
(9)document.domain + iframe
该方式只能用于二级域名相同的情况下,比如a.test.com
和b.test.com
适用于该方式。只需要给页面添加document.domain ='test.com'
表示二级域名都相同就可以实现跨域。
实现原理:两个页面都通过 JS 强制设置document.domain
为基础主域,就实现了同域。
//页面 a.zf1.cn:3000/a.html 获取页面 b.zf1.cn:3000/b.html 中变量 num 的值
// a.html
<body>
helloa
<iframe src="http://b.zf1.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
<script>
document.domain = 'zf1.cn';
function load () {
console.log(frame.contentWindow.a);
}
</script>
</body>
// b.html
<body>
hellob
<script>
document.domain = 'zf1.cn';
var num = 100;
</script>
</body>
四、总结
CORS 支持所有类型的 HTTP 请求,是跨域 HTTP 请求的根本解决方案。
JSONP 只支持 GET 请求,JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。
不管是 Node 中间件代理还是 Nginx 反向代理,主要是通过同源策略对服务器不加限制。
注:日常工作中,用得比较多的跨域方案是 CORS 和 Nginx 反向代理。
- 相关文章:什么是跨域?跨域解决方法