pin_drop当前位置:知识文库 ❯ 图文
后端通信:16. 跨域请求与CORS - 从入门到实践详解
教程简介
跨域请求是前端开发中最常见的问题之一。由于浏览器的同源策略(Same-Origin Policy)限制,不同源之间的HTTP请求默认会被浏览器拦截。CORS(Cross-Origin Resource Sharing,跨源资源共享)是W3C标准中解决跨域问题的官方方案,它通过一组HTTP头部字段,允许服务器声明哪些源站可以通过浏览器访问其资源。
本教程将深入讲解同源策略的原理、CORS的工作机制、预检请求流程、各种跨域解决方案的对比,以及如何在前后端正确配置CORS,帮助你彻底理解和解决跨域问题。
核心概念
同源策略(Same-Origin Policy)
同源策略是浏览器最核心的安全机制,它限制了一个源的文档或脚本如何与另一个源的资源进行交互。
同源的定义:协议(Protocol)、域名(Host)、端口(Port)三者完全相同。
同源策略的限制范围
CORS工作流程
代码示例
简单请求流程:
浏览器 请求 + Origin头 > 服务器
浏览器 < 响应 + Access-Control-*头 服务器
预检请求流程:
浏览器 OPTIONS预检请求 > 服务器
浏览器 < 预检响应(允许的源/方法/头) 服务器
浏览器 实际请求 > 服务器
浏览器 < 实际响应 服务器简单请求 vs 预检请求
简单请求条件(必须同时满足):
不满足简单请求条件的请求会触发预检请求(Preflight Request)。
CORS相关HTTP头部
语法与用法
Fetch API跨域请求
代码示例
// 简单跨域GET请求
fetch('https://api.example.com/data', {
method: 'GET',
mode: 'cors', // 跨域模式
credentials: 'include', // 携带Cookie
headers: {
'Accept': 'application/json'
}
});
// 需要预检的跨域POST请求
fetch('https://api.example.com/data', {
method: 'POST',
mode: 'cors',
credentials: 'include',
headers: {
'Content-Type': 'application/json', // 触发预检
'X-Custom-Header': 'value' // 自定义头触发预检
},
body: JSON.stringify({ name: 'test' })
});XMLHttpRequest跨域请求
代码示例
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://api.example.com/data', true);
xhr.withCredentials = true; // 携带Cookie
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.onload = function() {
console.log(xhr.responseText);
};
xhr.send(JSON.stringify({ name: 'test' }));CORS模式说明
代码示例
示例1:CORS原理演示
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CORS原理演示</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #1a1a2e;
color: #e0e0e0;
min-height: 100vh;
padding: 30px 20px;
}
.container { max-width: 900px; margin: 0 auto; }
h1 { text-align: center; color: #48dbfb; margin-bottom: 30px; }
.card {
background: #16213e;
border: 1px solid #2d2d44;
border-radius: 12px;
padding: 24px;
margin-bottom: 20px;
}
.card h2 { font-size: 16px; color: #feca57; margin-bottom: 16px; }
.flow-diagram {
background: #0a0a1a;
border-radius: 8px;
padding: 20px;
font-family: monospace;
font-size: 13px;
line-height: 1.8;
overflow-x: auto;
}
.flow-browser { color: #48dbfb; }
.flow-server { color: #2ecc71; }
.flow-header { color: #feca57; }
.flow-error { color: #ff6b6b; }
.request-tester {
margin-top: 16px;
}
.url-row {
display: flex;
gap: 10px;
margin-bottom: 12px;
}
.url-row input, .url-row select {
padding: 10px 12px;
background: #0f3460;
border: 1px solid #2d2d44;
border-radius: 6px;
color: #e0e0e0;
font-size: 14px;
outline: none;
}
.url-row input { flex: 1; }
.url-row select { min-width: 100px; }
.url-row input:focus, .url-row select:focus { border-color: #48dbfb; }
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
}
.btn-primary { background: #48dbfb; color: #0f0f23; }
.btn-primary:hover { background: #0abde3; }
.btn-danger { background: #ff6b6b; color: white; }
.btn-danger:hover { background: #ee5a5a; }
.btn-success { background: #2ecc71; color: white; }
.btn-success:hover { background: #27ae60; }
.result-panel {
background: #0a0a1a;
border-radius: 8px;
padding: 16px;
margin-top: 12px;
font-family: monospace;
font-size: 13px;
max-height: 300px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
.result-panel.success { border-left: 4px solid #2ecc71; }
.result-panel.error { border-left: 4px solid #ff6b6b; }
.headers-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
margin-top: 12px;
}
.headers-table th, .headers-table td {
padding: 8px 12px;
border: 1px solid #2d2d44;
text-align: left;
}
.headers-table th {
background: #0f3460;
color: #48dbfb;
width: 200px;
}
.headers-table td { color: #2ecc71; font-family: monospace; }
.tab-group {
display: flex;
gap: 4px;
margin-bottom: 16px;
}
.tab {
padding: 8px 16px;
background: transparent;
border: 1px solid #2d2d44;
border-radius: 6px;
color: #888;
font-size: 13px;
cursor: pointer;
}
.tab.active {
background: #48dbfb;
color: #0f0f23;
border-color: #48dbfb;
}
</style>
</head>
<body>
<div class="container">
<h1>CORS 跨域请求原理演示</h1>
<!-- 同源策略说明 -->
<div class="card">
<h2>同源策略判断</h2>
<p style="font-size:13px; color:#888; margin-bottom:12px;">
当前页面源: <span style="color:#48dbfb;" id="currentOrigin">-</span>
</p>
<table class="headers-table">
<thead>
<tr>
<th>目标URL</th>
<th>协议</th>
<th>域名</th>
<th>端口</th>
<th>是否同源</th>
</tr>
</thead>
<tbody id="originCheckTable"></tbody>
</table>
</div>
<!-- CORS流程图 -->
<div class="card">
<h2>CORS请求流程</h2>
<div class="tab-group">
<div class="tab active" onclick="showFlow('simple')">简单请求</div>
<div class="tab" onclick="showFlow('preflight')">预检请求</div>
</div>
<div class="flow-diagram" id="flowSimple">
<span class="flow-browser">浏览器</span> <span class="flow-server">服务器</span>
<span class="flow-header">GET /api/data HTTP/1.1</span>
<span class="flow-header">Origin: https://example.com</span>
>
<span class="flow-header">HTTP/1.1 200 OK</span>
<span class="flow-header">Access-Control-Allow-Origin: *</span>
<span class="flow-header">Content-Type: application/json</span>
<
请求成功,读取响应数据
</div>
<div class="flow-diagram" id="flowPreflight" style="display:none;">
<span class="flow-browser">浏览器</span> <span class="flow-server">服务器</span>
<span class="flow-header">OPTIONS /api/data HTTP/1.1</span> ← 预检请求
<span class="flow-header">Origin: https://example.com</span>
<span class="flow-header">Access-Control-Request-Method: PUT</span>
<span class="flow-header">Access-Control-Request-Headers: X-Token</span>
>
<span class="flow-header">HTTP/1.1 204 No Content</span> ← 预检响应
<span class="flow-header">Access-Control-Allow-Origin: *</span>
<span class="flow-header">Access-Control-Allow-Methods: PUT</span>
<span class="flow-header">Access-Control-Allow-Headers: X-Token</span>
<span class="flow-header">Access-Control-Max-Age: 86400</span>
<
<span class="flow-header">PUT /api/data HTTP/1.1</span> ← 实际请求
<span class="flow-header">Origin: https://example.com</span>
<span class="flow-header">X-Token: abc123</span>
>
<span class="flow-header">HTTP/1.1 200 OK</span> ← 实际响应
<span class="flow-header">Access-Control-Allow-Origin: *</span>
<
请求成功
</div>
</div>
<!-- 跨域请求测试 -->
<div class="card">
<h2>跨域请求测试</h2>
<div class="request-tester">
<div class="url-row">
<select id="testMethod">
<option value="GET">GET</option>
<option value="POST">POST</option>
<option value="PUT">PUT</option>
<option value="DELETE">DELETE</option>
<option value="PATCH">PATCH</option>
</select>
<input type="text" id="testUrl" value="https://httpbin.org/get" placeholder="输入请求URL">
</div>
<div class="url-row">
<select id="testContentType">
<option value="">无Content-Type</option>
<option value="application/x-www-form-urlencoded">application/x-www-form-urlencoded</option>
<option value="multipart/form-data">multipart/form-data</option>
<option value="text/plain">text/plain</option>
<option value="application/json" selected>application/json</option>
</select>
<input type="text" id="testCustomHeader" placeholder="自定义头: X-Custom-Header=value">
</div>
<div style="display:flex; gap:8px;">
<button class="btn btn-primary" onclick="sendTestRequest()">发送请求</button>
<button class="btn btn-danger" onclick="sendNoCorsRequest()">no-cors模式</button>
<button class="btn btn-success" onclick="sendProxyRequest()">代理请求</button>
</div>
<div class="result-panel" id="testResult" style="display:none;"></div>
</div>
</div>
<!-- CORS头部详解 -->
<div class="card">
<h2>CORS响应头部详解</h2>
<table class="headers-table">
<thead>
<tr>
<th>响应头</th>
<th>说明</th>
<th>示例值</th>
</tr>
</thead>
<tbody>
<tr>
<td>Access-Control-Allow-Origin</td>
<td style="color:#e0e0e0;">允许的源,*表示任意源</td>
<td>* 或 https://example.com</td>
</tr>
<tr>
<td>Access-Control-Allow-Methods</td>
<td style="color:#e0e0e0;">允许的HTTP方法</td>
<td>GET, POST, PUT, DELETE</td>
</tr>
<tr>
<td>Access-Control-Allow-Headers</td>
<td style="color:#e0e0e0;">允许的请求头</td>
<td>Content-Type, X-Token</td>
</tr>
<tr>
<td>Access-Control-Allow-Credentials</td>
<td style="color:#e0e0e0;">是否允许携带Cookie</td>
<td>true</td>
</tr>
<tr>
<td>Access-Control-Max-Age</td>
<td style="color:#e0e0e0;">预检结果缓存时间(秒)</td>
<td>86400</td>
</tr>
<tr>
<td>Access-Control-Expose-Headers</td>
<td style="color:#e0e0e0;">允许前端读取的响应头</td>
<td>X-Total-Count, X-Request-Id</td>
</tr>
</tbody>
</table>
</div>
</div>
<script>
// 显示当前源
document.getElementById('currentOrigin').textContent = window.location.origin;
// 同源判断表格
const testUrls = [
{ url: window.location.href, label: '当前页面' },
{ url: 'https://httpbin.org/get', label: 'httpbin.org' },
{ url: 'http://httpbin.org/get', label: 'httpbin.org (HTTP)' },
{ url: 'https://api.github.com/users', label: 'api.github.com' },
{ url: 'https://jsonplaceholder.typicode.com/posts', label: 'jsonplaceholder' },
];
const tbody = document.getElementById('originCheckTable');
testUrls.forEach(item => {
try {
const u = new URL(item.url);
const current = new URL(window.location.href);
const isSame = u.protocol === current.protocol &&
u.hostname === current.hostname &&
u.port === current.port;
const row = document.createElement('tr');
row.innerHTML = `
<td style="color:#e0e0e0;">${item.label}</td>
<td>${u.protocol.replace(':', '')}</td>
<td>${u.hostname}</td>
<td>${u.port || (u.protocol === 'https:' ? '443' : '80')}</td>
<td style="color:${isSame ? '#2ecc71' : '#ff6b6b'}; font-weight:600;">${isSame ? '是' : '否'}</td>
`;
tbody.appendChild(row);
} catch (e) {}
});
// 流程图切换
function showFlow(type) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
event.target.classList.add('active');
document.getElementById('flowSimple').style.display = type === 'simple' ? 'block' : 'none';
document.getElementById('flowPreflight').style.display = type === 'preflight' ? 'block' : 'none';
}
// 跨域请求测试
async function sendTestRequest() {
const method = document.getElementById('testMethod').value;
const url = document.getElementById('testUrl').value;
const contentType = document.getElementById('testContentType').value;
const customHeader = document.getElementById('testCustomHeader').value;
const result = document.getElementById('testResult');
result.style.display = 'block';
result.className = 'result-panel';
result.textContent = '发送请求中...\n';
try {
const options = {
method: method,
mode: 'cors',
headers: {}
};
if (contentType) {
options.headers['Content-Type'] = contentType;
}
if (customHeader) {
const [key, value] = customHeader.split('=');
if (key && value) {
options.headers[key.trim()] = value.trim();
}
}
if (['POST', 'PUT', 'PATCH'].includes(method)) {
options.body = JSON.stringify({ test: 'data' });
}
const startTime = performance.now();
const response = await fetch(url, options);
const elapsed = (performance.now() - startTime).toFixed(2);
// 读取响应头
let headers = '响应头:\n';
response.headers.forEach((value, key) => {
headers += ` ${key}: ${value}\n`;
});
const data = await response.text();
result.className = 'result-panel success';
result.textContent =
` 请求成功 (${elapsed}ms)\n\n` +
`状态码: ${response.status} ${response.statusText}\n` +
`类型: ${response.type}\n\n` +
headers + '\n' +
`响应体:\n${data.substring(0, 500)}`;
} catch (error) {
result.className = 'result-panel error';
result.textContent =
` 请求失败\n\n` +
`错误类型: ${error.name}\n` +
`错误信息: ${error.message}\n\n` +
`可能的原因:\n` +
`1. 目标服务器未设置CORS响应头\n` +
`2. 请求被浏览器同源策略拦截\n` +
`3. 网络连接问题\n` +
`4. 服务器不可达`;
}
}
// no-cors模式
async function sendNoCorsRequest() {
const url = document.getElementById('testUrl').value;
const result = document.getElementById('testResult');
result.style.display = 'block';
result.className = 'result-panel';
result.textContent = '发送no-cors请求中...\n';
try {
const response = await fetch(url, {
mode: 'no-cors'
});
result.className = 'result-panel success';
result.textContent =
`no-cors模式响应\n\n` +
`状态码: ${response.status}\n` +
`类型: ${response.type}\n` +
`ok: ${response.ok}\n\n` +
`注意:no-cors模式下:\n` +
`- 响应类型为 "opaque"\n` +
`- 状态码始终为 0\n` +
`- 无法读取响应头和响应体\n` +
`- 只能知道请求已发送,无法获取结果`;
} catch (error) {
result.className = 'result-panel error';
result.textContent = `请求失败: ${error.message}`;
}
}
// 通过代理请求
async function sendProxyRequest() {
const url = document.getElementById('testUrl').value;
const result = document.getElementById('testResult');
result.style.display = 'block';
result.className = 'result-panel';
result.textContent = '通过CORS代理请求中...\n';
try {
// 使用公共CORS代理(仅用于演示,生产环境请自建代理)
const proxyUrl = `https://api.allorigins.win/raw?url=${encodeURIComponent(url)}`;
const response = await fetch(proxyUrl);
const data = await response.text();
result.className = 'result-panel success';
result.textContent =
` 代理请求成功\n\n` +
`代理URL: ${proxyUrl}\n` +
`状态码: ${response.status}\n\n` +
`响应体:\n${data.substring(0, 500)}\n\n` +
`注意:公共CORS代理仅用于开发测试,\n` +
`生产环境应使用自己的代理服务器。`;
} catch (error) {
result.className = 'result-panel error';
result.textContent = `代理请求失败: ${error.message}`;
}
}
</script>
</body>
</html>示例2:各种跨域解决方案对比
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>各种跨域解决方案对比</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #f5f7fa;
min-height: 100vh;
padding: 30px 20px;
}
.container { max-width: 900px; margin: 0 auto; }
h1 { text-align: center; color: #2c3e50; margin-bottom: 30px; }
.solution-card {
background: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 16px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
border-left: 4px solid;
}
.solution-card.cors { border-left-color: #2ecc71; }
.solution-card.jsonp { border-left-color: #3498db; }
.solution-card.proxy { border-left-color: #e67e22; }
.solution-card.postMessage { border-left-color: #9b59b6; }
.solution-card.websocket { border-left-color: #1abc9c; }
.solution-card.nginx { border-left-color: #e74c3c; }
.solution-card h2 {
font-size: 18px;
margin-bottom: 8px;
display: flex;
align-items: center;
gap: 8px;
}
.badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.badge-recommended { background: #d4edda; color: #155724; }
.badge-legacy { background: #fff3cd; color: #856404; }
.badge-advanced { background: #d1ecf1; color: #0c5460; }
.solution-card p {
font-size: 14px;
color: #666;
line-height: 1.8;
margin-bottom: 12px;
}
.code-block {
background: #2c3e50;
color: #2ecc71;
padding: 16px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
overflow-x: auto;
white-space: pre;
margin: 12px 0;
}
.pros-cons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
margin-top: 12px;
}
.pros, .cons {
padding: 12px;
border-radius: 8px;
font-size: 13px;
}
.pros { background: #d4edda; }
.cons { background: #f8d7da; }
.pros h4 { color: #155724; margin-bottom: 6px; }
.cons h4 { color: #721c24; margin-bottom: 6px; }
.pros li, .cons li { margin-left: 16px; margin-bottom: 4px; }
.comparison-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
font-size: 13px;
}
.comparison-table th, .comparison-table td {
padding: 10px 12px;
border: 1px solid #e0e0e0;
text-align: center;
}
.comparison-table th {
background: #2c3e50;
color: white;
}
.comparison-table td { color: #555; }
.check { color: #2ecc71; font-weight: bold; }
.cross { color: #e74c3c; font-weight: bold; }
.partial { color: #f39c12; font-weight: bold; }
</style>
</head>
<body>
<div class="container">
<h1>跨域解决方案全面对比</h1>
<!-- CORS -->
<div class="solution-card cors">
<h2>CORS <span class="badge badge-recommended">推荐方案</span></h2>
<p>
CORS(Cross-Origin Resource Sharing)是W3C标准的跨域解决方案。
通过在服务器端设置Access-Control-*响应头,告诉浏览器允许哪些跨域请求。
这是最现代、最标准的跨域解决方案。
</p>
<div class="code-block">// 前端代码
fetch('https://api.example.com/data', {
method: 'POST',
mode: 'cors',
credentials: 'include',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'test' })
});
// 后端代码(Node.js Express示例)
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://example.com');
res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');
res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization');
res.header('Access-Control-Allow-Credentials', 'true');
res.header('Access-Control-Max-Age', '86400');
// 处理预检请求
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
});</div>
<div class="pros-cons">
<div class="pros">
<h4>优点</h4>
<ul>
<li>W3C标准方案</li>
<li>支持所有HTTP方法</li>
<li>支持发送和接收自定义头</li>
<li>支持携带Cookie</li>
<li>错误处理完善</li>
</ul>
</div>
<div class="cons">
<h4>缺点</h4>
<ul>
<li>需要服务器端配置</li>
<li>兼容IE10+</li>
<li>预检请求增加延迟</li>
<li>某些旧代理可能剥离CORS头</li>
</ul>
</div>
</div>
</div>
<!-- JSONP -->
<div class="solution-card jsonp">
<h2>JSONP <span class="badge badge-legacy">传统方案</span></h2>
<p>
JSONP(JSON with Padding)利用script标签不受同源策略限制的特性,
通过动态创建script标签加载跨域数据。只支持GET请求。
</p>
<div class="code-block">// 前端代码
function jsonp(url, callbackName) {
return new Promise((resolve, reject) => {
// 定义回调函数
window[callbackName] = function(data) {
resolve(data);
delete window[callbackName];
document.head.removeChild(script);
};
// 创建script标签
const script = document.createElement('script');
script.src = `${url}?callback=${callbackName}`;
script.onerror = () => reject(new Error('JSONP请求失败'));
document.head.appendChild(script);
});
}
// 使用
jsonp('https://api.example.com/data', 'handleData')
.then(data => console.log(data));
// 后端返回格式
// handleData({"name": "zhangsan", "age": 25});</div>
<div class="pros-cons">
<div class="pros">
<h4>优点</h4>
<ul>
<li>兼容性好(支持IE8+)</li>
<li>实现简单</li>
<li>不需要服务器特殊配置</li>
</ul>
</div>
<div class="cons">
<h4>缺点</h4>
<ul>
<li>只支持GET请求</li>
<li>存在安全风险(XSS)</li>
<li>无法获取HTTP状态码</li>
<li>无法设置请求头</li>
<li>错误处理困难</li>
</ul>
</div>
</div>
</div>
<!-- 代理服务器 -->
<div class="solution-card proxy">
<h2>代理服务器 <span class="badge badge-advanced">开发常用</span></h2>
<p>
通过同源的代理服务器转发请求,浏览器只与同源代理通信,
代理服务器再与目标服务器通信,绕过浏览器同源策略限制。
</p>
<div class="code-block">// 开发环境 - Vite配置代理
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
};
// 开发环境 - webpack devServer配置
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
}
};
// 生产环境 - Node.js代理
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
app.use('/api', createProxyMiddleware({
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}));</div>
<div class="pros-cons">
<div class="pros">
<h4>优点</h4>
<ul>
<li>前端无需修改</li>
<li>支持所有HTTP方法</li>
<li>可添加额外处理逻辑</li>
<li>开发环境配置简单</li>
</ul>
</div>
<div class="cons">
<h4>缺点</h4>
<ul>
<li>增加服务器负担</li>
<li>多一层网络延迟</li>
<li>生产环境需额外部署</li>
<li>可能成为性能瓶颈</li>
</ul>
</div>
</div>
</div>
<!-- postMessage -->
<div class="solution-card postMessage">
<h2>postMessage <span class="badge badge-advanced">iframe通信</span></h2>
<p>
window.postMessage方法可以安全地实现跨源通信,
主要用于不同源的两个窗口(如主页面和iframe)之间的数据传递。
</p>
<div class="code-block">// 主页面(发送消息到iframe)
const iframe = document.getElementById('myIframe');
iframe.contentWindow.postMessage(
{ type: 'REQUEST_DATA', payload: { id: 123 } },
'https://other-domain.com' // 目标源
);
// 主页面(接收iframe的回复)
window.addEventListener('message', (event) => {
// 验证消息来源
if (event.origin !== 'https://other-domain.com') return;
console.log('收到消息:', event.data);
});
// iframe页面(接收消息并回复)
window.addEventListener('message', (event) => {
if (event.origin !== 'https://main-domain.com') return;
if (event.data.type === 'REQUEST_DATA') {
// 处理请求
event.source.postMessage(
{ type: 'RESPONSE_DATA', payload: { result: 'success' } },
event.origin
);
}
});</div>
<div class="pros-cons">
<div class="pros">
<h4>优点</h4>
<ul>
<li>安全可控</li>
<li>双向通信</li>
<li>支持复杂数据</li>
<li>兼容性好</li>
</ul>
</div>
<div class="cons">
<h4>缺点</h4>
<ul>
<li>仅限窗口间通信</li>
<li>需要iframe配合</li>
<li>需验证消息来源</li>
<li>不适合API请求</li>
</ul>
</div>
</div>
</div>
<!-- Nginx反向代理 -->
<div class="solution-card nginx">
<h2>Nginx反向代理 <span class="badge badge-recommended">生产推荐</span></h2>
<p>
使用Nginx作为反向代理,将API请求转发到后端服务器,
同时可以统一域名和端口,从根本上解决跨域问题。
</p>
<div class="code-block"># Nginx配置
server {
listen 80;
server_name example.com;
# 静态资源
location / {
root /var/www/html;
try_files $uri $uri/ /index.html;
}
# API代理
location /api/ {
proxy_pass https://api-server:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# 或者:同域部署 + CORS头
location /api/ {
proxy_pass https://api-server:8080/;
add_header Access-Control-Allow-Origin *;
add_header Access-Control-Allow-Methods "GET,POST,PUT,DELETE,OPTIONS";
add_header Access-Control-Allow-Headers "Content-Type,Authorization";
if ($request_method = OPTIONS) {
return 204;
}
}
}</div>
<div class="pros-cons">
<div class="pros">
<h4>优点</h4>
<ul>
<li>前端完全无感知</li>
<li>性能好</li>
<li>可负载均衡</li>
<li>可缓存静态资源</li>
</ul>
</div>
<div class="cons">
<h4>缺点</h4>
<ul>
<li>需要运维配置</li>
<li>增加架构复杂度</li>
<li>调试相对困难</li>
</ul>
</div>
</div>
</div>
<!-- 综合对比表 -->
<div class="solution-card" style="border-left-color:#2c3e50;">
<h2>方案综合对比</h2>
<table class="comparison-table">
<thead>
<tr>
<th>方案</th>
<th>GET</th>
<th>POST</th>
<th>PUT/DELETE</th>
<th>Cookie</th>
<th>自定义头</th>
<th>需要后端</th>
<th>推荐度</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>CORS</strong></td>
<td class="check">✓</td>
<td class="check">✓</td>
<td class="check">✓</td>
<td class="check">✓</td>
<td class="check">✓</td>
<td class="check">✓</td>
<td class="check">首选</td>
</tr>
<tr>
<td><strong>JSONP</strong></td>
<td class="check">✓</td>
<td class="cross">✗</td>
<td class="cross">✗</td>
<td class="cross">✗</td>
<td class="cross">✗</td>
<td class="partial">部分</td>
<td class="cross">过时</td>
</tr>
<tr>
<td><strong>代理服务器</strong></td>
<td class="check">✓</td>
<td class="check">✓</td>
<td class="check">✓</td>
<td class="check">✓</td>
<td class="check">✓</td>
<td class="check">✓</td>
<td class="check">推荐</td>
</tr>
<tr>
<td><strong>postMessage</strong></td>
<td class="partial">特殊</td>
<td class="partial">特殊</td>
<td class="cross">✗</td>
<td class="cross">✗</td>
<td class="cross">✗</td>
<td class="check">✓</td>
<td class="partial">特定场景</td>
</tr>
<tr>
<td><strong>Nginx代理</strong></td>
<td class="check">✓</td>
<td class="check">✓</td>
<td class="check">✓</td>
<td class="check">✓</td>
<td class="check">✓</td>
<td class="check">✓</td>
<td class="check">推荐</td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>示例3:CORS预检请求与缓存
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CORS预检请求与缓存</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #0f0f23;
color: #e0e0e0;
min-height: 100vh;
padding: 30px 20px;
}
.container { max-width: 800px; margin: 0 auto; }
h1 { text-align: center; color: #48dbfb; margin-bottom: 30px; }
.card {
background: #1a1a2e;
border: 1px solid #2d2d44;
border-radius: 12px;
padding: 24px;
margin-bottom: 20px;
}
.card h2 { font-size: 16px; color: #feca57; margin-bottom: 16px; }
.scenario {
background: #0a0a1a;
border-radius: 8px;
padding: 16px;
margin-bottom: 12px;
}
.scenario h3 {
font-size: 14px;
margin-bottom: 8px;
}
.scenario .result {
font-size: 13px;
padding: 8px 12px;
border-radius: 6px;
margin-top: 8px;
}
.result.simple { background: #0a2e1a; color: #2ecc71; border-left: 3px solid #2ecc71; }
.result.preflight { background: #2e1a0a; color: #feca57; border-left: 3px solid #feca57; }
.request-log {
background: #0a0a1a;
border-radius: 8px;
padding: 16px;
font-family: monospace;
font-size: 12px;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
}
.log-time { color: #888; }
.log-method { color: #48dbfb; font-weight: bold; }
.log-url { color: #feca57; }
.log-header { color: #2ecc71; }
.log-status { color: #ff6b6b; font-weight: bold; }
.log-success { color: #2ecc71; font-weight: bold; }
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
margin: 4px;
}
.btn-primary { background: #48dbfb; color: #0f0f23; }
.btn-primary:hover { background: #0abde3; }
.btn-warning { background: #feca57; color: #0f0f23; }
.btn-warning:hover { background: #f0b730; }
.btn-danger { background: #ff6b6b; color: white; }
.btn-danger:hover { background: #ee5a5a; }
.test-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
margin-top: 12px;
}
.test-item {
padding: 12px;
background: #0f3460;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
text-align: center;
}
.test-item:hover { background: #162d50; transform: translateY(-2px); }
.test-item .method { color: #48dbfb; font-weight: 600; font-size: 13px; }
.test-item .desc { color: #888; font-size: 11px; margin-top: 4px; }
.test-item .type { font-size: 10px; margin-top: 6px; padding: 2px 6px; border-radius: 3px; }
.type-simple { background: #0a2e1a; color: #2ecc71; }
.type-preflight { background: #2e1a0a; color: #feca57; }
</style>
</head>
<body>
<div class="container">
<h1>CORS 预检请求与缓存</h1>
<!-- 判断规则 -->
<div class="card">
<h2>简单请求 vs 预检请求 判断</h2>
<div class="scenario">
<h3 style="color:#2ecc71;">简单请求场景</h3>
<div id="simpleScenarios"></div>
</div>
<div class="scenario">
<h3 style="color:#feca57;">预检请求场景</h3>
<div id="preflightScenarios"></div>
</div>
</div>
<!-- 交互式测试 -->
<div class="card">
<h2>交互式请求测试</h2>
<p style="font-size:13px; color:#888; margin-bottom:12px;">
点击下方按钮发送不同类型的请求,观察是否触发预检请求。
</p>
<div class="test-grid">
<div class="test-item" onclick="testRequest('GET', {})">
<div class="method">GET</div>
<div class="desc">无自定义头</div>
<div class="type type-simple">简单请求</div>
</div>
<div class="test-item" onclick="testRequest('POST', {'Content-Type':'application/x-www-form-urlencoded'})">
<div class="method">POST</div>
<div class="desc">x-www-form-urlencoded</div>
<div class="type type-simple">简单请求</div>
</div>
<div class="test-item" onclick="testRequest('POST', {'Content-Type':'application/json'})">
<div class="method">POST</div>
<div class="desc">application/json</div>
<div class="type type-preflight">预检请求</div>
</div>
<div class="test-item" onclick="testRequest('PUT', {})">
<div class="method">PUT</div>
<div class="desc">PUT方法</div>
<div class="type type-preflight">预检请求</div>
</div>
<div class="test-item" onclick="testRequest('DELETE', {})">
<div class="method">DELETE</div>
<div class="desc">DELETE方法</div>
<div class="type type-preflight">预检请求</div>
</div>
<div class="test-item" onclick="testRequest('GET', {'X-Custom-Header':'value'})">
<div class="method">GET</div>
<div class="desc">自定义头</div>
<div class="type type-preflight">预检请求</div>
</div>
<div class="test-item" onclick="testRequest('POST', {'Authorization':'Bearer token'})">
<div class="method">POST</div>
<div class="desc">Authorization头</div>
<div class="type type-preflight">预检请求</div>
</div>
<div class="test-item" onclick="testRequest('PATCH', {})">
<div class="method">PATCH</div>
<div class="desc">PATCH方法</div>
<div class="type type-preflight">预检请求</div>
</div>
</div>
<div style="margin-top:16px; display:flex; gap:8px;">
<button class="btn btn-warning" onclick="clearLog()">清空日志</button>
</div>
<div class="request-log" id="requestLog">等待测试...</div>
</div>
<!-- 预检缓存 -->
<div class="card">
<h2>预检请求缓存</h2>
<p style="font-size:13px; color:#888; line-height:1.8;">
预检请求会增加额外的网络开销。通过 Access-Control-Max-Age 头部,
服务器可以告诉浏览器缓存预检结果的时间。<br>
在缓存有效期内,相同来源的跨域请求不需要再次发送预检请求。<br><br>
各浏览器对预检缓存的最大时间限制不同:<br>
- Chrome: 7200秒(2小时)<br>
- Firefox: 86400秒(24小时)<br>
- Safari: 604800秒(7天)
</p>
<div style="margin-top:12px; display:flex; gap:8px;">
<button class="btn btn-primary" onclick="testPreflightCache()">测试预检缓存</button>
</div>
<div class="request-log" id="cacheLog" style="margin-top:12px;">等待测试...</div>
</div>
</div>
<script>
// 填充场景列表
const simpleScenarios = [
'GET请求,无自定义头',
'HEAD请求,无自定义头',
'POST请求,Content-Type: application/x-www-form-urlencoded',
'POST请求,Content-Type: multipart/form-data',
'POST请求,Content-Type: text/plain',
];
const preflightScenarios = [
'PUT、DELETE、PATCH等非简单方法',
'Content-Type: application/json',
'自定义请求头(如X-Custom-Header)',
'Authorization头',
'请求中使用了ReadableStream',
];
document.getElementById('simpleScenarios').innerHTML = simpleScenarios
.map(s => `<div class="result simple">${s}</div>`).join('');
document.getElementById('preflightScenarios').innerHTML = preflightScenarios
.map(s => `<div class="result preflight">${s}</div>`).join('');
// 请求日志
let logCount = 0;
function addLog(message) {
const log = document.getElementById('requestLog');
const time = new Date().toLocaleTimeString();
logCount++;
log.textContent += `[${time}] #${logCount} ${message}\n`;
log.scrollTop = log.scrollHeight;
}
function clearLog() {
document.getElementById('requestLog').textContent = '日志已清空\n';
logCount = 0;
}
// 判断请求类型
function isSimpleRequest(method, headers) {
// 检查方法
if (!['GET', 'HEAD', 'POST'].includes(method)) return false;
// 检查Content-Type
const contentType = headers['Content-Type'] || '';
const simpleContentTypes = [
'application/x-www-form-urlencoded',
'multipart/form-data',
'text/plain'
];
if (contentType && !simpleContentTypes.includes(contentType)) return false;
// 检查自定义头
const simpleHeaders = ['accept', 'accept-language', 'content-language', 'content-type'];
const customHeaders = Object.keys(headers).filter(
h => !simpleHeaders.includes(h.toLowerCase())
);
if (customHeaders.length > 0) return false;
return true;
}
// 测试请求
async function testRequest(method, headers) {
const url = 'https://httpbin.org/' + method.toLowerCase();
const isSimple = isSimpleRequest(method, headers);
addLog(``);
addLog(`<span class="log-method">${method}</span> <span class="log-url">${url}</span>`);
addLog(`请求类型: ${isSimple ? '简单请求' : '预检请求'}`);
if (!isSimple) {
addLog(`触发预检原因:`);
if (!['GET', 'HEAD', 'POST'].includes(method)) {
addLog(` - 非简单方法: ${method}`);
}
const contentType = headers['Content-Type'];
if (contentType && contentType !== 'application/x-www-form-urlencoded' &&
contentType !== 'multipart/form-data' && contentType !== 'text/plain') {
addLog(` - 非简单Content-Type: ${contentType}`);
}
const customHeaders = Object.keys(headers).filter(
h => !['accept', 'accept-language', 'content-language', 'content-type'].includes(h.toLowerCase())
);
if (customHeaders.length > 0) {
addLog(` - 自定义头: ${customHeaders.join(', ')}`);
}
}
addLog(`请求头: ${JSON.stringify(headers)}`);
try {
const startTime = performance.now();
const options = { method, mode: 'cors', headers };
if (['POST', 'PUT', 'PATCH'].includes(method)) {
options.body = JSON.stringify({ test: 'data' });
}
const response = await fetch(url, options);
const elapsed = (performance.now() - startTime).toFixed(2);
addLog(`<span class="log-success"> 响应: ${response.status} (${elapsed}ms)</span>`);
// 读取CORS相关响应头
const corsHeaders = [
'access-control-allow-origin',
'access-control-allow-methods',
'access-control-allow-headers',
'access-control-allow-credentials',
'access-control-max-age'
];
corsHeaders.forEach(h => {
const value = response.headers.get(h);
if (value) {
addLog(` <span class="log-header">${h}: ${value}</span>`);
}
});
} catch (error) {
addLog(`<span class="log-status"> 失败: ${error.message}</span>`);
}
addLog('');
}
// 测试预检缓存
async function testPreflightCache() {
const cacheLog = document.getElementById('cacheLog');
cacheLog.textContent = '测试预检缓存...\n\n';
const url = 'https://httpbin.org/put';
const headers = { 'Content-Type': 'application/json', 'X-Custom': 'test' };
// 第一次请求(可能触发预检)
cacheLog.textContent += '第1次请求(可能触发预检):\n';
const start1 = performance.now();
try {
await fetch(url, { method: 'PUT', mode: 'cors', headers, body: '{}' });
cacheLog.textContent += ` 耗时: ${(performance.now() - start1).toFixed(0)}ms\n`;
} catch (e) {
cacheLog.textContent += ` 失败: ${e.message}\n`;
}
// 短暂延迟后第二次请求
await new Promise(r => setTimeout(r, 500));
cacheLog.textContent += '\n第2次请求(预检可能已缓存):\n';
const start2 = performance.now();
try {
await fetch(url, { method: 'PUT', mode: 'cors', headers, body: '{}' });
cacheLog.textContent += ` 耗时: ${(performance.now() - start2).toFixed(0)}ms\n`;
} catch (e) {
cacheLog.textContent += ` 失败: ${e.message}\n`;
}
cacheLog.textContent += '\n注意:如果第2次请求更快,说明预检结果被缓存了。\n';
cacheLog.textContent += '缓存时间由服务器的 Access-Control-Max-Age 头决定。';
}
</script>
</body>
</html>示例4:CORS与Cookie凭证
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CORS与Cookie凭证</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #fafafa;
min-height: 100vh;
padding: 30px 20px;
}
.container { max-width: 800px; margin: 0 auto; }
h1 { text-align: center; color: #2c3e50; margin-bottom: 30px; }
.card {
background: white;
border-radius: 12px;
padding: 24px;
margin-bottom: 20px;
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}
.card h2 { font-size: 16px; color: #2c3e50; margin-bottom: 16px; }
.warning-box {
background: #fff3cd;
border-left: 4px solid #ffc107;
padding: 12px 16px;
border-radius: 4px;
margin-bottom: 16px;
font-size: 13px;
color: #856404;
line-height: 1.8;
}
.info-box {
background: #d1ecf1;
border-left: 4px solid #17a2b8;
padding: 12px 16px;
border-radius: 4px;
margin-bottom: 16px;
font-size: 13px;
color: #0c5460;
line-height: 1.8;
}
.code-block {
background: #2c3e50;
color: #2ecc71;
padding: 16px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
overflow-x: auto;
white-space: pre;
margin: 12px 0;
}
.config-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
margin: 12px 0;
}
.config-table th, .config-table td {
padding: 10px 12px;
border: 1px solid #e0e0e0;
text-align: left;
}
.config-table th {
background: #2c3e50;
color: white;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
margin: 4px;
}
.btn-primary { background: #3498db; color: white; }
.btn-primary:hover { background: #2980b9; }
.result-area {
background: #2c3e50;
color: #2ecc71;
padding: 16px;
border-radius: 8px;
font-family: monospace;
font-size: 13px;
white-space: pre-wrap;
word-break: break-all;
max-height: 200px;
overflow-y: auto;
margin-top: 12px;
}
</style>
</head>
<body>
<div class="container">
<h1>CORS 与 Cookie 凭证</h1>
<div class="card">
<h2>CORS携带凭证的三要素</h2>
<div class="warning-box">
<strong>重要:</strong>跨域请求携带Cookie需要前后端同时配置,缺一不可!
</div>
<table class="config-table">
<thead>
<tr>
<th>要素</th>
<th>前端配置</th>
<th>后端配置</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>1. 凭证标志</strong></td>
<td>credentials: 'include'</td>
<td>Access-Control-Allow-Credentials: true</td>
</tr>
<tr>
<td><strong>2. 允许源</strong></td>
<td>-</td>
<td>Access-Control-Allow-Origin 必须是具体域名,不能是 *</td>
</tr>
<tr>
<td><strong>3. Cookie属性</strong></td>
<td>-</td>
<td>Set-Cookie: SameSite=None; Secure</td>
</tr>
</tbody>
</table>
<div class="code-block">// ===== 前端代码 =====
// Fetch API
fetch('https://api.example.com/data', {
method: 'POST',
mode: 'cors',
credentials: 'include', // 关键:携带Cookie
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'test' })
});
// XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://api.example.com/data', true);
xhr.withCredentials = true; // 关键:携带Cookie
xhr.send();
// ===== 后端代码(Node.js) =====
// 错误配置 - 携带凭证时不能使用 *
res.header('Access-Control-Allow-Origin', '*'); // 浏览器会拒绝
res.header('Access-Control-Allow-Credentials', 'true');
// 正确配置 - 必须指定具体域名
res.header('Access-Control-Allow-Origin', 'https://example.com'); //
res.header('Access-Control-Allow-Credentials', 'true');
// 动态设置允许的源
const allowedOrigins = ['https://example.com', 'https://app.example.com'];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
res.header('Access-Control-Allow-Credentials', 'true');
}
// Cookie设置(跨域场景)
res.cookie('token', 'abc123', {
httpOnly: true,
secure: true, // HTTPS only
sameSite: 'none', // 允许跨域发送
maxAge: 86400000 // 1天
});</div>
</div>
<div class="card">
<h2>credentials模式对比</h2>
<table class="config-table">
<thead>
<tr>
<th>值</th>
<th>说明</th>
<th>发送Cookie</th>
<th>适用场景</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>omit</code></td>
<td>从不发送Cookie</td>
<td>否</td>
<td>不需要认证的公开API</td>
</tr>
<tr>
<td><code>same-origin</code></td>
<td>仅同源时发送Cookie(默认)</td>
<td>同源时发送</td>
<td>默认行为</td>
</tr>
<tr>
<td><code>include</code></td>
<td>始终发送Cookie</td>
<td>始终发送</td>
<td>跨域认证请求</td>
</tr>
</tbody>
</table>
</div>
<div class="card">
<h2>SameSite Cookie属性</h2>
<div class="info-box">
Chrome 80+ 默认将Cookie的SameSite设为Lax,这意味着跨站POST请求不会发送Cookie。
跨域场景需要显式设置 SameSite=None; Secure。
</div>
<table class="config-table">
<thead>
<tr>
<th>SameSite值</th>
<th>跨站GET</th>
<th>跨站POST</th>
<th>跨站XHR/Fetch</th>
<th>安全性</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>Strict</code></td>
<td>不发送</td>
<td>不发送</td>
<td>不发送</td>
<td>最高</td>
</tr>
<tr>
<td><code>Lax</code>(默认)</td>
<td>发送</td>
<td>不发送</td>
<td>不发送</td>
<td>中</td>
</tr>
<tr>
<td><code>None</code></td>
<td>发送</td>
<td>发送</td>
<td>发送</td>
<td>需配合Secure</td>
</tr>
</tbody>
</table>
</div>
<div class="card">
<h2>常见错误排查</h2>
<div class="code-block">// 错误1:Allow-Origin为*且Allow-Credentials为true
// 浏览器控制台错误:
// "Credentials flag is 'true', but the 'Access-Control-Allow-Origin'
// header is '*'."
// 解决:将*替换为具体的域名
// 错误2:Cookie未设置SameSite=None
// 浏览器控制台警告:
// "A cookie associated with a cross-site resource was set without
// the `SameSite` attribute."
// 解决:Set-Cookie: token=abc; SameSite=None; Secure
// 错误3:重定向后丢失CORS头
// 浏览器控制台错误:
// "Redirect from 'https://a.com/api' to 'https://b.com/api'
// has been blocked by CORS policy."
// 解决:确保重定向目标也设置了正确的CORS头
// 错误4:响应头缺少Expose-Headers
// 前端无法读取自定义响应头
// 解决:后端添加 Access-Control-Expose-Headers: X-Total-Count
// 错误5:预检请求未正确处理
// 浏览器控制台错误:
// "Response to preflight request doesn't pass access control check."
// 解决:确保OPTIONS请求返回204且包含正确的CORS头</div>
</div>
</div>
</body>
</html>示例5:开发环境代理配置
代码示例
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>开发环境代理配置</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: #1e1e2e;
color: #cdd6f4;
min-height: 100vh;
padding: 30px 20px;
}
.container { max-width: 900px; margin: 0 auto; }
h1 { text-align: center; color: #89b4fa; margin-bottom: 30px; }
.tool-card {
background: #313244;
border-radius: 12px;
padding: 24px;
margin-bottom: 16px;
border: 1px solid #45475a;
}
.tool-card h2 {
font-size: 16px;
margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
}
.tool-tag {
padding: 2px 8px;
border-radius: 4px;
font-size: 11px;
font-weight: 600;
}
.tag-vite { background: #646cff; color: white; }
.tag-webpack { background: #8dd6f9; color: #1e1e2e; }
.tag-node { background: #a6e3a1; color: #1e1e2e; }
.tag-next { background: #f38ba8; color: #1e1e2e; }
.code-block {
background: #1e1e2e;
color: #a6e3a1;
padding: 16px;
border-radius: 8px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
overflow-x: auto;
white-space: pre;
margin: 12px 0;
}
.comment { color: #6c7086; }
.keyword { color: #cba6f7; }
.string { color: #a6e3a1; }
.number { color: #fab387; }
p { font-size: 14px; line-height: 1.8; margin-bottom: 12px; color: #bac2de; }
</style>
</head>
<body>
<div class="container">
<h1>开发环境代理配置大全</h1>
<!-- Vite -->
<div class="tool-card">
<h2>Vite 代理配置 <span class="tool-tag tag-vite">Vite</span></h2>
<div class="code-block"><span class="comment">// vite.config.js</span>
<span class="keyword">import</span> { defineConfig } <span class="keyword">from</span> <span class="string">'vite'</span>
<span class="keyword">import</span> react <span class="keyword">from</span> <span class="string">'@vitejs/plugin-react'</span>
<span class="keyword">export default</span> defineConfig({
plugins: [react()],
server: {
<span class="comment">// 基础代理配置</span>
proxy: {
<span class="comment">// 将 /api 请求代理到目标服务器</span>
<span class="string">'/api'</span>: {
target: <span class="string">'https://api.example.com'</span>,
changeOrigin: <span class="keyword">true</span>,
<span class="comment">// 重写路径:/api/users → /users</span>
rewrite: (path) => path.replace(<span class="string">/^\/api/</span>, <span class="string">''</span>)
},
<span class="comment">// WebSocket代理</span>
<span class="string">'/ws'</span>: {
target: <span class="string">'ws://localhost:8080'</span>,
ws: <span class="keyword">true</span>
},
<span class="comment">// 多个API端点</span>
<span class="string">'/auth'</span>: {
target: <span class="string">'https://auth.example.com'</span>,
changeOrigin: <span class="keyword">true</span>
}
}
}
})</div>
</div>
<!-- Webpack -->
<div class="tool-card">
<h2>Webpack DevServer 代理配置 <span class="tool-tag tag-webpack">Webpack</span></h2>
<div class="code-block"><span class="comment">// webpack.config.js</span>
module.exports = {
devServer: {
proxy: {
<span class="comment">// 基础代理</span>
<span class="string">'/api'</span>: {
target: <span class="string">'https://api.example.com'</span>,
changeOrigin: <span class="keyword">true</span>,
pathRewrite: { <span class="string">'^/api'</span>: <span class="string">''</span> },
<span class="comment">// 添加自定义请求头</span>
headers: {
<span class="string">'X-Custom-Header'</span>: <span class="string">'dev-proxy'</span>
},
<span class="comment">// 仅代理HTTPS请求</span>
secure: <span class="keyword">true</span>,
<span class="comment">// 代理超时</span>
timeout: <span class="number">30000</span>
},
<span class="comment">// 条件代理</span>
<span class="string">'/api'</span>: {
target: <span class="string">'https://api.example.com'</span>,
changeOrigin: <span class="keyword">true</span>,
<span class="comment">// 仅代理GET请求</span>
bypass: function(req, res, options) {
<span class="keyword">if</span> (req.method !== <span class="string">'GET'</span>) {
<span class="keyword">return</span> <span class="string">'/fallback.html'</span>;
}
}
},
<span class="comment">// 多个目标</span>
<span class="string">'/api/v1'</span>: {
target: <span class="string">'https://v1.example.com'</span>,
changeOrigin: <span class="keyword">true</span>
},
<span class="string">'/api/v2'</span>: {
target: <span class="string">'https://v2.example.com'</span>,
changeOrigin: <span class="keyword">true</span>
}
}
}
}</div>
</div>
<!-- Node.js -->
<div class="tool-card">
<h2>Node.js 代理服务器 <span class="tool-tag tag-node">Node.js</span></h2>
<div class="code-block"><span class="comment">// server.js - Express + http-proxy-middleware</span>
<span class="keyword">const</span> express = require(<span class="string">'express'</span>);
<span class="keyword">const</span> { createProxyMiddleware } = require(<span class="string">'http-proxy-middleware'</span>);
<span class="keyword">const</span> app = express();
<span class="comment">// CORS中间件(如果前后端分离部署)</span>
app.use((req, res, next) => {
<span class="keyword">const</span> allowedOrigins = [<span class="string">'http://localhost:3000'</span>];
<span class="keyword">const</span> origin = req.headers.origin;
<span class="keyword">if</span> (allowedOrigins.includes(origin)) {
res.setHeader(<span class="string">'Access-Control-Allow-Origin'</span>, origin);
res.setHeader(<span class="string">'Access-Control-Allow-Methods'</span>, <span class="string">'GET,POST,PUT,DELETE'</span>);
res.setHeader(<span class="string">'Access-Control-Allow-Headers'</span>, <span class="string">'Content-Type,Authorization'</span>);
res.setHeader(<span class="string">'Access-Control-Allow-Credentials'</span>, <span class="string">'true'</span>);
}
<span class="keyword">if</span> (req.method === <span class="string">'OPTIONS'</span>) {
<span class="keyword">return</span> res.sendStatus(<span class="number">204</span>);
}
next();
});
<span class="comment">// API代理</span>
app.use(<span class="string">'/api'</span>, createProxyMiddleware({
target: <span class="string">'https://api.example.com'</span>,
changeOrigin: <span class="keyword">true</span>,
pathRewrite: { <span class="string">'^/api'</span>: <span class="string">''</span> },
<span class="comment">// 修改代理请求</span>
onProxyReq: (proxyReq, req, res) => {
<span class="comment">// 添加认证头</span>
proxyReq.setHeader(<span class="string">'X-Proxy-Secret'</span>, <span class="string">'my-secret'</span>);
},
<span class="comment">// 处理代理响应</span>
onProxyRes: (proxyRes, req, res) => {
console.log(<span class="string">`[Proxy] ${req.method} ${req.url} → ${proxyRes.statusCode}`</span>);
},
<span class="comment">// 错误处理</span>
onError: (err, req, res) => {
console.error(<span class="string">'Proxy error:'</span>, err);
res.status(<span class="number">500</span>).json({ error: <span class="string">'Proxy error'</span> });
}
}));
<span class="comment">// 静态文件服务</span>
app.use(express.static(<span class="string">'dist'</span>));
app.listen(<span class="number">3000</span>, () => {
console.log(<span class="string">'Server running on http://localhost:3000'</span>);
});</div>
</div>
<!-- Next.js -->
<div class="tool-card">
<h2>Next.js API Routes 代理 <span class="tool-tag tag-next">Next.js</span></h2>
<div class="code-block"><span class="comment">// next.config.js - Rewrites方式</span>
module.exports = {
async rewrites() {
<span class="keyword">return</span> [
{
source: <span class="string">'/api/:path*'</span>,
destination: <span class="string">'https://api.example.com/:path*'</span>,
},
]
}
}
<span class="comment">// pages/api/[...path].js - API Route方式</span>
<span class="keyword">export default async function</span> handler(req, res) {
<span class="keyword">const</span> { path } = req.query;
<span class="keyword">const</span> targetUrl = <span class="string">`https://api.example.com/${path.join('/')}`</span>;
<span class="keyword">try</span> {
<span class="keyword">const</span> response = <span class="keyword">await</span> fetch(targetUrl, {
method: req.method,
headers: {
<span class="string">'Content-Type'</span>: <span class="string">'application/json'</span>,
<span class="string">'Authorization'</span>: req.headers.authorization || <span class="string">''</span>,
},
body: [<span class="string">'GET'</span>, <span class="string">'HEAD'</span>].includes(req.method) ? <span class="keyword">undefined</span> : JSON.stringify(req.body),
});
<span class="keyword">const</span> data = <span class="keyword">await</span> response.json();
res.status(response.status).json(data);
} <span class="keyword">catch</span> (error) {
res.status(<span class="number">500</span>).json({ error: <span class="string">'Proxy error'</span> });
}
}</div>
</div>
</div>
</body>
</html>浏览器兼容性
注意事项与最佳实践
1. 安全配置原则
代码示例
// 后端CORS安全配置(Node.js示例)
function corsMiddleware(req, res, next) {
// 1. 不要使用 * 通配符(生产环境)
// res.header('Access-Control-Allow-Origin', '*'); // 不安全
// 2. 使用白名单验证Origin
const allowedOrigins = [
'https://example.com',
'https://app.example.com'
];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin); // 动态设置
} else {
// 不在白名单中的源,不设置CORS头
return res.status(403).json({ error: 'Origin not allowed' });
}
// 3. 限制允许的方法
res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE');
// 4. 限制允许的请求头
res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization');
// 5. 仅在需要时允许凭证
res.header('Access-Control-Allow-Credentials', 'true');
// 6. 设置合理的预检缓存时间
res.header('Access-Control-Max-Age', '86400');
// 7. 暴露必要的响应头
res.header('Access-Control-Expose-Headers', 'X-Total-Count,X-Request-Id');
// 8. 处理预检请求
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
}2. 避免CORS的常见陷阱
代码示例
// 陷阱1:重定向导致CORS头丢失
// 301/302重定向后,浏览器会重新检查CORS头
// 如果重定向目标没有设置CORS头,请求会失败
// 解决:确保重定向目标也设置了正确的CORS头
// 陷阱2:错误处理中的CORS问题
fetch('https://api.example.com/data')
.then(response => {
// 即使HTTP状态码是4xx/5xx,只要CORS头正确,
// 这个then也会执行(不会进入catch)
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
})
.catch(error => {
// 只有网络错误或CORS被拒绝时才会进入catch
// CORS被拒绝时,error.message通常是 "Failed to fetch"
// 无法获取具体的CORS错误信息
console.error('请求失败:', error);
});
// 陷阱3:304 Not Modified与CORS
// 浏览器缓存了304响应,但CORS头可能已变化
// 解决:在开发时禁用缓存,或确保CORS配置一致
// 陷阱4:文件协议(file://)的CORS限制
// file://协议的页面发送请求时,Origin为null
// 大多数服务器不会允许Origin: null
// 解决:使用本地开发服务器(http://localhost)3. 调试CORS问题
代码示例
// 1. 使用浏览器开发者工具
// Network面板中查看:
// - 请求是否发送了OPTIONS预检请求
// - 响应头中是否包含Access-Control-*头
// - 控制台中的CORS错误信息
// 2. 常见CORS错误信息及解决方案
const corsErrors = {
'No Access-Control-Allow-Origin':
'服务器未设置Access-Control-Allow-Origin响应头',
'CORS policy: Response to preflight':
'预检请求失败,检查OPTIONS响应的CORS头',
'Credentials flag is true':
'携带凭证时Allow-Origin不能为*,需指定具体域名',
'Redirect is not allowed':
'预检请求不能重定向',
'Does not match any allowed origins':
'请求源不在服务器允许的列表中'
};
// 3. 使用curl测试(绕过浏览器CORS)
// curl -H "Origin: https://example.com" \
// -H "Access-Control-Request-Method: POST" \
// -H "Access-Control-Request-Headers: Content-Type" \
// -X OPTIONS https://api.example.com/data -v代码规范示例
规范的CORS中间件
代码示例
/**
* CORS中间件
* 提供安全、灵活的跨域配置
*/
class CorsMiddleware {
/**
* @param {Object} options - 配置项
* @param {string[]} options.allowedOrigins - 允许的源列表
* @param {string[]} options.allowedMethods - 允许的HTTP方法
* @param {string[]} options.allowedHeaders - 允许的请求头
* @param {string[]} options.exposedHeaders - 暴露的响应头
* @param {boolean} options.allowCredentials - 是否允许凭证
* @param {number} options.maxAge - 预检缓存时间(秒)
*/
constructor(options = {}) {
this.allowedOrigins = options.allowedOrigins || [];
this.allowedMethods = options.allowedMethods || ['GET', 'POST', 'PUT', 'DELETE'];
this.allowedHeaders = options.allowedHeaders || ['Content-Type', 'Authorization'];
this.exposedHeaders = options.exposedHeaders || [];
this.allowCredentials = options.allowCredentials || false;
this.maxAge = options.maxAge || 86400;
}
/**
* 检查Origin是否被允许
*/
isOriginAllowed(origin) {
if (!origin) return false;
return this.allowedOrigins.some(allowed => {
// 支持通配符匹配
if (allowed.startsWith('*.')) {
const domain = allowed.slice(2);
return origin === `https://${domain}` ||
origin.endsWith(`.${domain}`);
}
return origin === allowed;
});
}
/**
* Express中间件
*/
express() {
return (req, res, next) => {
const origin = req.headers.origin;
if (!this.isOriginAllowed(origin)) {
if (req.method === 'OPTIONS') {
return res.sendStatus(403);
}
return next();
}
// 设置CORS头
res.setHeader('Access-Control-Allow-Origin', origin);
res.setHeader('Access-Control-Allow-Methods',
this.allowedMethods.join(','));
res.setHeader('Access-Control-Allow-Headers',
this.allowedHeaders.join(','));
if (this.allowCredentials) {
res.setHeader('Access-Control-Allow-Credentials', 'true');
}
if (this.exposedHeaders.length > 0) {
res.setHeader('Access-Control-Expose-Headers',
this.exposedHeaders.join(','));
}
if (this.maxAge > 0) {
res.setHeader('Access-Control-Max-Age',
this.maxAge.toString());
}
// 处理预检请求
if (req.method === 'OPTIONS') {
return res.sendStatus(204);
}
next();
};
}
}
// 使用示例
const cors = new CorsMiddleware({
allowedOrigins: [
'https://example.com',
'*.example.com', // 支持通配符
'http://localhost:3000' // 开发环境
],
allowedMethods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-Id'],
exposedHeaders: ['X-Total-Count', 'X-Request-Id'],
allowCredentials: true,
maxAge: 86400
});
app.use(cors.express());常见问题与解决方案
问题1:本地开发跨域问题
代码示例
// 问题:前端localhost:3000访问后端localhost:8080被CORS拦截
// 解决方案1:开发服务器代理(推荐)
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
};
// 解决方案2:后端开发环境允许所有源
if (process.env.NODE_ENV === 'development') {
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', '*');
res.header('Access-Control-Allow-Headers', '*');
if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});
}
// 解决方案3:浏览器禁用CORS(仅开发用)
// Chrome启动参数:
// --disable-web-security --user-data-dir=/tmp/chrome_dev问题2:第三方API无CORS头
代码示例
// 问题:调用第三方API,但对方未设置CORS头
// 解决方案1:自建代理服务器
app.use('/proxy', createProxyMiddleware({
target: 'https://third-party-api.com',
changeOrigin: true,
pathRewrite: { '^/proxy': '' }
}));
// 解决方案2:使用公共CORS代理(仅开发)
const proxyUrl = 'https://api.allorigins.win/raw?url=';
fetch(proxyUrl + encodeURIComponent(targetUrl));
// 解决方案3:JSONP(如果API支持)
function jsonp(url, callback) {
const script = document.createElement('script');
script.src = `${url}?callback=${callback}`;
document.head.appendChild(script);
}问题3:CORS与认证Token
代码示例
// 问题:使用Bearer Token认证时CORS配置
// 前端:在请求头中添加Authorization
fetch('https://api.example.com/data', {
headers: {
'Authorization': `Bearer ${token}` // 触发预检请求
}
});
// 后端:必须允许Authorization头
res.header('Access-Control-Allow-Headers', 'Content-Type,Authorization');
// 注意:Bearer Token不受SameSite Cookie限制
// 如果可能,优先使用Token认证而非Cookie认证总结
跨域请求与CORS是前端与后端通信中必须掌握的核心知识,本教程涵盖了以下内容:
同源策略:理解浏览器同源策略的定义、限制范围和存在意义,这是理解所有跨域问题的基础。
CORS机制:掌握CORS的工作流程,包括简单请求和预检请求的区别、触发条件、相关HTTP头部的含义和配置。
跨域解决方案:全面了解CORS、JSONP、代理服务器、postMessage、Nginx反向代理等方案,根据场景选择合适的方案。
Cookie与凭证:理解跨域请求携带Cookie的三要素(credentials、Allow-Credentials、SameSite),以及常见的配置错误。
开发环境配置:掌握Vite、Webpack、Node.js、Next.js等工具和框架的代理配置方法,解决本地开发跨域问题。
安全与调试:学习CORS的安全配置原则、常见陷阱和调试方法,确保跨域配置既安全又正确。
最佳实践:生产环境使用CORS或Nginx代理,开发环境使用devServer代理,避免使用通配符*,验证Origin白名单。
常见问题
什么是16. 跨域请求与CORS?
16. 跨域请求与CORS是HTML5开发中的重要技术,本教程详细介绍了其核心概念和实践方法。
如何学习16. 跨域请求与CORS的实际应用?
教程中提供了完整的代码示例和实践指导,建议结合示例代码动手练习。
16. 跨域请求与CORS有哪些注意事项?
常见注意事项包括兼容性、性能优化等,教程的注意事项与最佳实践部分有详细说明。
16. 跨域请求与CORS适合初学者吗?
本教程从基础概念讲起,循序渐进,适合有一定HTML和JavaScript基础的初学者。
16. 跨域请求与CORS的核心要点是什么?
核心要点包括教程简介等内容,建议按教程顺序逐步学习。
本文由小确幸生活整理发布,转载请注明出处
本文涉及AI创作
内容由AI创作,请仔细甄别