引言
当你的团队在写一个项目时,你写前端,你的同事写后端,他给了你一个接口,说这是首页的地址朝这里发请求能拿到数据,如果你直接用你的前端朝那个后端接口发请求,是拿不到数据的,因为浏览器存在一个同源策略,想要解决这个问题必须使用跨域的解决方案
同源策略
同源策略是浏览器实施的一种安全策略,用于限制一个源上的项目与另一个源上的项目进行资源的交互,保护后端资源安全。这里的“同源”是指协议、域名和端口号三部分都一样。
前端和后端运行在不同的端口上,它们是不同源的。当前端向后端请求数据时,浏览器能把这个请求发送出去,但是会拒收后端发送回来的响应
跨域的实现方案
此时我有一份前端代码,它在页面上什么都不展示,只朝 3000 端口发送一个请求
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
const xhr = new XMLHttpRequest()
xhr.open('GET', 'https://localhost:3000')
xhr.send()
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText)
}
}
</script>
</body>
</html>
还有一份后端代码,监听 3000 端口,结束响应时发送 ‘hello,world’给客户端
const http = require('http')
http.createServer((req, res) => {
res.end('hello,world')
}).listen(3000)
事实上仅仅如此浏览器是收不到后端返回的响应的,为了解决跨域问题,我们可以采用以下几种解决方案——
JSONP
不难发现,我们通过<img src=""">
<video src="">
<script src="">
<link rel="stylesheet" href="http://XXXX.css">
这些标签发送的请求都能成功接收到服务端返回的资源,因为这些标签的 src
或 href
属性不受同源策略影响
为了可以成功朝 3000 端口发送请求再收到响应,并能对响应的数据进行处理,总体来说进行以下步骤:
- 借助
script
标签的src
属性来发送请求 - 给后端携带一个参数 callback 并在前端定义 callback 的函数体
- 后端返回 callback 字符串形式的函数调用,并将要响应的数据作为 callback 的实参
- 当浏览器接收到响应后,就会触发全局的 callback 函数,从而让 callback 以参数形式接收到后端响应的数据
代码实现如下:
<!-- 前端 -->
<body>
<script>
function jsonp(url, cb) {
return new Promise((resolve, reject) => {
// 创建一个 script 标签
const script = document.createElement('script')
// 把url交给script 的 src
script.src = `${url}?cb=${cb}`
// 全局创建一个函数体cb
window[cb] = function(data) {
resolve(data)
}
// 插入到 HTML 中,否则无法发送请求
document.body.appendChild(script)
})
}
jsonp('http://localhost:3000', 'callback').then(res => {
console.log(res); // res就是我们所需的数据
})
</script>
</body>
// 后端
const http = require('http');
http.createServer(function(req, res) {
// 从请求的url中获取参数
const query = new URL(req.url, `http://${req.headers.host}`).searchParams
// 如果查询参数中存在 cb
if(query.get('cb')) {
const cb = query.get('cb')
const data = "hello world"
const result = `${cb}("${data}")` // 字符串形式的函数调用 'cb("hello world")'
res.end(result)
}
}).listen(3000)
注意!当执行到以下两行代码时,能凭空创建一个 script 标签,并给 src 赋值,但是这个 script 只有被添加到 html 中的时候才会去请求资源
const script = document.createElement('script') script.src = url
而如果是创建了一个 Image,不管这个 Image 是被添加到了 html 中还是只是在 js 中,只要它的 src 有值,都会去请求图片资源
const image = new Image() image.src = url
JSONP 方法用的也不是很多,因为有两个缺陷:
- 需要后端配合 —— 后端不能直接返回数据,而是要返回一个字符串形式的函数调用,把数据塞进这个函数作为参数
- 只能发送GET请求——但凡后端的接口是以POST方式接收请求,就用不了这种方法
cors (Cross-Origin Resource Sharing)
同源策略是不允许接收响应而不是不允许发送请求,所以可以通过在响应头中设置某些字段来允许满足条件的请求跨域,比如设置 Access-Control-Allow-Origin
字段允许来自某个源的请求跨域,比如设置 Access-Control-Allow-Methods
字段允许'GET'或者'POST'方式的请求跨域
例如,以下后端允许来自本机 5500 端口的 POST 请求可以跨域:
// 后端
const http = require('http')
const server = http.createServer((req, res) => {
// 设置响应头
res.writeHead(200, {
'Access-Control-Allow-Origin': 'http://127.0.0.1:5500',
'Access-Control-Allow-Methods': 'POST'
})
res.end('hello,world')
}).listen(3000)
代理 和 nginx
通过设置一个 node 后端作为中间层,前端发送的请求首先到达这个中间层,然后再由中间层将请求转发到目标服务器。响应过程也是如此,服务器先响应给中间层,中间层再将响应数据发送回前端。
这个中间层就起到了一个代理的作用。这样,浏览器看到的是同源请求,从而绕过了CORS限制。
假设前端现在要将请求发送给 http://192.168.1.63:3000 这个后端,就可以先由本机的 3001 端口作一个代理
<!-- 前端 -->
<body>
<script>
const xhr = new XMLHttpRequest()
xhr.open('GET', 'https://localhost:3001')
xhr.send()
xhr.onreadystatechange = function() {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText)
}
}
</script>
</body>
// 后端
const http = require('http')
// 监听本机3001端口,有新请求时调用回调函数
http.createServer((req, res) => {
// 设置响应头,以允许前端应用访问响应内容
res.writeHead(200, {
'Access-Control-Allow-Origin': '*'
})
// 转发请求到目标服务器,并处理响应
http.request({
host: '192.168.1.63',
port: 3000,
path: '/',
method: 'GET',
headers: {}
}, proxyRes => {
proxyRes.on('data', chunk => {
res.end(chunk.toString())
})
}).end()
}).listen(3001)
现在企业开发中如果要使用代理的方法解决跨域的问题,大多采用 nginx
代理服务器。这个代理不是用 node 写的,它被封装成了一个软件,在服务器装上后写入一点配置,同样实现上述最基本的 node 代理效果,并且更强大更灵活。
(图片来源网络)
WebSocket
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。与 HTTP 不同,WebSocket 提供了一种持久的、低延迟的双向通信机制,适用于实时应用,如在线聊天、实时游戏、股票行情等。
特点:
- 能跨域:WebSocket 协议天生不受同源策略的限制
- 全双工通信:WebSocket 允许客户端和服务器之间同时发送和接收数据,而不需要像 HTTP 那样每次请求都需要重新建立连接。
- 持久连接:一旦 WebSocket 连接建立,它会保持打开状态,直到显式关闭或网络中断。
利用 WebSocket 协议不受同源策略限制的特点,我们可以用它来发送跨域的请求、接收到响应并能对响应回的数据做处理
<!-- 前端 -->
<body>
<script>
function myWebSocket(url, params = {}) {
return new Promise((resolve, reject) => {
// 创建一个 WebSocket 对象
const socket = new WebSocket(url)
// 当 WebSocket 连接成功打开时触发回调函数
socket.onopen = () => {
// 给后端传参数,只能是字符串
socket.send(JSON.stringify(params))
}
// 监听后端消息,接收到后端消息时触发回调函数
socket.onmessage = function(e) {
resolve(e.data)
}
})
}
myWebSocket('ws://localhost:3000', {age: 18}).then(res => {
console.log(res); // 响应回的数据
})
</script>
</body>
先初始化一个后端项目;由于node 中不自带WebSocket
库,所以要 npm 安装 ws 模块
npm init -y
npm i ws
const WebSocket = require('ws')
// 创建一个 WebSocket 服务器,监听 3000 端口
const ws = new WebSocket.Server({port: 3000});
// 监听连接,当有新的连接时触发
ws.on('connection', (obj) => {
// 接收到前端消息时触发
obj.on('message', (data) => {
// 向前端发送 'hello socket' 消息
obj.send('hello socket')
})
})
postMessage
postMessage
是 HTML5 引入的一个 API,允许来自不同源的脚本采用异步方式进行有限的通信
现有首页和详情页两个页面,通过在首页放置<iframe src=""></iframe>
标签来内嵌详情页,形成了一种父级窗口和子级窗口的关系。
将父窗口跑在本机5500端口,子窗口跑在本机8080端口,如果两个窗口要直接通信,本来是会被同源策略限制的,但是使用 postMessage 方法可以跨域
父窗口
- 获取 iframe 的 Window对象
- 通过
postMessage
API 向iframe发送数据 - 通过
window.onmessage
监听并接收消息
<body>
<h2>首页</h2>
<!-- 内嵌网站 -->
<iframe id="frame" src="http://127.0.0.1:8080" width="100%" height="500px" frameborder="0"></iframe>
<script>
// 创建要传递的消息对象
let obj = {name: '今天一定晴', age: 18}
// iframe 加载完之后向其发送消息
document.getElementById('frame').onload = function() {
// 获取iframe DOM的 window 对象,向其发送消息
this.contentWindow.postMessage(obj, 'http://127.0.0.1:8080')
// 接收其他窗口传递过来的消息
window.onmessage = function(e) {
console.log(e.data)
}
}
</script>
</body>
子窗口
- 通过
window.onmessage
监听并接收消息 - 结构出数据,插入到DOM结点中
- 通过
postMessage
给父窗口传递消息
<body>
<h3>详情页 -- <span id="title"></span> </h3>
<script>
let title = document.getElementById('title')
window.onmessage = function(e) {
let {data: {name, age}, origin} = e
title.innerText = name + age
// 给父窗口传递消息
e.source.postMessage(`今天一定晴 ${++age} 岁`, origin)
}
</script>
</body>
document.domain
当两个页面通过 iframe 进行嵌套,且两个页面的二级域名一致时,能通过设置相同 document.domain
值的方法来进行跨域
- 什么是二级域名?
一个完整的域名可以被分为多个部分,每个部分由点(.
)分隔。从右到左,这些部分依次是顶级域名、二级域名、三级域名等。每个根域名左边的部分就是它的子域名
父窗口
<body>
<h2>首页</h2>
<!-- 内嵌网站 -->
<iframe id="frame" src="http://127.0.0.1:5500/postMessage/detail.html" width="100%" height="500px" frameborder="0"></iframe>
<script>
// 设置当前页面的 document.domain 为 '127.0.0.1'
document.domain = '127.0.0.1'
// iframe 加载完之后执行函数
document.getElementById('frame').onload = function() {
console.log(this.contentWindow.data);
}
</script>
</body>
子窗口
<body>
<h3>详情页 </h3>
<script>
document.domain = '127.0.0.1'
var data = '小晴'
</script>
</body>
当两个页面属于同一个根域名下的不同子域名时,可以通过设置 document.domain
来实现一定程度的跨域资源共享。此处首页和详情页都设置了document.domain
为相同的127.0.0.1
,绕开了同源策略。
总结
-
JSONP:是一种利用了
<script>
标签不受同源策略限制的特点来请求数据的技术。前端定义 callback 的函数体并将此作为参数传给后端,后端返回 callback 字符串形式的函数调用,并将要响应的数据作为 callback 的实参,前端会触发全局的 callback 函数,从而让 callback函数 以参数形式接收到后端响应的数据。缺点是只能支持 GET 请求,并且存在一定的安全风险。 -
cors: 服务器通过设置响应头
Access-Control-Allow-Origin
和Access-Control-Allow-Methods
来指定允许访问的源和方法。需要服务器端的支持和配置 -
代理和nginx:客户端在同源的服务器上设置一个代理,该代理负责转发请求到目标服务器并转发目标服务器的响应,客户端只与同源的代理服务器通信,因此不会触发同源策略。需要额外的服务器资源来运行代理服务
-
WebSocket:WebSocket 提供了一个全双工的通信通道,一旦建立连接,就不再受同源策略限制。通过 WebSocket 对象和服务器它们自带的
send
、onMessage
或on
等方法进行消息的发送和接收。需要服务器端支持 WebSocket 协议。 -
postMessage:通常用于
iframe
和父窗口之间的通信,父窗口获取iframe
的 Window对象,通过 postMessage API 向iframe
发送数据,而iframe 通过 onmessage 监听或接收消息。反之iframe
也可以通过 postMessage 向父窗口发送消息。 -
document.domain:当两个页面的根域名相同时,可以通过设置
document.domain
为共同的父域名来实现跨子域的通信,因为这两个页面会被视为同源