前端跨域主要分为一下几个知识点:

  1. 跨域问题的产生和意义
  2. JSONP跨域的解决方案和底层原理
  3. CORS(Cross-Origin Resource Sharing)跨域资源共享
  4. 基于HTTP Proxy实现跨域请求
  5. 基于postMessage实现跨域处理
  6. 基于Websocket实现跨域的解决方案
  7. 基于Nginx反向代理实现跨域处理

同源策略是Netscape公司于1995年引入浏览器的,它最开始的目的是为了隔离不同网站的数据。由于现在的网站都广泛的依赖于HTTP Cookies来维持自身的状态,那么暴露Cookies就显得十分危险了,这时候就需要有一种约束机制,让客户端严格隔离无关站点提供的内容,防止机密数据丢失。

同源策略(SOP, Same-Origin-Policy)即前端可以访问当前URL下的任何服务器,但只有协议号、域名、端口三者有任何一项不同,那么就不能构成同源,此时访问服务器就是跨域请求。跨域是一种浏览器禁止的行为,而不是后端服务器禁止的行为,所以我们在后端HTTP报文流出时,添加浏览器认可的请求头,让浏览器允许通过,来达到跨域的目的

一般是通过设置CORS Header的内容实现跨域,常见的CORS Header主要有一下几种:

  • Access-Control-Allow-Origin : 指示请求的资源能共享给哪些域,可以是具体的域名或者*表示所有域。
  • Access-Control-Allow-Credentials : 指示当请求的凭证标记为 true 时,是否响应该请求。
  • Access-Control-Allow-Headers : 用在对预请求的响应中,指示实际的请求中可以使用哪些 HTTP 头。
  • Access-Control-Allow-Methods: 指定对预请求的响应中,哪些 HTTP 方法允许访问请求的资源。

跨域请求报错:

1
Access to XMLHttpRequest at 'http://localhost:1000/books' from origin 'http://127.0.0.1:5500' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

XMLHttpRequest 可以在页面不刷新的情况下请求服务器资源,是实现Ajax技术的原理

JSONP

在前端代码中,凡是https://ymlog.cn/file/script/link/iframe标签,可以请求任意服务器的数据,不会造成跨域问题,JSONP就是利用script标签的这种特性,客户端请求时将Callback函数放在src中,服务端将函数名和数据部分作为响应一起发送给客户端,客户端收到后直接执行函数名并将数据作为参数传入,从而绕过跨域的限制。

JSONP的几个缺点:

  1. func为全局函数
  2. JSONP需要服务器端支持
  3. JSONP只能处理GET请求
  4. 响应能被拦截,不安全

后端代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const express = require("express");
const app = express();
const http = require("http").createServer(app);
const PORT = 1000;
const cors = require("cors");

app.use(express.static('src'));

app.get("/books", (req, res) => {
let {
callback = Function.prototype
} = req.query; // 获取请求部分
let data = {
one: '沉默的大多数',
two: '尘埃落定'
}

res.send(`${callback}(${JSON.stringify(data)})`);
})

http.listen(PORT, () => {
console.log(`listen on http://localhost:${PORT}`);
})

前端代码(jquery实现):

1
2
3
4
5
6
7
8
$.ajax({
url: 'http://localhost:1000/books',
dataType: "jsonp", // 数据类型为JSONP
method: 'get', // 请求方法为GET
success: data => {
console.log(data);
}
})

jqury_JSONP

可以看到,jquery帮我们构造了一条 这样的请求:

1
http://localhost:1000/books?callback=jQuery35103885115371400778_1604278755622&_=1604278755623

&_后面的内容是为了防止浏览器缓存添加的字段,可以不用考虑,jQuery后面一串数字是jquery帮我们生成的函数名, 我们发送的请求就是:

1
http://localhost:1000/books?callback=funcName

后端接收到请求,将callback赋值为funcName,然后把funcName(data)这样的字符串发送给客户端,客户端拿到之后直接执行,执行的就是客户端自己定义的funcName,这里是success函数执行结果。这是用jquery封装的JSONP,原生的JSONP应该是如下内容(后端代码不需要修改):

1
2
3
4
5
6
7
8
9
10
请求
<script>
function getBooks(data) {
console.log("接收到了数据:", data);
}
</script>
<script src="http://localhost:1000/books?callback=getBooks"></script>

响应
接收到了数据: {one: "沉默的大多数", two: "尘埃落定"}

CORS

上面的JSONP需要客户端和服务端的代码配合,而CORS技术则只需要服务端调整代码即可。CORS通过在HTTP首部字段中添加相关参数,从而允许其他站点的浏览器向本机发出HTTP请求并返回响应。

另外,如果用CORS技术实现跨域,浏览器必须首先使用OPTIONS方法发起一个预检请求(preflight request),从而获知服务端是否允许跨域请求,服务端允许之后,才能发起实际的HTTP请求,服务端也可以通知客户但是否需要携带身份凭证,包括Cookies等HTTP认证数据,预请求是浏览器自动完成的,不需要用户参与。

缺点:允许多源则不可以携带Cookies,只能在允许一个源客户端和允许所有之间选择

CORS实现跨域,前端无需修改,代码主要几种在后端,这里的前端代码没有使用ajax,而是用原生 JS写了Ajax请求。

frontend:

1
2
3
4
5
6
7
var URL = "http://localhost:1000/books"
let invocation = new XMLHttpRequest();
invocation.open("GET", URL, true);
invocation.send();
invocation.onload = () => {
console.log(invocation.responseText);
}

backend: 后端代码通过给HTTP首部添加字段,让服务器允许跨域

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
const express = require("express");
const app = express();
const http = require("http").createServer(app);
const PORT = 1000;
const cors = require("cors");
app.use(express.static('src'));

// 所有的请求都会被这一层做一次处理,相当于Python中的Middleware
app.use((req, res, next) => {
// 资源允许xxx访问
res.header("Access-Control-Allow-Origin", "http://127.0.0.1:5500"); // * 表示允许所有浏览器访问
// 允许客户端携带验证信息,例如Cookies
res.header("Access-Control-Allow-Credentials", true);
// 服务器支持的头部信息
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,HEAD,OPTIONS");
if (req.method === 'OPTIONS') {
res.sendStatus(200);
return;
}
next(); // 将请求往下传递,继续匹配路由
});


app.get("/books", (req, res) => {
res.send("SUCCESS!")
})

http.listen(PORT, () => {
console.log(`listen on http://localhost:${PORT}`);
})

HTTP Proxy

代理的方式可以实现跨域,主要用到的技术是Webpack,主要分为以下步骤:

  1. 安装Webpack环境
  2. 编写JS代码,发起跨域请求(正常写,但是URL要写成/api/books的形式,不加HOST)
  3. 在webpack.config.js中配置proxy代理,将JS请求换源,以target为origin发送出去(其中还可以对URL进行操作)
  4. 编写后端代码(正常写)

所以我们需要安装一下Webpack的工程

1
2
3
4
5
6
mkdir htpx
cd htpx
yarn add webpack@4.44.1 webpack-cli@3.3.1 webpack-dev-server@3.11.0 html-webpack-plugin
touch index.js
touch index.html
touch webpack.config.js

编写Webpack配置项,对devServer添加代理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const HtmlPlguin = require("html-webpack-plugin");
module.exports = {
entry: './index.js',
devServer: {
port: 3000,
progress: true,
proxy: {
'/api': {
target: 'http://localhost:1000',
pathRewrite: {'^/api' : ''}, // 重写路径 ,将/api替换为空字符串
changeOrigin: true // 更改发起请求的源,如果是跨域则要选择true
}
}
},
plugins: [new HtmlPlguin({template: "index.html"})]
}

前端代码:

1
2
3
4
5
6
7
8
9
document.getElementById("btn").addEventListener('click', () => {
var URL = "/api/books"
let invocation = new XMLHttpRequest();
invocation.open("GET", URL, true);
invocation.send();
invocation.onload = () => {
console.log(invocation.responseText);
}
})

后端代码:

1
2
3
4
5
6
7
8
9
10
11
12
const express = require("express");
const app = express();
const http = require("http").createServer(app);
const PORT = 1000;

app.get("/books", (req, res) => {
res.send("SUCCESS!")
})

http.listen(PORT, () => {
console.log(`listen on http://localhost:${PORT}`);
})

Nginx

后端代码需要改一下允许访问的IP,我们改为0.0.0.0,即允许所有IP访问,其他保持一致

1
2
3
4
5
6
7
8
9
10
11
12
13
const express = require("express");
const app = express();
const http = require("http").createServer(app);
const PORT = 1000;

app.get("/books", (req, res) => {
res.send("SUCCESS!")
})

http.listen(PORT,"0.0.0.0", () => {
console.log(`listen on http://talk.ymlog.cn:${PORT}`);
})

前端代码和原来一样,只是修改一下里面的URL:

1
2
3
4
5
6
7
8
9
document.getElementById("btn").addEventListener('click', () => {
var URL = "http://talk.ymlog.cn:1000/books"
let invocation = new XMLHttpRequest();
invocation.open("GET", URL, true);
invocation.send();
invocation.onload = () => {
console.log(invocation.responseText);
}
})

尝试着访问,发现跨域导致异常,下面开始配置nginx。

cors-nginx-realize

配置nginx 并开启 node 程序

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
user nginx;
worker_processes auto;
pid /run/nginx.pid;
include /usr/share/nginx/modules/*.conf;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# 定义后端代理池,这里是node的启动ip和端口
upstream nodeapp {
server talk.ymlog.cn:1000;
}
server {
listen 80 default_server; # 监听80端口
location / {
proxy_pass http://nodeapp; # 将80端口收到的报文转发到nodeapp代理池中
add_header Access-Control-Allow-Origin *; # 添加头部信息
add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
add_header Access-Control-Allow-Headers 'Keep-Alive,User-Agent,If-Modified-Since,Cache-Control,Content-Type,Authorization';
if ( $request_method = 'OPTIONS' ){
return 204;
}
}
}
}

前端代码中,需要把请求的host改为:

1
var URL = "http://talk.ymlog.cn/books"

综上就是nginx跨域的详细信息,就是用nginx给http报文添加头部信息,从而让浏览器允许跨域行为。

postMessage

postMessage方法可以向其他窗口发送一个MessageEvent消息,接收窗口可以调用”message”监听事件获取到该消息,比如有两个页面,分别为index1.html和index2.html,由1向2发送消息,2接收1传来的消息,2接收到1的消息并打印到控制台

[send]index1.html:http://localhost:5500/index1.html

1
2
3
4
5
6
7
8
9
10
11
<body>
<iframe src="http://localhost:1000/index2.html" style="display: none;" id="iframe1"></iframe>
<script>
// 发送事件
let f = document.getElementById("iframe1");
// 注意,这里要写onload,等到iframe加载完成再发送消息,不然报错: The target origin provided does not match the recipient window's origin ('null')
f.onload = function () {
f.contentWindow.postMessage('hello, I am index1', 'http://localhost:1000/')
}
</script>
</body>

[recv]index2.html:http://localhost:1000/index2.html

1
2
3
4
5
6
7
8
9
10
11
12
<body>
<h1>hi iframe2 </h1>
<iframe style="display: none;"
id="iframe2"></iframe>
<script>

// 事件监听 , 除了event.data,还有event.source.postMessage和event.origin,可以回发消息
window.addEventListener("message", (event) => {
console.log(event.data);
})
</script>
</body>

WebSocket

WebSocket自带跨域的功能,因为WebSocket只在和服务器建立连接时采用HTTP协议,而当连接建立之后,采用WS://WSS:// 进行通信;浏览器的跨域拦截主要是通过过滤HTTP的报头来实现的,但是WebSocket不使用HTTP进行数据传递,所以浏览器没有拦截WebSocket数据的能力。

WebSocket 是一个全新的、独立的协议,基于 TCP 协议,与 HTTP 协议兼容却不会融入 HTTP 协议,仅仅作为 HTML5 的一部分。相比于传统的HTTP每次“请求-应答”,Websocket采取长连接,一旦连接建立,除非一方主动断开连接,否则可以一致以较小的开销传递数据,且是全双工的方式传递(WebSocket的Client和Server都可以向对方主动发送和接收数据)。

websocket-communication

目前WebSocket对浏览器的兼容性不是很好,Socket.IO基于Node和JavaScript封装了WebSocket,包含Client和Server,但在这里,我们还是写原生的WebSocket


Frontend:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建websocket对象
var ws = new WebSocket("ws://localhost:1000");

// 监听建立连接
ws.onopen = function (res) {
ws.send("Hi"); // 向服务器发送数据
console.log("连接成功!");
}

//监听有新消息
ws.onmessage = function (res) {
console.log(res.data); //接收服务器发来的数据
}

Backend:

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
# yarn add websocket 安装websocket
const express = require("express");
const app = express();
const http = require("http").createServer(app);
const WebSocket = require("websocket").server;
const PORT = 1000;

http.listen(PORT, () => {
console.log(`listen on http://localhost:${PORT}`);
})


let ws = new WebSocket({
httpServer: http,
autoAcceptConnections: false
});

ws.on("request", (request) => {
let connect = request.accept(null, request.origin);// 获取当前连接
console.log(`A new connection on`)

setInterval(()=>{
connect.sendUTF(new Date()); // 持续向客户端发送数据
},1000)
})
websocket

Reference

[1] https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Access_control_CORS

[2] https://www.ruanyifeng.com/blog/2016/04/cors.html

[3] https://github.com/amandakelake/blog/issues/62

[4] https://zhuanlan.zhihu.com/p/23467317

[5] https://github.com/chimurai/http-proxy-middleware

[6] https://juejin.im/post/6844904161369128967

[7] https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage

[8] http://jartto.wang/2019/06/11/post-message/

[9] https://icewind-blog.com/2014/08/22/the-principle-of-javascript-prohibits-cross-domain/

[10] https://blog.securityevaluators.com/websockets-not-bound-by-cors-does-this-mean-2e7819374acc

[11] https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket

[12] https://www.ruanyifeng.com/blog/2017/05/websocket.html