什么是跨域
在Web开发中,跨域问题是一个常见的安全限制。让我们先理解什么是同源策略和跨域:
javascript
http://192.168.31.45:8080/user
// 组成:协议号://域名:端口号/路径
同源策略:只有当协议号、域名和端口号都相同的地址,浏览器才认为是同源的。这是浏览器的一种安全机制,防止恶意网站窃取数据。
跨域:当请求的资源与当前页面的源不同时,后端返回给浏览器的数据会被浏览器的同源策略拦截下来,导致前端无法获取响应数据。
开发阶段的跨域解决方案
1. JSONP解决方案
JSONP(JSON with Padding)是一种利用<script>
标签实现跨域请求的技术。
原理:
- 借助script标签的src属性不受同源策略限制的特性
- 通过动态创建script标签发送请求
- 后端返回一个函数调用,前端预先定义该函数处理数据
前端实现:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>JSONP示例</title>
</head>
<body>
<button id="btn">获取数据</button>
<script>
function jsonp(url, cb) {
return new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = `${url}?cb=${cb}`; // 添加回调函数名参数
document.body.appendChild(script);
// 全局定义回调函数
window[cb] = (data) => {
resolve(data);
document.body.removeChild(script);
delete window[cb];
}
});
}
document.getElementById('btn').addEventListener('click', () => {
jsonp('http://localhost:3000', 'callback')
.then((res) => {
console.log('后端返回结果:', res);
});
});
</script>
</body>
</html>
后端实现:
javascript
const koa = require('koa');
const app = new koa();
app.use((ctx) => {
const cb = ctx.query.cb; // 获取前端传递的回调函数名
const data = '给前端的数据';
ctx.body = `${cb}('${data}')`; // 返回函数调用字符串
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
JSONP的优缺点:
- 优点:
- 兼容性好,支持老式浏览器
- 实现简单
- 缺点:
- 只能发送GET请求
- 需要后端配合特定格式返回
- 安全性较低,容易受到XSS攻击
JSONP 工作原理详解
JSONP(JSON with Padding)是一种巧妙利用浏览器特性的跨域解决方案,其核心原理基于以下几个关键点:
首先,浏览器对<script>
标签的src属性加载资源时不受同源策略限制。当我们在页面中动态创建<script>
元素并设置其src属性时,浏览器会向目标URL发起GET请求,这与直接使用AJAX请求不同,不会触发跨域限制。
其次,JSONP利用了JavaScript的函数调用机制。前端在发起请求前,会在全局作用域(通常是window对象)上预先定义一个唯一的回调函数。这个函数名通过查询参数(通常命名为callback或cb)传递给后端,例如:http://api.example.com/data?callback=handleResponse
。
后端接收到请求后,会进行特殊处理:不是返回普通的JSON数据,而是返回一段JavaScript代码,这段代码是对前端预定义回调函数的调用,并将数据作为参数传入。例如返回的内容可能是:handleResponse({"data": "value"})
。
当浏览器接收到这个响应时,由于是通过<script>
标签加载的内容,会立即执行这段JavaScript代码,从而触发预先定义的回调函数,实现了数据的传递。整个过程完全绕过了浏览器的同源策略限制,因为从浏览器的角度看,这只是一个普通的脚本加载过程。
2. CORS跨域资源共享
CORS(Cross-Origin Resource Sharing)是现代浏览器支持的标准跨域解决方案。
原理:
- 后端通过设置响应头告诉浏览器允许跨域请求
- 浏览器根据响应头决定是否允许前端获取响应
前端实现:
html
<!DOCTYPE html>
<html>
<head>
<title>CORS示例</title>
</head>
<body>
<button id="btn">获取数据</button>
<script>
document.getElementById('btn').addEventListener('click', () => {
fetch('http://localhost:3000')
.then(res => res.json())
.then(data => console.log(data))
.catch(err => console.error(err));
});
</script>
</body>
</html>
后端实现:
javascript
const http = require('http');
const server = http.createServer((req, res) => {
// 设置CORS响应头
res.setHeader('Access-Control-Allow-Origin', 'http://127.0.0.1:5500');
res.setHeader('Access-Control-Allow-Methods', 'GET,POST,OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
const data = { msg: 'Hello CORS' };
res.end(JSON.stringify(data));
});
server.listen(3000, () => {
console.log('Server is running on port 3000');
});
CORS的注意事项:
-
简单请求与非简单请求:
- 简单请求:GET/HEAD/POST,且Content-Type为text/plain、multipart/form-data或application/x-www-form-urlencoded
- 非简单请求会先发送OPTIONS预检请求
-
常用响应头:
Access-Control-Allow-Origin
: 允许的源Access-Control-Allow-Methods
: 允许的HTTP方法Access-Control-Allow-Headers
: 允许的请求头Access-Control-Allow-Credentials
: 是否允许发送Cookie
CORS 工作机制深入分析
跨域资源共享(CORS)是现代浏览器提供的标准化跨域解决方案,其工作原理可以分为几个层次:
浏览器在发送跨域请求时,会根据请求类型自动添加Origin
头,标明请求来源。对于简单请求(如GET/HEAD/POST且使用特定Content-Type),浏览器会直接发送请求,但会检查响应中的Access-Control-Allow-Origin
头。如果该头不包含当前源或通配符,浏览器会阻止前端JavaScript访问响应内容。
对于非简单请求(如PUT/DELETE方法或使用自定义头),浏览器会先发送一个OPTIONS预检请求。这个预检请求包含:
Access-Control-Request-Method
:声明实际请求将使用的方法Access-Control-Request-Headers
:声明实际请求将携带的自定义头Origin
:请求来源
服务器需要正确响应这个预检请求,在响应中包含:
Access-Control-Allow-Origin
:允许的源Access-Control-Allow-Methods
:允许的方法Access-Control-Allow-Headers
:允许的头Access-Control-Max-Age
:预检响应缓存时间
只有预检请求通过后,浏览器才会发送实际请求。整个过程由浏览器自动处理,对开发者透明,但要求后端必须正确配置CORS头。
3. Node代理解决方案
在开发环境下,我们可以利用构建工具提供的代理功能解决跨域问题。
Vite配置示例:
javascript
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
'/api': {
target: 'http://localhost:3000', // 后端地址
changeOrigin: true, // 修改请求头中的host为目标地址
rewrite: (path) => path.replace(/^\/api/, '') // 重写路径
}
}
}
});
前端调用:
javascript
// 实际请求的是 /api,但会被代理到 http://localhost:3000
fetch('/api/data')
.then(res => res.json())
.then(data => console.log(data));
后端实现:
javascript
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/data') {
const data = { msg: 'Hello Node Proxy' };
res.end(JSON.stringify(data));
}
});
server.listen(3000, () => {
console.log('Server is running on port 3000');
});
代理工作原理:
- 前端请求开发服务器的
/api
路径 - 开发服务器(Node)作为中间代理将请求转发到目标服务器
- 目标服务器返回响应给开发服务器
- 开发服务器将响应返回给前端
优点:
- 前端代码无需修改,保持生产环境相同的请求路径
- 只在开发环境使用,不影响生产环境配置
- 支持所有HTTP方法
Node代理的核心原理
Node代理解决跨域问题的本质是"中间人"模式,其工作原理如下:
在开发环境下,前端应用通常运行在开发服务器(如Vite/Webpack Dev Server)上,这些工具内置了Node.js代理功能。当前端代码发起向/api/xxx
的请求时,请求首先被发送到开发服务器。
开发服务器收到请求后,会根据配置的代理规则,将请求转发到实际的后端服务器。这个转发过程发生在服务器之间,而服务器间的HTTP通信不受浏览器同源策略的限制。代理服务器会:
- 修改请求头中的Host为目标服务器地址
- 可选地重写请求路径(如去掉/api前缀)
- 转发请求到目标服务器
目标服务器处理请求后,将响应返回给代理服务器,代理服务器再原样返回给前端浏览器。对浏览器而言,它只与同源的开发服务器通信,完全感知不到背后的跨域请求,从而完美规避了同源策略限制。
这种方案的另一个优势是保持开发环境和生产环境的API路径一致。在生产环境,可以通过Nginx等Web服务器实现相同的代理功能,而前端代码无需任何修改。
小结
本文介绍了三种开发阶段常用的跨域解决方案:
- JSONP:利用script标签实现跨域,兼容性好但功能有限
- CORS:通过HTTP头实现跨域,是现代Web应用的首选方案
- Node代理:开发环境下通过中间层转发请求,无缝对接前端开发
下一篇文章我们将继续探讨nginx代理、domain修改和postMessage这三种跨域解决方案,以及它们在生产环境中的应用场景和实现方式。