当面试官让我们聊一聊某些东西时,我们首先要想到对话的方式,一般都遵循
- xxx是什么?
- xxx特性 (优缺点)?
- xxx的应用场景?
这三点来进行回答,那么接下来我们来聊一聊跨域
跨域是什么?
跨域问题主要出现在前端开发中,特别是涉及到前后端分离的项目。当一个网页的JavaScript代码试图通过Ajax等方式访问不同源(包括协议、域名、端口号之一不同)的API接口时,就会遇到跨域问题,这就是浏览器的同源策略。浏览器为了安全性考虑,默认阻止了这种跨域请求,除非服务器明确允许。
一、同源策略:跨域问题的根源
什么是同源策略?
同源策略(Same-Origin Policy)要求以下三要素完全一致:
- 协议(如 HTTP/HTTPS)
- 域名(如
example.com
) - 端口(如 80/443)
同源策略(Same-Origin Policy)是一种重要的安全机制,用于控制不同源(origin)之间的交互。这里的“源”由协议、域名和端口号三部分组成。当两个URL的这三个部分完全相同时,则它们属于同一源;只要有任何一部分不同,它们就被认为是不同源。
http://www.example.com:80/a.html
与https://www.example.com/b.html
不同源(协议不同)http://shop.example.com
与http://pay.example.com
不同源(子域名不同)
## 跨域的特性与矛盾
安全性保障
- 数据安全:防止恶意网站窃取用户敏感数据
- 服务器防护:减少 XSS、CSRF 等攻击风险
- 资源隔离:避免第三方脚本随意操作 DOM
开发痛点
- 前后端分离架构下,本地开发常需处理跨域
- 第三方 API 集成时需协调跨域策略
- 多子域系统(如微服务)需跨域通信
解决跨域问题的七种方案
一. jsonp
这是一个比较取巧的方法,利用script 标签src属性不受同源策略限制的特性,来发送请求
实现原理:
利用 <script>
标签不受同源策略限制的特性:
- 前端携带一个参数 callback 给到后端
- 后端将数据作为 callback 函数的实参,返回给前端一个 callback 的调用形式
- 浏览器接收到callback的调用会自动执行全局的callback函数
特性:
- 必须要前后端配合
- 只能发送 get 请求
- 不安全,容易受到xss攻击
代码示例:
前端
<!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>
<button onclick="handle()">请求</button>
<script>
function jsonp(url, cb) {
return new Promise((resolve, reject) => {
const script = document.createElement('script')
window[cb] = function (data) {
// console.log(data) // 后端返回的数据
resolve(data)
}
script.src = `${url}?cb=${cb}`
document.body.appendChild(script)
// callback('hello world')
})
}
function handle() {
jsonp('http://localhost:3000', 'callback').then(res => {
console.log(res)
})
}
</script>
</body>
</html>
后端
const http = require('http');
http.createServer((req, res) => {
const query = new URL(req.url, `http://${req.headers.host}`).searchParams
// console.log(query.get('cb'));
if (query.get('cb')) {
const cb = query.get('cb') // 'callback'
const data = 'hello world'
const result = `${cb}("${data}")` // "callback('hello world')"
res.end(result)
}
// res.end('hello world')
}).listen(3000);
二. CORS
这是我们个人写项目最常用的解决跨域问题的方案
实现原理
后端设置 Access-Control-Allow-Origin: '域名白名单',来通知浏览器哪些域名可以跨域访问
代码实例:
前端
<!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>
<button onclick="handle()">请求</button>
<script>
function handle() {
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://localhost:3000", true);
xhr.send();
xhr.readystatechange = function () {
if (xhr.readyState === 4 && xhr.status === 200) {
console.log(xhr.responseText);
}
};
}
</script>
</body>
</html>
后端
const http = require('http')
const server = http.createServer((req, res) => {
res.writeHead(200, {
'Access-Control-Allow-Origin': 'http://192.168.1.1:5500/', // 允许所有域名跨域
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, PATCH, OPTIONS', // 允许的请求方式
'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-Requested-With' // 允许的请求头
})
res.end('hello world')
})
server.listen(3000)
三. nginx 反向代理
Nginx 反向代理是一种服务器配置技术,它允许一台服务器作为另一台服务器的中介,接收来自客户端的请求,并将这些请求转发给后端服务器。在企业项目中,一般用这种方式解决跨域问题。
实现原理
前端服务器和后端服务器不在同一个域名下,前端服务器通过 nginx 反向代理来访问后端服务器
如图所示
- 客户端请求:用户尝试访问某个网站或服务时,其请求首先到达Nginx反向代理服务器。
- 请求转发:Nginx根据配置规则,决定将这个请求转发到哪个后端服务器。
- 处理请求:后端服务器接收到请求后进行处理,并将响应返回给Nginx反向代理服务器。
- 响应客户端:Nginx接收到后端服务器的响应后,再将该响应返回给原始的客户端。
四. node 中间件代理
实现原理
前端服务器和后端服务器不在同一个域名下,前端服务器通过 node 中间件来访问后端服务器
与nginx 反向代理大差不差,只是换了一种语言
五. websocket
传统的前后端通信是基于http协议的, 是单向的, 只能从一端发到另一端, 无法双向通信。为了解决这个问题,开发者编写了websocket方法
websocket的特点
- websocket 是基于tcp协议的, 是双向的, 可以从一端发送到另一端, 也可以从另一端发送到一端
- socket 协议一旦建立链接, 就可以一直保持通信状态, 不需要每次都建立链接
- 天生就可以跨域
代码示例:
前端
<!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>
function WebSocketTest(url, params = {}) {
return new Promise((resolve, reject) => {
const socket = new WebSocket(url)
socket.onopen = () => {
socket.send(JSON.stringify(params))
}
socket.onmessage = (event) => {
resolve(event.data)
}
})
}
WebSocketTest('ws://localhost:3000', {age: 18}).then(res => {
console.log(res)
})
</script>
</body>
</html>
后端
const WebSocket = require('ws');
// 在 3000 端口上建立 WebSocket 伺服器 (随时都在线的服务)
const ws = new WebSocket.Server({ port: 3000 });
let count = 0
ws.on('connection', (obj) => {
// console.log(obj);
obj.on('message', (msg) => { // 收到客户端发来的消息
// console.log(msg.toString());
obj.send('收到了')
setInterval(() => {
count++
obj.send(count)
}, 2000)
})
})
六. postMessage
前面我们提到的跨域问题,都是产生在前端与后端传输数据时。其实,前端与前端也可以传输数据,并且也会有跨域问题,这里就要提到 iframe 标签了,它可以在一个前端页面中嵌套另一个页面,这两个页面进行数据传输时也会有跨域问题。
让我们用一个例子来说明:
父页面
<!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>
<h2>首页</h2>
<iframe id="frame" src="http://127.0.0.1:5500/%E8%B7%A8%E5%9F%9F/postMessage/detail.html" frameborder="0" width="800" height="500"></iframe>
<script>
let obj = {name: '阿杰', age: 18}
document.getElementById('frame').onload = function () {
this.contentWindow.postMessage(obj, 'http://127.0.0.1:5500') // 向iframe发送消息
window.onmessage = function (e) { // 接收iframe发送的消息
console.log(e.data);
}
}
</script>
</body>
</html>
子页面
<!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>
<h3>详情页 --<span id="title"></span></h3>
<script>
let title = document.getElementById('title')
window.onmessage = function(e){
console.log(e.data);
title.innerHTML = e.data.age
e.source.postMessage('阿杰 20了', e.origin) // 向父级页面发送信息
}
</script>
</body>
</html>
通信流程示意图
父页面 子页面
| |
|-- postMessage(data) ------>|
| |
|<----- responseMessage -----|
| |
七. document.domain
document.domain
允许将页面的域名设置为当前域或其父域,从而实现跨子域通信。与上述方法一致,但谷歌已经禁用此方法
方法对比
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
JSONP | 兼容老旧浏览器 | 无需服务端改动 | 仅 GET,安全性低 |
CORS | 现代 Web 应用 | 标准化,支持所有方法 | 需服务端配合 |
Nginx 代理 | 生产环境部署 | 高性能,零侵入 | 需运维知识 |
WebSocket | 实时通信场景 | 全双工,低延迟 | 协议升级成本高 |
postMessage | 跨窗口通信 | 安全可控 | 仅限窗口间通信 |
这些就是我们解决跨域问题的七种方法,看到这里,你对跨域问题应该有了更深的理解。