跨域

跨域:指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。

同源策略(Sameoriginpolicy) 是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。同源策略会阻止一个域的javascript脚本和另外一个域的内容进行交互。

所谓 同源 (即指在同一个域)就是两个页面具有相同的 协议**(protocol),主机**(host)和 **端口号**(port)

跨域的解决方法

1. jsonp

我们在做项目的时候是不是经常会通过script标签来引入资源,但是

html
1
<script src="https://unpkg.com/vue/dist/vue.js"></script>

我们先引入一个vue文件,然后看看network

image.png
可以发现script标签的src属性可以跨域,其实不止script标签可以跨域,还有<img> <link>标签。

jsonp的原理其实就是通过script标签的src属性来实现的。

我们来模拟实现一下,后端服务器用 koa2 搭建。

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 后端
router.get('/string', async (ctx, next) => {
const {name,age} = ctx.request.query
const data = `我叫${name},今年${age}岁!`
ctx.body = `func(${JSON.stringify(data)})`
})

// js
var script = document.createElement('script');
script.src = 'http://localhost:3000/string?name=leo&age=30&callback=func';
document.body.appendChild(script)
function func(res){
console.log(res) // 我叫leo,今年30岁!
}

仔细看src其实跟普通的get请求差不多,唯一就是多了个callback,而callback才是重点,其作用是为了接收接口请求回来的数据。

当我们执行完代码,可以发现控制台输出了后端返回给我们的数据,我们再看看f12的source,发现执行了该回调函数并带上了后端返回的数据给我们。

image.png

这样就实现了跨域,整个过程其实就是前端定义回调函数,后端返回回调函数并带上数据。

缺点

需要前后端协商好回调函数命名,并且只支持get请求

2. CORS

跨域资源共享(Cross-Origin Resource Sharing)是一种机制,用来允许不同源服务器上的指定资源可以被特定的Web应用访问。

举个例子:如果A站的网页a.com访问B站网页b.com的API的时候,B站能够返回响应头Access-Control-Allow-Origin: http://a.com,那么,浏览器就允许A站的JavaScript访问B站的API。

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 后端
// 使用koa-cors中间件 app.js
const cors = require('koa-cors');
app.use(cors())

router.get('/string', async (ctx, next) => {
ctx.body = `我叫笨鸟`
})

// 前端 http://localhost:8099/cross.html
var xmlhttp;
if (window.XMLHttpRequest) {
xmlhttp=new XMLHttpRequest();
} else {
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState==4 && xmlhttp.status==200) {
console.log(xmlhttp.responseText) // 我叫笨鸟
}
}
xmlhttp.open("GET","http://localhost:3000/string",true);
xmlhttp.send();

1639327794(1).jpg
浏览器发现这次跨域 AJAX 请求是简单请求,就自动在头信息之中,添加一个Origin字段。服务器会在返回的头部信息中添加Access-Control-Allow-Origin字段,浏览器会根据该字段与当前页面域名进行判断,如果一致就会放行。

优点

CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

3. websocket

WebSocket是一种在单个TCP连接上进行全双工通信的协议。

下面是一个websocket请求的的请求头

image.png

  • Connection: Upgrade:表示要升级协议
  • Upgrade: websocket:表示要升级到websocket协议
  • Sec-WebSocket-Key:与服务端响应的头部的Sec-webSocket-Accept是配套的,提供基本的防护,比如恶意链接或者无意链接
  • Sec-WebSocket-Version:websocket版本,如果服务端不支持该版本,需要返回一个Sec-WebSocket-Versionheader,里面包含服务端支持的版本号。

websocket为什么没有同源限制?

因为websocket使用类似ws://这样的方式进行连接,并不是使用http协议进行数据传输。所以浏览器的SOP无法限制它。而且websocket本来就是设计成支持跨域访问的协议的。在websocket请求的请求头中会像cors一样加入origin字段,服务端可以根据这个字段来判断是否通过该请求。

实现

js
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
// 后端 ws.js(需要通过node ws.js 命令启动服务)
const WebSocket = require('ws')
const ws = new WebSocket.Server({ port: 3888 }, () => {
// 监听接口
console.log('socket start')
})
ws.on('connection', (client) => {
client.send('连接成功')
client.on('message', (msg) => {
const { name, age } = JSON.parse(msg.toString())
client.send(`我叫${name},今年${age}岁`) // 通过send方法来给前端发送消息
})
client.on('close', (msg) => {
console.log('关闭服务器连接')
})
})

// 前端

// html
<button onclick="send()">发送消息</button>
//js
const ws = new WebSocket("ws://localhost:3888/") // 监听地址端口号
ws.onopen = function(){
console.log("服务器连接")
}
ws.onmessage= (msg)=>{
console.log(msg.data)
}
ws.onclose=()=>{
console.log("服务器关闭")
}
function send(){
let msg = {
name: '笨鸟',
age: 22
}
ws.send(JSON.stringify(msg))
}

image.png

4. postMessage

postMessage() 方法允许来自不同源的脚本采用异步方式进行有效的通信,可以实现跨文本文档,多窗口,跨域消息传递。多用于窗口间数据通信,这也使它成为跨域通信的一种有效的解决方案。

简而言之,可以跨域实现两个网页间的通讯

我在网上搜到关于postMessage原理和应用场景写的比较好的文章

注意点

  • 首先,信息传递安全问题。为了你的信息传递能准确传达,无论是作为主页面还是子页面,传递重要信息时都应该填写 origin 而不是 “*”,避免被截获。
  • 其次,iframe 或者 window.open 的 load 事件是不准确的。为了避免漏发漏接的情况,建议在通讯页面里回传加载状态。

a页面

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// http://localhost:8099/a.html
<div id="app">
我是a页面
<div id="recMessage"></div>
</div>

window.onload = function() {
var messageEle = document.getElementById('recMessage');
window.addEventListener('message', function (e) { // 监听 message 事件
if (e.origin !== "http://localhost:8848") { // 验证消息来源地址
return;
}
messageEle.innerHTML = "从"+ e.origin +"收到消息: " + e.data;
});
}

b页面

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// http://localhost:8848/b.html
<div id="app">
我是b页面
<iframe id="receiver" src="http://localhost:8099/a.html" frameborder="0"></iframe>
<button id="sendMessage">发送信息</button>
</div>

window.onload = function() {
var receiver = document.getElementById('receiver').contentWindow;
var btn = document.getElementById('sendMessage');
btn.addEventListener('click', function (e) {
e.preventDefault();
receiver.postMessage("Hello!我是来自8848端口的b页面",'http://localhost:8099');
});
}

postmessage.png

5. node中间件代理

实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。

  • 接受客户端请求 。
  • 将请求 转发给服务器。
  • 拿到服务器 响应 数据。
  • 将 响应 转发给客户端。

image.png

1) 非vue框架的跨域

koa2 + node + koa-server-http-proxy

koa-server-http-proxy相当于http-proxy-middleware的koa版本

前端

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  var xmlhttp;
if (window.XMLHttpRequest) {
xmlhttp=new XMLHttpRequest();
} else {
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState==4 && xmlhttp.status==200) {
console.log(xmlhttp.responseText) // 我叫笨鸟
}
}
xmlhttp.open("GET","http://localhost:3000/string",true);
xmlhttp.send();

后端1

js
1
2
3
4
5
6
7
8
9
// http://localhost:3000
const proxy = require('koa-better-http-proxy')
const cors = require('koa-cors')
app.use(cors())
app.use(
proxy('127.0.0.1', {
port: 3001,
})
)

后端2

js
1
2
3
4
5
// http://localhost:3001
router.get('/string', async (ctx, next) => {
ctx.body = `我叫笨鸟`
})

2) vue框架的跨域

平时我们用脚手架搭建的vue项目,为啥在webpack.config.js配置文件配置proxy参数就可以实现代理?

当你运行项目的时候,会配置启动一个node服务,这个服务的作用1是静态文件服务,让你可以访问到html/js等文件包括监听文件变动等;2是启动一个http代理,你js发送的请求会请求到这个服务A,由服务A代理到服务B,而服务A和静态文件服务器是同源的,并不影响同源策略。
脚手架的代理
vue-cliproxyTable用的是http-proxy-middleware中间件

create-react-app用的是webpack-dev-server内部也是用的http-proxy-middleware

webpack.config.js部分配置

js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
entry: {},
module: {},
...
devServer: {
historyApiFallback: true,
proxy: [{
context: '/login',
target: 'http://www.domain2.com:8080', // 代理跨域目标接口
changeOrigin: true,
secure: false, // 当代理某些https服务报错时用
cookieDomainRewrite: 'www.domain1.com' // 可以为false,表示不修改
}],
noInfo: true
}
}

6. nginx 反向代理

实现原理类似于 Node 中间件代理,需要你搭建一个中转 nginx 服务器,用于转发请求。

使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。

nginx配置(nginx -s reload启动nginx)

Code
1
2
3
4
5
6
7
 server {
listen 8888;
server_name localhost;
location / {
proxy_pass http://localhost:3001;
}
}

后端

js
1
2
3
4
// http://localhost:3001
router.get('/string', async (ctx, next) => {
ctx.body = `我叫笨鸟`
})

前端

js
1
2
3
4
5
6
7
8
9
10
11
12
13
  var xmlhttp;
if (window.XMLHttpRequest) {
xmlhttp=new XMLHttpRequest();
} else {
xmlhttp=new ActiveXObject("Microsoft.XMLHTTP");
}
xmlhttp.onreadystatechange = function () {
if (xmlhttp.readyState==4 && xmlhttp.status==200) {
console.log(xmlhttp.responseText) // 我叫笨鸟
}
}
xmlhttp.open("GET","http://localhost:8888/string",true);
xmlhttp.send();

总结

  • CORS 支持所有类型的 HTTP 请求,是跨域 HTTP 请求的根本解决方案
  • JSONP 只支持 GET 请求,JSONP 的优势在于支持老式浏览器,以及可以向不支持 CORS 的网站请求数据。
  • 不管是 Node 中间件代理还是 nginx 反向代理,主要是通过同源策略对服务器不加限制。
  • 日常工作中,用得比较多的跨域方案是 cors 和 nginx 反向代理