Jinyun's Notes

没什么天赋,爱好也不多,但愿坚持做些喜欢的事情

0%

前端跨域方案

201910271121.png

所谓跨域,顾名思义,跨到了另外的域,域不仅仅指的是不同的域名网站,可能同一个域名不同的端口号也算不同的域。浏览器是有规则的,只要 协议、域名、端口 有任何一个不同,都被当作是不同的域。协议指的是 http,或者 https 等。

跨域概念

一个域下的文档或脚本试图去请求另一个域下的资源。

跨域形式

  • <script src="..."></script> 标签嵌入跨域脚本。语法错误信息只能在同源脚本中捕捉到
  • <link rel="stylesheet" href="..."> 标签嵌入 CSS。由于 CSS 的松散的语法规则,CSS 的跨域需要一个设置正确的 Content-Type 消息头。不同浏览器有不同的限制:IE、Firefox、Chrome、Safari(跳至CVE-2010-0051)部分和 Opera
  • <img> 嵌入图片。支持的图片格式包括 PNG、JPEG、GIF、BMP、SVG、…
  • <video> 和 <audio> 嵌入多媒体资源
  • <object>、<embed> 和 <applet> 的插件
  • @font-face 引入的字体。一些浏览器允许跨域字体(cross-origin fonts),一些需要同源字体(same-origin fonts)
  • <frame> 和 <iframe> 载入的任何资源。站点可以使用 X-Frame-Options 消息头来阻止这种形式的跨域交互

同源策略

同源策略-SOP(Same Origin Policy)是一种约定,由 Netscape 公司 1995 年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到 XSS、CSFR 等攻击。所谓同源是指 协议+域名+端口 三者相同,即便两个不同的域名指向同一个 IP 地址,也非同源。

  • 协议相同
  • 域名相同
  • 端口相同

同源策略目的

为了保证用户信息的安全,防止恶意的网站窃取数据。

同源策略限制范围

  • Cookie、LocalStorage 和 IndexDB 无法读取
  • DOM 无法获得
  • AJAX 请求不能发送

跨域场景

URL说明是否允许
http://www.example.com/a.js
http://www.example.com/b.js
http://www.example.com/src/c.js
同一域名,不同文件或路径OK
http://www.example.com:8001/a.js
http://www.example.com:8002/b.js
同一域名,不同端口NO
http://www.example.com/a.js
https://www.example.com/b.js
同一域名,不同协议NO
http://www.example.com/a.js
http://cdn.example.com/b.js
相同主域,不同子域NO
http://www.example.com/a.js
http://192.168.10.10/b.js
域名对应 IPNO
http://www.example.com/a.js
http://www.sample.com/b.js
不同域名NO

跨域解决方案

  • jsonp 跨域
  • document.domain + iframe 跨域
  • location.hash + iframe 跨域
  • window.name + iframe 跨域
  • postMessage 跨域
  • 跨域资源共享(CORS)
  • Nginx 代理跨域
  • NodeJS 中间件代理跨域
  • WebSocket 协议跨域

jsonp 跨域

原生方案

前端代码:

1
2
3
4
5
6
7
8
9
var script = document.createElement('script')
script.type = 'text/javascript'
script.src = 'http://www.sample.com/test?callback=mycallback'
document.head.appendChild(script)

// 回调函数
function mycallback(response) {
console.log(response)
}

后端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

use Psr\Http\Message\RequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\Factory\AppFactory;

require __DIR__ . '/../vendor/autoload.php';

$app = AppFactory::create();
$app->get('/test', static function (Request $request, Response $response, array $args) {
$content = ['status' => true, 'message' => 'OK', 'result' => []];
$content = json_encode($content);
$response->getBody()->write("mycallback({$content})");

return $response;
});
$app->run();

jQuery Ajax 方案

前端代码:

1
2
3
4
5
6
7
8
9
10
11
$.ajax({
type: 'get',
url: 'http://www.sample.com/test',
dataType: 'jsonp',
jsonpCallback: "mycallback",
data: { hello: 'world' }
})

function mycallback(response) {
console.log(response)
}

后端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php

use Psr\Http\Message\RequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\Factory\AppFactory;

require __DIR__ . '/../vendor/autoload.php';

$app = AppFactory::create();
$app->get('/test', static function (Request $request, Response $response, array $args) {
$params = $request->getQueryParams();
$content = ['status' => true, 'message' => 'OK', 'result' => [$params]];
$payload = json_encode($content);
$response->getBody()->write("mycallback({$payload})");

return $response->withHeader('Content-Type', 'application/json')
->withStatus(200);
});
$app->run();

以上的方式看起来不错,但遗憾的是仅支持 GET 请求。

document.domain + iframe 跨域

这个跨域仅限主域相同,子域不同的跨域应用场景。

两个页面都通过 js 强制设置 document.domain 为主域,就实现了同域。

父窗口代码:

1
2
3
4
5
6
7
8
9
<iframe src="http://sub.example.com" frameborder="0"></iframe>
<script>
document.domain = 'example.com'
var content = {
status: true,
message: 'OK',
result: {}
}
</script>

子窗口代码:

1
2
document.domain = 'example.com'
console.log(window.parent.content)

location.hash + iframe 跨域

window.name + iframe 跨域

postMessage 跨域

window.postMessage() 方法可以安全地实现 Window 对象之间的跨域通信。postMessage() 方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。

发送代码:

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>

<body>
<div>
<iframe src="http://www.sample.com/index.html" id="frame" style="display:none;"></iframe>
<div id="message"></div>
</div>
<script>
const domain = 'http://www.sample.com'
let iframe = document.getElementById('frame')
iframe.onload = (e) => {
let data = { message: 'Hello' }
iframe.contentWindow.postMessage(JSON.stringify(data), domain)
console.group('Mesage request from ' + document.location.href)
console.log(data)
console.groupEnd('adsfa')
}

window.addEventListener('message', (e) => {
console.group('Mesage response from ' + e.origin)
console.log(e.data)
console.groupEnd()
}, false)
</script>
</body>

</html>

接收代码:

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>

<body>
<script>
const domain = 'http://www.example.com'
window.addEventListener('message', (e) => {
if (e.origin !== domain) {
return
}
let data = JSON.parse(e.data)
data.status = true;
data.message += ' World!'
data.result = {}

window.parent.postMessage(data, domain);
})
</script>
</body>

</html>

WebSocket 协议跨域

WebSocket 是 HTML5 的一种新通信协议,它实现了浏览器与服务器之间的双向通讯,属于应用层协议。它基于 TCP 传输协议,并复用 HTTP 的握手通道。由于原生 WebSocket API 使用起来不太方便,我们使用 Socket.IO,而 Socket.IO 是一个完全由 JavaScript 实现、基于 Node.js、支持 WebSocket 的协议用于实时通信、跨平台的开源框架,它包括了客户端的 JavaScript 和服务器端的 Node.js。Socket.IO 除了支持 WebSocket 通讯协议外,还支持许多种轮询(Polling)机制以及其它实时通信方式,并封装成了通用的接口,并且在服务端实现了这些实时机制的相应代码。Socket.IO 实现的 Polling 通信机制包括 Adobe Flash Socket、AJAX 长轮询、AJAX multipart streaming、持久 Iframe、JSONP 轮询等。Socket.IO 能够根据浏览器对通讯机制的支持情况自动地选择最佳的方式来实现网络实时应用。它的设计目的是构建能够在不同浏览器和移动设备上良好运行的实时应用,如实时分析系统、二进制流数据处理应用、在线聊天室、在线客服系统、评论系统、WebIM 等。

前端代码:

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
<!DOCTYPE html>
<html lang="en">

<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>

<body>
<div>Input: <input type="text" id="input"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.3.0/socket.io.js"></script>
<script>
const socket = io('http://www.sample.com:6001');

socket.on('connect', () => {
socket.on('message', (message) => {
console.group('From server: ')
console.log(message)
console.groupEnd()
})

socket.on('disconnect', () => {
console.log('Server closed')
})
})

document.getElementById('input').onblur = (e) => {
let value = e.target.value
console.group('Send message:')
console.log(value)
console.groupEnd();
socket.emit('message', value);
}
</script>
</body>

</html>

后端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?php

use PHPSocketIO\SocketIO;
use Workerman\Worker;

require __DIR__ . '/../vendor/autoload.php';

$socket = new SocketIO(6001);
$socket->on('connection', function ($socket) {
$socket->on('message', function ($message) use ($socket) {
$data = [
'status' => true,
'message' => $message,
'result' => [],
];
$socket->emit('message', $data);
});

$socket->on('discount', static function () {
});
});

Worker::runAll();

未完待续。。。

本笔记是笔者在学习和工作中的一些整理,如对您有用,请鼓励我继续写作