当一个请求url的协议、域名、端口三者之间任意一个与当前页面url不同即为跨域。
出于浏览器的同源策略限制。同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS
、CSRF
等攻击。即便两个不同的域名指向同一个ip地址,也非同源。同源策略会阻止一个域的javascript
脚本和另外一个域的内容进行交互。所谓同源(即指在同一个域)就是两个页面具有相同的协议(protocol
),主机(host
)和端口号(port
)。
同源策略限制内容有:
Cookie
、LocalStorage
、IndexedDB
等存储性内容DOM
节点AJAX
请求发送后,结果被浏览器拦截了有三个标签是允许跨域加载资源:
跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了
CORS
CORS
是跨域资源分享(Cross-Origin Resource Sharing)的缩写。它是 W3C 标准,属于跨源 AJAX 请求的根本解决方法。CORS
需要浏览器和后端同时支持。
服务端设置 Access-Control-Allow-Origin
就可以开启 CORS
。 该属性表示哪些域名可以访问资源,如果设置通配符(*
)则表示所有网站都可以访问资源,这时的客户端不需要任何配置即可进行跨域访问。
router.get('/cors', (req, res, next) => {
// 只允许127.0.0.1:8080
res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:8080');
// res.setHeader('Access-Control-Allow-Origin', '*');
res.json(data);
});
jsonp
利用 标签没有跨域限制的漏洞,在
script
标签中我们可以引用其他服务上的脚本,最常见的场景就是CDN。
流程:
javascript
。script
标签,将其src
属性指向对应接口。并将第一步定义的回调函数名作为请求的参数。javascript
文件传入回调中。javascript
。客户端
// 1. 创建回调函数callback
function callback(res) {
console.log(JSON.stringify(res, null , 2));
}
// 2. 动态创建script标签,并设置src属性,注意参数cb=callback
var script = document.createElement('script');
script.src = 'http://127.0.0.1:3000/info/jsonp?cb=callback';
document.getElementsByTagName('head')[0].appendChild(script);
服务端:
router.get('/jsonp', (req, res, next) => {
var str = JSON.stringify(data);
// 3. 创建script脚本内容,用`callback`函数包裹住数据
// 形式:callback(data)
var script = `${req.query.cb}(${str})`;
res.send(script);
});
// 4. 前端收到响应数据会自动执行该脚本
jsonp只支持GET
方法
websocket
Websocket
是 HTML5
的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket
和 HTTP
都是应用层协议,都基于 TCP
协议。但是 WebSocket
是一种双向通信协议,在建立连接之后,WebSocket
的 服务器与 客户端都能主动向对方发送或接收数据。同时,WebSocket
在建立连接时需要借助 HTTP
协议,连接建立好了之后 client
与 server
之间的双向通信就与 HTTP
无关了。
客户端
let socket = new WebSocket('ws://localhost:3000');
socket.onopen = function () {
socket.send('connect');//向服务器发送数据
}
socket.onmessage = function (e) {
console.log(e.data);//接收服务器返回的数据
}
服务端
let express = require('express');
let app = express();
let WebSocket = require('ws');
let wss = new WebSocket.Server({port:3000});
wss.on('connection',function(ws) {
ws.on('message', function (data) {
ws.send(data)
});
})
这种方法本质上仍然遵循了同源政策,只是换了一个请求的思路,将请求移至了后端。
同源政策是浏览器层面的限制。那么,如果我们不在前端跨域,而将“跨域”的任务交给后端服务,那么就规避了同源政策了。
router.get('*', (req, res, next) => {
let path = req.path.replace(/^\/proxy/, '');
request.get(`http://127.0.0.1:3002${path}`, (err, response) => {
res.json(JSON.parse(response.body));
});
});
Nginx
代理实现原理类似于上面的代理。就是搭建一个中转 nginx
服务器,用于转发请求。
使用 nginx
反向代理实现跨域,是最简单的跨域方式。只需要修改 nginx
的配置即可解决跨域问题,支持所有浏览器,支持 session
,不需要修改任何代码,并且不会影响服务器性能。
我们只需要配置nginx
,在一个服务器上配置多个前缀来转发http/https
请求到多个真实的服务器即可。这样,这个服务器上所有url
都是相同的域 名、协议和端口。因此,对于浏览器来说,这些url都是同源的,没有跨域限制。而实际上,这些url实际上由物理服务器提供服务。这些服务器内的 javascript
可以跨域调用所有这些服务器上的url
。
先下载nginx
,然后将 nginx
目录下的 nginx.conf
修改如下:
server {
listen 81;
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;
# 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为*
add_header Access-Control-Allow-Credentials true;
}
}
postMessage
postMessage
是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window
属性之一。
调用postMessage
方法实现父窗口http://test1.com
向子窗口http://test2.com
发消息(子窗口同样可以通过该方法发送消息给父窗口)
它可用于解决以下方面的问题:
postMessage()
方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
语法:
otherWindow.postMessage(message, targetOrigin, [transfer]);
http://localhost:8080/a.html
页面向http://localhost:8090/b.html
之间传递信息。
<iframe src="http://localhost:8090/b.html" frameborder="0" id="frame" onload="load()">iframe> //等它加载完触发一个事件
//内嵌在http://localhost:8080/a.html
<script>
function load() {
let frame = document.getElementById('frame')
frame.contentWindow.postMessage('request', 'http://localhost:8090') //发送数据
window.onmessage = function(e) { //接受返回数据
console.log(e.data) //我不爱你
}
}
script>
// http://localhost:8090/b.html
window.onmessage = function(e) {
e.source.postMessage('response', e.origin)
}
document.domain
+ iframe
该方式只能用于二级域名相同的情况下,比如 a.test.com
和 b.test.com
适用于该方式。
只需要给页面添加 document.domain ='test.com'
表示二级域名都相同就可以实现跨域。
实现原理:两个页面都通过js强制设置document.domain
为基础主域,就实现了同域。
http://a.localhost:8080/a.html
页面向http://b.localhost:8080/b.html
之间传递信息。
<iframe src="http://b.localhost:8080/b.html" frameborder="0" id="frame" onload="load()">iframe> //等它加载完触发一个事件
//内嵌在http://localhost:8080/a.html
<script>
document.domain = 'localhost:8080'
function load() {
console.log(frame.contentWindow.a);
}
script>
// http://b.localhost:8080/b.html
document.domain = 'localhost:8080';
var a = 100;
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/b.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同域,内容为空。
```html
// c.html(http://localhost:4000/c.html)
<script>
window.name = '我不爱你'
script>
总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。
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#iloveyou">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#idontloveyou';
document.body.appendChild(iframe);