跨域

# 跨域

[TOC]

# 一、跨域

  • 跨域:不同域之间相互请求资源。
    • 比如,http://www.abc.com/index.html请求http://www.efg.com/service.php
  • img、link、script标签不受同源策略限制,可跨域。

# 1.1 一个域名地址的组成

http:// www . abc.com : 8080 / scripts/jquery.js?id=001#mediaId=6238
协议    子域名  主域名     端口号  请求路径	      请求参数 哈希码
1
2
  • 域名 = 子域名 + 主域名
  • 主机 = 域名 + 端口号

# 1.2 同源策略

  • 同源策略限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键的安全机制。
  • 同源:两者拥有相同的协议域名端口时,就属于同一个源(origin)(或者说同一个域)。
  • 限制体现在:
    • Cookie、LocalStorage和IndexDB (opens new window)无法获取。
    • 无法获取和操作DOM。
    • 不能发送Ajax请求,Ajax只适合同源的通信。

事实上HTTP和HTTPS两个协议的url看上去都可以省略端口号,但是他们访问的默认端口不同。 HTTP默认访问80端口,HTTPS默认访问443端口,所以http访问https肯定是跨域。

# 二、处理跨域方法

参考示例:快速入门跨域demo (opens new window)

# 2.1 后端设置proxy代理

# 2.1.1 原理

  • 因为JS同源策略是浏览器的安全策略,所以在浏览器客户端不能跨域访问。
  • 服务器调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就没有跨越问题。
  • 简单地说,就是浏览器不能跨域,服务器可以跨域。

# 2.1.2 示例

# 2.1.2.1 使用http-proxy-middleware插件设置后端的代理
  • 前端部分
<h2 style="text-align: center;">通过http-proxy-middleware插件设置代理</h2>
<button>点击跨域</button>
<p>hello world!</p>

<script>
    var btn = document.getElementsByTagName('button')[0];
    var text = document.getElementsByTagName('p')[0];
    btn.addEventListener('click', function () {
        var xhr = new XMLHttpRequest();
        var url = 'http://localhost:3000/api';
        xhr.open('GET', url);
        xhr.send(null);
        xhr.onreadystatechange = () => {
            if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
                text.innerHTML = xhr.response;
            }
        }
    })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
  • 后端部分
    • changeOrigin设置为true,本地会虚拟一个服务端接收你的请求并代你发送该请求。
// serverReq.js
var express = require('express');
var proxy = require('http-proxy-middleware');

var requestPort = 3000;  // 请求页面跑在3000端口
var app = express();

app.use(express.static(__dirname));

// http://localhost:3000/api   -->   http://localhost:3001/api
app.use('/api', proxy({target: 'http://localhost:3001/', changeOrigin: true}));

app.listen(requestPort, function () {
    console.log('Requester is listening on port '+ requestPort);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// serverRes.js
var express = require('express');
var app = express();

var responsePort = 3001;  // 请求页面跑在3001端口

app.get('/api', (req, res) => {
    res.send("Hello world from Proxy  :)")
});

app.listen(responsePort, function () {
    console.log('Responser is listening on port '+ responsePort);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
# 2.1.2.2 直接使用http模块发出请求
  • 前端部分
<h2 style="text-align: center;">不使用插件设置代理跨域</h2>
<button>通过Proxy跨域</button>
<p>hello world!</p>

<script>
    var btn = document.getElementsByTagName('button')[0];
    var text = document.getElementsByTagName('p')[0];
    btn.addEventListener('click', function () {
        var xhr = new XMLHttpRequest();
        
		// url为实际请求的地址,向3000/proxy发出请求的同时携带这个包含url的对象,这个url在这里只是参数,不是请求路径
        var proxy_url = 'http://localhost:3000/proxy?url=http://localhost:3001/'; 

        xhr.open('GET', proxy_url);
        xhr.send();
        xhr.onreadystatechange = () => {
            if (xhr.readyState === XMLHttpRequest.DONE && xhr.status === 200) {
                text.innerHTML = xhr.response;
            }
        }
    })
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
  • 后端部分
var express = require('express');
var http= require('http');

var portNumber = 3000;
var app = express();

app.use(express.static(__dirname)); //即index.html

app.get('/proxy', function(request, response){
    var url = request.query.url    // http://localhost:3001/

    
    // 向url发出请求
    http.get(url, function(responseFromOtherDomain) {
        // data事件会在数据接收过程中,每收到一段数据就触发一次,接收到的数据被传入回调函数。
        responseFromOtherDomain.on("data", function(data) {
            response.writeHead(200, {'Content-Type': 'text/html; charset=utf-8'});
            response.end(data);
        });
    });
});

app.listen(portNumber, function () {
    console.log('Requester with proxy is listening on port '+ portNumber);
});
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

# 2.2 JSONP*

现在不常用,不安全。

# 2.2.1 JSONP的原理

通过<script>标签的异步加载来实现的。

动态插入 script 标签,通过 script 标签引入一个 js 文件,这个 js 文件载入成功后会执行我们在 url 参数中指定的函数,并且会把我们需要的 json 数据作为参数传入。

比如说,实际开发中,我们发现,head标签里,可以通过<script>标签的src,里面放url,加载很多在线的插件。这就是用到了JSONP。

JSONP:JSON + padding(填充式JSON或参数式JSON),被包含在函数调用的JSON。

callback({ "name": "Lin" });
1

script标签不存在跨域问题。

# 2.2.2 JSONP的实现

JSONP只支持get请求,不支持post请求。

// www.aaa.com
<script>
    function jsonp(json){
    	alert(json['name']);
	}
</script>
// script标签是按顺序执行的,所以只能放在下面。要放在前面的话,可以动态加载JS文件。
<script src="http://www.bbb.com/jsonp.js"></script>

// www.bbb.com
jsonp({'name':'古力',‘age':22})
1
2
3
4
5
6
7
8
9
10
11
// 动态加载JS文件
function createJs(sUrl) {
    let oScript = document.createElement('script');
    oScript.src = sUrl;
    document.head.appendChild(oScript);
}
// ?callback=fn 控制 名为fn({...})
// 避免不知道json.js里面的jsonp叫什么名字
createJs('jsonp.js?callback=fn');
1
2
3
4
5
6
7
8
9

比如说,客户端这样写:

<script src="http://www.smyhvae.com/?data=name&callback=myjsonp"></script>
1

上面的src中,data=name是get请求的参数,myjsonp是和后台约定好的函数名。 服务器端这样写:

myjsonp({
    "data": {}
})
1
2
3

于是,本地要求创建一个myjsonp 的全局函数,才能将返回的数据执行出来。

# 2.3 WebSocket*

# 2.3.1 使用

参考资料:http://www.ruanyifeng.com/blog/2017/05/websocket.html (opens new window)

// 前端部分
// 创建WebSocket的对象。参数可以是 ws 或 wss,后者表示加密。
// url:服务器绝对URL
const ws=new WebSocket('ws://localhost:8080/');

//发送请求
ws.onopen = (evt) => {
    console.log('Connection open ...');
    ws.send('Hello WebSockets!');
};

//接收消息
ws.onmessage = (evt) => {
    console.log('Received Message: ', evt.data);
    ws.close();
};

//关闭连接
ws.onclose = (evt) => {
    console.log('Connection closed.');
};

ws.onerror=()=>{};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// server.js
const net=require('net');
const crypto=require('crypto');

// 解析头部
function parseHeader(str){
    // 协议的换行符是\r\n
    let arr=str.split('\r\n').filter(line=>line);
    // 移除第一行 GET / HTTP/1.1
    arr.shift();

    let headers={};
    arr.forEach(line=>{
        let [name, value]=line.split(':');
		
        // 去除首尾空格,并转成小写
        name=name.replace(/^\s+|\s+$/g, '').toLowerCase();
        value=value.replace(/^\s+|\s+$/g, '');

        headers[name]=value;
    });

    return headers;
}

let server=net.createServer(sock=>{
    sock.once('data', buffer=>{
        // 这里因为是对头部进行处理,所以可以toString()
        let str=buffer.toString();
        let headers=parseHeader(str);
		
        // Upgrade:websocket表示协议升级为websocket
        if(headers['upgrade']!='websocket'){
            console.log('no upgrade');
            sock.end();
        }else if(headers['sec-websocket-version']!='13'){
            // Sec-WebSocket-Version:13表示websocket版本号
            console.log('no 13');
            sock.end();
        }else{
            // Sec-Websocket-Key用来确认对方能否懂websocket
            let key=headers['sec-websocket-key'];
            // 这个是固定的websock mark,作者自己定的
            let uuid='258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
            let hash=crypto.createHash('sha1');

            hash.update(key+uuid);
            let key2=hash.digest('base64');
			
            // 写响应头
            // 101状态码:Switching Protocols协议切换
            sock.write(`HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection:upgrade\r\nSec-Websocket-Accept:${key2}\r\n\r\n`);
        }
    });

    sock.on('end', ()=>{console.log('connection closed!')});
});
server.listen(8080);
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
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
  • 性能高

    • 服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息,是真正的双向平等对话,属于服务器推送技术的一种。(HTTP协议的通信只能由客户端发起。)
    • 使用的是自定义协议,能够在客户端和服务端之间发送非常少量的数据,而不必担心HTTP那样字节级的开销。由于传递的数据包很小,因此Web Socket适合移动端使用。
    • 长连接。
  • 在JS中创建Web Socket后,会有一个HTTP请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会使用HTTP升级从HTTP协议交换为Web Socket协议。

# 2.3.2 socket.io

  • 特点
    • 简单、方便。
    • 兼容 IE5。
    • 自动数据解析。
  • 方法
    • sock.emit('name', 数据) 主动发送数据
    • sock.on('name', function (数据){}) 接收数据
// server.js
const http=require('http');
const io=require('socket.io');

//1.建立普通http
let server=http.createServer((req, res)=>{});
server.listen(8080);

//2.建立ws
let wsServer=io.listen(server);
// 建立完连接后执行回调函数
wsServer.on('connection', sock=>{
    sock.on('aaa', function (a, b){
        console.log(a, b, a+b);
    });
    setInterval(function (){
        sock.emit('timer', new Date().getTime());
    }, 1000);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<script src="http://localhost:8080/socket.io/socket.io.js" charset="utf-8"></script>
<script>
    let sock=io.connect('ws://localhost:8080/');
    sock.emit('aaa', 12, 5);
    sock.on('timer', time=>{
        console.log(time);
    });
</script>
1
2
3
4
5
6
7
8

# 2.4 CORS*

Cross-Origin Resource Sharing,跨域资源共享。

CORS使用自定义的HTTP头部让浏览器与服务器进行交流。服务器对于 CORS 的支持,主要就是通过设置 Access-Control-Allow-Origin 来进行的。如果浏览器检测到相应的设置,就可以允许 Ajax 进行跨域的访问。

CORS 可以理解成是既可以同源、也可以跨域的Ajax。

fetch 是一个比较新的API,用来实现CORS通信。用法如下:

// url(必选),options(可选)
fetch('/some/url/', {
    method: 'get',
}).then(function (response) {  //类似于 ES6中的promise

}).catch(function (err) {
    // 出错了,等价于 then 的第二个参数,但这样更好用更直观
});
1
2
3
4
5
6
7
8
  • 实现方式

    1. 跨域时,浏览器会拦截Ajax请求,并在http头中加Origin字段。
    GET /cors HTTP/1.1
    Origin: http://api.bob.com
    Host: api.alice.com
    Accept-Language: en-US
    Connection: keep-alive
    User-Agent: Mozilla/5.0...
    
    1
    2
    3
    4
    5
    6

    上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。

    1. 如果服务器认为这个请求可以接受,就在Access-Control-Allow-Origin头部中回发相同的源信息(如果是公共资源,可以回发“*”)。
    var express = require('express');
    var app = express();
    
    app.get('/', (req, res) => {
        // 设置允许跨域的origin,允许3000端口访问本端口(3001)
        res.set('Access-Control-Allow-Origin', 'http://localhost:3000');
        res.send("Hello world from CROS.");
    });
    
    app.listen(3001, function () {
        console.log('cros_responser is listening on port '+ 3001);
    });
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    1. 如果没有这个头部,或者有这个头部但源信息不匹配,浏览器就会驳回请求。正常情况下,浏览器会处理请求。
  • IE10以下的版本都不支持。

  • Ajax由于受同源策略限制而不能跨域。但跨域时,服务器能正常返回资源,但会被浏览器拦截,需要服务器设置access-control-allow-origin才能通过。

const http=require('http');

let allowOrigin={
    'http://localhost': true,
    'http://aaa.com': true,
    'https://aaa.com': true,null': true	// 为了支持本地文件设的
}

http.createServer((req, res)=>{
    let {origin}=req.headers;

    if(allowOrigin[origin]){
        res.setHeader('access-control-allow-origin', '*');
    }

    res.write('{"a": 12, "b": "Blue"}');
    res.end();
}).listen(8080);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 2.5 Hash*

# 2.5.1 原理

  • location.hash+iframe(数据直接暴露在了 url 中且数据容量和类型都有限)

  • url的#后面的内容就叫Hash。因为hash只会出现在URL中,不会被包含在http请求中,所以Hash的改变,不会引起页面刷新。这就是用 Hash 做跨域通信的基本原理。

  • 补充:url的?后面的内容叫Search。Search的改变,会导致页面刷新,因此不能做跨域通信。

# 2.5.2 示例

  • http://localhost:3000/a.html 使用js动态生成一个隐藏的iframe,设置src属性为' http://localhost:3001/c.html#getdata '。
  • c.html判断hash值是否为'#getdata',如果为'#getdata',则在当前的iframe(c.html)中再生成一个隐藏的iframe,其src属性指向' http://localhost:3000/b.html '。
  • 因为a.htmlb.html同源,所以可以在b.html里面修改a.html的hash值,这样a.html就可以通过获取自身的hash值得到数据。
  • staticReq
<!-- a.html -->
<p>hello world</p>
<script>
var p = document.getElementsByTagName('p')[0];
var iframe = document.createElement('iframe');
iframe.src = 'http://localhost:3001/c.html#getdata';   // location.hash为'#getdata'
iframe.style.display = 'none';
document.body.appendChild(iframe);

function checkHash () {
    if (location.hash) {
        let data = location.hash.substring(1);     // 去除'#'号
        p.innerHTML = data;
    }
}
setInterval(checkHash, 2000);   // 每隔2s监听hash值是否发生变化
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!-- b.html -->
<script>
// 3000/b.html属于3001/c.html的子窗口,3001/c.html属于3000/a.html的子窗口,故parent.parent为a.html
// 因为parent.parent和自身属于同一个域,所以可以改变其location.hash的值
parent.parent.location.hash = self.location.hash.substring(1);
</script>
1
2
3
4
5
6
  • staticRes
<!-- c.html -->
<script type="text/javascript">
var message = 'hello world from hash.'
if (location.hash === '#getdata') {
    var ifrproxy = document.createElement('iframe');
    ifrproxy.style.display = 'none';
    ifrproxy.src = 'http://localhost:3000/b.html#' + message; // 注意该文件在3000端口下
    document.body.appendChild(ifrproxy);
}
</script>
1
2
3
4
5
6
7
8
9
10

# 2.6 postMessage*

H5中新增的postMessage()方法,适用于不同窗口iframe之间的跨域。

场景:窗口 A (http:A.com)向跨域的窗口 B (http:B.com)发送信息。

(1)在A窗口中操作如下:

// 窗口A(http:A.com)向跨域的窗口B(http:B.com)发送信息
// 注意:此处的window是B窗口下的window对象
window.postMessage('data', 'http://B.com');
1
2
3

(2)在B窗口中操作如下:

// 在窗口B中监听 message 事件
// 这里的window是A窗口里的window对象
window.addEventListener('message', function (event) { 
    // 获取 :url。这里指:http://A.com
    console.log(event.origin);
    // 对发送消息的窗口对象的引用
    // 获取:A window对象
    console.log(event.source);  
    // 获取传过来的数据
    console.log(event.data);    
}, false);
1
2
3
4
5
6
7
8
9
10
11

# 2.7 document.domain

  • 一级域名相同但二级域名不同的网页只要设置相同的document.domain,就获取DOM。
  • document.domain设置成自身或更高一级的父域,且主域必须相同。

# 2.8 FromData

所有向服务器提交的HTTP数据,其实都是一个表单。

FromData是一种容器,用于模拟表单,向服务器提交数据。主要用于处理文件上传问题。(Ajax2.0出的)

const http=require('http');
const multiparty=require('multiparty');

http.createServer((req, res)=>{
  let form=new multiparty.Form({uploadDir: './upload/'});

  form.parse(req);

  form.on('field', (name, value)=>{
    console.log('field:', name, value);
  });
  form.on('file', (name, file)=>{
    console.log('file:', name, file);
  });

  form.on('close', ()=>{
    console.log('成功');
  });
}).listen(8080);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<form id="form1" action="http://localhost:8080/" method="post">
    用户:<input type="text" name="user" /><br>
    密码:<input type="password" name="pass" /><br>
    文件:<input type="file" name="f1" /><br>
    <input type="submit" value="提交">
</form>
1
2
3
4
5
6

# 2.9.1 form表单转FromData对象

<from>标签直接转化为一个FromData对象。

let oForm=document.querySelector('#form1');

oForm.onsubmit=function (){
    let formdata=new FormData(oForm);

    let xhr=new XMLHttpRequest();

    xhr.open(oForm.method, oForm.action, true);
    xhr.send(formdata);

    xhr.onreadystatechange=function (){
        if(xhr.readyState==4){
            if(xhr.status==200){
                alert('成功');
            }else{
                alert('失败');
            }
        }
    };

    // 阻止默认的提交事件
    return false;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 2.9.2 创建空白FromData对象(不推荐)

创建空白FromData对象,然后向其中添加数据。

let oBtn=document.querySelector('#btn1');
oBtn.onclick=function (){
    let formdata=new FormData();

    formdata.append('username', document.querySelector('#user').value);
    formdata.append('password', document.querySelector('#pass').value);
    formdata.append('f1', document.querySelector('#f1').files[0]);

    let xhr=new XMLHttpRequest();

    xhr.open('post', 'http://localhost:8080/', true);
    xhr.send(formdata);

    xhr.onreadystatechange=function (){
        if(xhr.readyState==4){
            if(xhr.status==200){
                alert('成功');
            }else{
                alert('失败');
            }
        }
    };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 2.10 window.name

  • 传输的数据,大小一般为2M,IE和firefox下可以大至32M左右。
  • 数据格式可以自定义,如json、字符串。

# 2.10.1 原理

  • 在一个窗口的生命周期内,窗口载入的所有的页面都是共享一个window.name的。
  • 每一个页面对window.name都有读写的权限。
  • window.name不会因不同的页面(甚至不同域名)加载后被重置。

# 2.10.2 示例

  • http://localhost:3000/a.html 使用js动态生成一个隐藏的iframe,设置src属性为' http://localhost:3001/c.html '。
  • 等这个iframe加载完之后,重新设置src属性为同源的地址' http://localhost:3000/b.html '(b.html是一个空的html文件)。
  • 现在iframea.html同源,那就可以访问window.name属性,而name值没有变化。
  • staticReq - port 3000
<!-- a.html -->
<!-- 另设一个空的b.html -->
<p>hello world</p>
<script>
var p = document.getElementsByTagName('p')[0];
var isFirst = true;
var iframe = document.createElement('iframe');

iframe.src = 'http://localhost:3001/c.html'; //第一次加载url
iframe.style.display = 'none';
document.body.appendChild(iframe);

var loadFunc = function () {
    if(isFirst){
        //加载完url后,修改src属性,使其与3000端口同源
        iframe.src = 'http://localhost:3000/b.html';
        isFirst = false;
    }else {
        //iframe回到原域后,获取name的值,执行回调函数,然后销毁iframe
        p.innerHTML = iframe.contentWindow.name;
        iframe.contentWindow.close();
        document.body.removeChild(iframe);
        iframe.src = '';
        iframe = null;
    }
}

//监听iframe是否加载,加载完执行loadFunc
iframe.onload = loadFunc;
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
  • staticRes - port 3001
<!-- c.html -->
<script type="text/javascript">
    window.name = 'I was there!';
</script>
1
2
3
4