前言

跨域问题是前端开发中最常遇到的问题之一,它源于浏览器的同源策略安全机制。随着现代Web应用架构的复杂化,前后端分离、微服务架构的普及,跨域问题变得更加突出。本文将从同源策略的基本概念出发,深入分析跨域问题的本质,并详细介绍各种跨域解决方案的原理、实现方式和适用场景。

一、同源策略基础

(一)什么是同源策略

同源策略(Same-Origin Policy)是浏览器的一个重要安全机制,它限制了从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文档的重要安全机制。

1. 源的定义

一个源由三个部分组成:

  • 协议(Protocol):如 http://https://
  • 域名(Domain):如 example.comapi.example.com
  • 端口(Port):如 :80:443:3000
1
2
3
4
5
6
7
8
9
10
11
// 同源示例
const currentOrigin = 'https://www.example.com:443';

// 以下URL与当前源的对比:
const urls = [
'https://www.example.com:443/api/data', // ✅ 同源(协议、域名、端口都相同)
'https://www.example.com/api/data', // ✅ 同源(HTTPS默认端口443)
'http://www.example.com:443/api/data', // ❌ 跨域(协议不同)
'https://api.example.com:443/api/data', // ❌ 跨域(域名不同)
'https://www.example.com:8080/api/data', // ❌ 跨域(端口不同)
];

2. 同源策略的限制范围

同源策略主要限制以下几个方面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 1. XMLHttpRequest和Fetch API
fetch('https://api.other-domain.com/data')
.then(response => response.json())
.catch(error => {
// 跨域请求会被阻止
console.error('CORS error:', error);
});

// 2. DOM访问限制
// 无法访问不同源的iframe内容
const iframe = document.getElementById('cross-origin-iframe');
try {
const iframeDocument = iframe.contentDocument; // 会抛出错误
} catch (error) {
console.error('Cannot access cross-origin iframe:', error);
}

// 3. Cookie、LocalStorage、SessionStorage访问限制
// 无法读取其他域的存储数据

(二)同源策略的安全意义

1. 防止恶意脚本攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 恶意网站示例:如果没有同源策略
// 恶意网站 evil.com 上的脚本
function stealUserData() {
// 如果没有同源策略,恶意脚本可以:

// 1. 读取用户在银行网站的Cookie
const bankCookies = document.cookie; // 被同源策略阻止

// 2. 向银行API发送请求
fetch('https://bank.com/api/transfer', {
method: 'POST',
body: JSON.stringify({
to: 'evil-account',
amount: 10000
})
}); // 被同源策略阻止

// 3. 读取用户的个人信息
const userInfo = localStorage.getItem('userInfo'); // 被同源策略阻止
}

2. 保护用户隐私

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
// 同源策略保护用户隐私的示例
class PrivacyProtection {
constructor() {
this.sensitiveData = {
personalInfo: localStorage.getItem('personalInfo'),
authToken: sessionStorage.getItem('authToken'),
userPreferences: this.getCookieValue('preferences')
};
}

// 只有同源的脚本才能访问这些数据
getSensitiveData() {
// 这些数据只能被同源的脚本访问
return this.sensitiveData;
}

getCookieValue(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
return parts.pop().split(';').shift();
}
return null;
}
}

二、跨域问题的表现形式

(一)常见的跨域错误

1. CORS错误

1
2
3
4
5
6
7
8
9
10
11
12
13
// 典型的CORS错误示例
async function fetchCrossOriginData() {
try {
const response = await fetch('https://api.external-service.com/data');
const data = await response.json();
return data;
} catch (error) {
// 浏览器控制台会显示类似错误:
// Access to fetch at 'https://api.external-service.com/data'
// from origin 'https://my-website.com' has been blocked by CORS policy
console.error('CORS Error:', error);
}
}

2. 不同类型请求的跨域表现

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 跨域请求检测工具
class CrossOriginDetector {
constructor() {
this.currentOrigin = window.location.origin;
}

// 检测URL是否跨域
isCrossOrigin(url) {
try {
const targetURL = new URL(url);
const currentURL = new URL(this.currentOrigin);

return (
targetURL.protocol !== currentURL.protocol ||
targetURL.hostname !== currentURL.hostname ||
targetURL.port !== currentURL.port
);
} catch (error) {
console.error('Invalid URL:', url);
return true;
}
}

// 测试不同类型的跨域请求
async testCrossOriginRequests() {
const testUrls = [
'https://api.github.com/users/octocat',
'https://jsonplaceholder.typicode.com/posts/1',
'https://httpbin.org/get'
];

for (const url of testUrls) {
console.log(`Testing: ${url}`);
console.log(`Is cross-origin: ${this.isCrossOrigin(url)}`);

try {
// 测试fetch请求
const response = await fetch(url);
console.log(`✅ Fetch successful: ${response.status}`);
} catch (error) {
console.log(`❌ Fetch failed: ${error.message}`);
}

// 测试图片加载(通常不受CORS限制)
this.testImageLoad(url);
}
}

testImageLoad(url) {
const img = new Image();
img.onload = () => console.log(`✅ Image load successful: ${url}`);
img.onerror = () => console.log(`❌ Image load failed: ${url}`);
img.src = url;
}
}

// 使用示例
const detector = new CrossOriginDetector();
detector.testCrossOriginRequests();

(二)哪些资源不受同源策略限制

1. 标签嵌入资源

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
<!-- 以下资源通常不受同源策略限制 -->

<!-- 图片 -->
<img src="https://external-site.com/image.jpg" alt="External Image">

<!-- 样式表 -->
<link rel="stylesheet" href="https://external-site.com/styles.css">

<!-- 脚本文件 -->
<script src="https://external-site.com/script.js"></script>

<!-- 字体文件 -->
<style>
@font-face {
font-family: 'ExternalFont';
src: url('https://external-site.com/font.woff2');
}
</style>

<!-- 视频和音频 -->
<video src="https://external-site.com/video.mp4"></video>
<audio src="https://external-site.com/audio.mp3"></audio>

<!-- iframe(但内容访问受限) -->
<iframe src="https://external-site.com/page.html"></iframe>

2. 表单提交

1
2
3
4
5
<!-- 表单可以向任何域提交数据 -->
<form action="https://external-api.com/submit" method="POST">
<input type="text" name="data" value="test">
<button type="submit">Submit to External Domain</button>
</form>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// JavaScript表单提交也不受同源策略限制
function submitToExternalDomain() {
const form = document.createElement('form');
form.method = 'POST';
form.action = 'https://external-api.com/submit';

const input = document.createElement('input');
input.type = 'hidden';
input.name = 'data';
input.value = 'test data';

form.appendChild(input);
document.body.appendChild(form);
form.submit(); // 这个提交不会被同源策略阻止
}

三、CORS(跨域资源共享)详解

(一)CORS基本概念

CORS(Cross-Origin Resource Sharing)是W3C标准,它使用额外的HTTP头来告诉浏览器让运行在一个域上的Web应用被准许访问来自不同源服务器上的指定资源。

1. CORS工作原理

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// CORS请求流程示例
class CORSExample {
constructor() {
this.apiBase = 'https://api.external-service.com';
}

// 简单请求示例
async makeSimpleRequest() {
try {
const response = await fetch(`${this.apiBase}/data`, {
method: 'GET',
headers: {
'Content-Type': 'application/json'
}
});

// 浏览器会自动添加Origin头
// Origin: https://my-website.com

// 服务器需要返回适当的CORS头
// Access-Control-Allow-Origin: https://my-website.com
// 或者 Access-Control-Allow-Origin: *

return await response.json();
} catch (error) {
console.error('CORS request failed:', error);
}
}

// 预检请求示例
async makePreflightRequest() {
try {
const response = await fetch(`${this.apiBase}/data`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'custom-value'
},
body: JSON.stringify({ data: 'test' })
});

// 浏览器会先发送OPTIONS预检请求
// OPTIONS /data HTTP/1.1
// Origin: https://my-website.com
// Access-Control-Request-Method: PUT
// Access-Control-Request-Headers: X-Custom-Header

return await response.json();
} catch (error) {
console.error('Preflight request failed:', error);
}
}
}

2. 简单请求 vs 预检请求

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 请求类型判断工具
class CORSRequestAnalyzer {
// 简单请求的条件
isSimpleRequest(method, headers, contentType) {
// 1. 方法必须是以下之一
const simpleMethods = ['GET', 'HEAD', 'POST'];
if (!simpleMethods.includes(method.toUpperCase())) {
return false;
}

// 2. 只能包含简单头部
const simpleHeaders = [
'accept',
'accept-language',
'content-language',
'content-type'
];

const headerKeys = Object.keys(headers).map(key => key.toLowerCase());
const hasComplexHeaders = headerKeys.some(key => !simpleHeaders.includes(key));

if (hasComplexHeaders) {
return false;
}

// 3. Content-Type必须是以下之一
if (contentType) {
const simpleContentTypes = [
'application/x-www-form-urlencoded',
'multipart/form-data',
'text/plain'
];

if (!simpleContentTypes.includes(contentType)) {
return false;
}
}

return true;
}

// 分析请求类型
analyzeRequest(method, headers = {}, contentType = null) {
const isSimple = this.isSimpleRequest(method, headers, contentType);

return {
isSimple,
requiresPreflight: !isSimple,
analysis: {
method: method,
headers: headers,
contentType: contentType,
reason: isSimple ? 'Meets all simple request criteria' : 'Contains complex headers or method'
}
};
}
}

// 使用示例
const analyzer = new CORSRequestAnalyzer();

// 简单请求示例
console.log(analyzer.analyzeRequest('GET', {
'Accept': 'application/json'
}));

// 预检请求示例
console.log(analyzer.analyzeRequest('PUT', {
'Content-Type': 'application/json',
'Authorization': 'Bearer token123'
}));

(二)服务端CORS配置

1. Node.js/Express CORS配置

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// Express服务器CORS配置示例
const express = require('express');
const cors = require('cors');
const app = express();

// 1. 基本CORS配置
app.use(cors());

// 2. 自定义CORS配置
const corsOptions = {
origin: function (origin, callback) {
// 允许的域名列表
const allowedOrigins = [
'https://my-website.com',
'https://admin.my-website.com',
'http://localhost:3000'
];

// 允许没有origin的请求(如移动应用)
if (!origin) return callback(null, true);

if (allowedOrigins.indexOf(origin) !== -1) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With'],
credentials: true, // 允许携带Cookie
maxAge: 86400 // 预检请求缓存时间(秒)
};

app.use(cors(corsOptions));

// 3. 手动设置CORS头
app.use((req, res, next) => {
const origin = req.headers.origin;
const allowedOrigins = ['https://my-website.com', 'http://localhost:3000'];

if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}

res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Max-Age', '86400');

// 处理预检请求
if (req.method === 'OPTIONS') {
res.status(200).end();
return;
}

next();
});

// API路由
app.get('/api/data', (req, res) => {
res.json({ message: 'CORS enabled data', timestamp: Date.now() });
});

app.listen(3001, () => {
console.log('CORS-enabled server running on port 3001');
});

2. 其他服务器CORS配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Nginx CORS配置
server {
listen 80;
server_name api.example.com;

location /api/ {
# 处理预检请求
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization, X-Requested-With' always;
add_header 'Access-Control-Max-Age' 86400 always;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain';
return 204;
}

# 处理实际请求
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;

proxy_pass http://backend;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
# Apache CORS配置 (.htaccess)
<IfModule mod_headers.c>
# 处理预检请求
RewriteEngine On
RewriteCond %{REQUEST_METHOD} OPTIONS
RewriteRule ^(.*)$ $1 [R=200,L]

# 设置CORS头
Header always set Access-Control-Allow-Origin "*"
Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header always set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With"
Header always set Access-Control-Max-Age "86400"
</IfModule>

四、JSONP跨域解决方案

(一)JSONP原理与实现

JSONP(JSON with Padding)是一种利用<script>标签不受同源策略限制的特性来实现跨域请求的技术。

1. JSONP基本原理

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
// JSONP实现原理
class JSONPClient {
constructor() {
this.callbackCounter = 0;
this.callbacks = {};
}

// 发送JSONP请求
request(url, options = {}) {
return new Promise((resolve, reject) => {
// 生成唯一的回调函数名
const callbackName = `jsonp_callback_${Date.now()}_${++this.callbackCounter}`;

// 设置超时时间
const timeout = options.timeout || 10000;
let timeoutId;

// 创建全局回调函数
window[callbackName] = (data) => {
// 清理工作
this.cleanup(callbackName, timeoutId);
resolve(data);
};

// 创建script标签
const script = document.createElement('script');
script.src = this.buildURL(url, callbackName, options.params);

// 错误处理
script.onerror = () => {
this.cleanup(callbackName, timeoutId);
reject(new Error('JSONP request failed'));
};

// 设置超时
timeoutId = setTimeout(() => {
this.cleanup(callbackName, timeoutId);
reject(new Error('JSONP request timeout'));
}, timeout);

// 添加到页面并发送请求
document.head.appendChild(script);
});
}

// 构建请求URL
buildURL(url, callbackName, params = {}) {
const urlObj = new URL(url);

// 添加回调参数
urlObj.searchParams.set('callback', callbackName);

// 添加其他参数
Object.keys(params).forEach(key => {
urlObj.searchParams.set(key, params[key]);
});

return urlObj.toString();
}

// 清理资源
cleanup(callbackName, timeoutId) {
// 清除超时定时器
if (timeoutId) {
clearTimeout(timeoutId);
}

// 删除全局回调函数
if (window[callbackName]) {
delete window[callbackName];
}

// 移除script标签
const script = document.querySelector(`script[src*="${callbackName}"]`);
if (script && script.parentNode) {
script.parentNode.removeChild(script);
}
}
}

// 使用示例
const jsonpClient = new JSONPClient();

async function fetchWeatherData() {
try {
const data = await jsonpClient.request('https://api.weather.com/data', {
params: {
city: 'Beijing',
key: 'your-api-key'
},
timeout: 5000
});

console.log('Weather data:', data);
return data;
} catch (error) {
console.error('Failed to fetch weather data:', error);
}
}

2. 服务端JSONP支持

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
40
41
42
43
// Node.js服务端JSONP支持
const express = require('express');
const app = express();

// JSONP中间件
function jsonp(req, res, next) {
res.jsonp = function(data) {
const callback = req.query.callback;

if (callback) {
// 安全检查:确保callback是有效的函数名
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(callback)) {
return res.status(400).json({ error: 'Invalid callback name' });
}

// 返回JSONP响应
res.set('Content-Type', 'application/javascript');
res.send(`${callback}(${JSON.stringify(data)});`);
} else {
// 普通JSON响应
res.json(data);
}
};

next();
}

app.use(jsonp);

// API端点
app.get('/api/data', (req, res) => {
const data = {
message: 'Hello from JSONP API',
timestamp: Date.now(),
params: req.query
};

res.jsonp(data);
});

app.listen(3002, () => {
console.log('JSONP server running on port 3002');
});

(二)JSONP的优缺点

1. 优点

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
40
41
// JSONP优点演示
class JSONPAdvantages {
// 1. 兼容性好 - 支持所有浏览器
demonstrateCompatibility() {
console.log('JSONP works in all browsers, including IE6+');

// 即使在老旧浏览器中也能工作
const script = document.createElement('script');
script.src = 'https://api.example.com/data?callback=handleData';
document.head.appendChild(script);
}

// 2. 实现简单
simpleImplementation() {
// 最简单的JSONP实现
function jsonp(url, callback) {
const script = document.createElement('script');
const callbackName = 'jsonp_' + Date.now();

window[callbackName] = function(data) {
callback(data);
document.head.removeChild(script);
delete window[callbackName];
};

script.src = url + '?callback=' + callbackName;
document.head.appendChild(script);
}

// 使用
jsonp('https://api.example.com/data', function(data) {
console.log('Received data:', data);
});
}

// 3. 不需要服务器特殊配置(相比CORS)
noSpecialServerConfig() {
console.log('Server only needs to support callback parameter');
console.log('No need for CORS headers configuration');
}
}

2. 缺点和限制

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// JSONP缺点演示
class JSONPLimitations {
// 1. 只支持GET请求
demonstrateGETOnly() {
console.log('JSONP只能发送GET请求,无法发送POST、PUT、DELETE等');

// 无法实现的操作
// jsonp('https://api.example.com/users', 'POST', userData); // 不可能
}

// 2. 安全风险
demonstrateSecurityRisks() {
// XSS风险:恶意服务器可以执行任意JavaScript代码
function unsafeJSONP(url) {
const script = document.createElement('script');
script.src = url; // 如果服务器返回恶意代码,会被执行
document.head.appendChild(script);
}

// 安全的JSONP实现应该验证响应
function safeJSONP(url, callback) {
const script = document.createElement('script');
const callbackName = 'jsonp_' + Date.now();

window[callbackName] = function(data) {
try {
// 验证数据格式
if (typeof data === 'object' && data !== null) {
callback(data);
} else {
throw new Error('Invalid JSONP response');
}
} catch (error) {
console.error('JSONP security error:', error);
} finally {
// 清理
document.head.removeChild(script);
delete window[callbackName];
}
};

script.src = url + '?callback=' + callbackName;
document.head.appendChild(script);
}
}

// 3. 错误处理困难
demonstrateErrorHandling() {
function jsonpWithErrorHandling(url, callback, errorCallback) {
const script = document.createElement('script');
const callbackName = 'jsonp_' + Date.now();
let timeoutId;

// 成功回调
window[callbackName] = function(data) {
clearTimeout(timeoutId);
callback(data);
cleanup();
};

// 错误处理
script.onerror = function() {
clearTimeout(timeoutId);
errorCallback(new Error('JSONP request failed'));
cleanup();
};

// 超时处理
timeoutId = setTimeout(() => {
errorCallback(new Error('JSONP request timeout'));
cleanup();
}, 10000);

function cleanup() {
if (script.parentNode) {
script.parentNode.removeChild(script);
}
delete window[callbackName];
}

script.src = url + '?callback=' + callbackName;
document.head.appendChild(script);
}
}
}

五、代理服务器解决方案

(一)开发环境代理配置

1. Webpack Dev Server代理

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
40
// webpack.config.js
module.exports = {
// ... 其他配置
devServer: {
port: 3000,
proxy: {
// 代理API请求
'/api': {
target: 'https://api.external-service.com',
changeOrigin: true,
pathRewrite: {
'^/api': '' // 移除/api前缀
},
secure: true, // 支持HTTPS
logLevel: 'debug'
},

// 多个代理配置
'/auth': {
target: 'https://auth.external-service.com',
changeOrigin: true,
headers: {
'Authorization': 'Bearer your-token'
}
},

// 条件代理
'/conditional': {
target: 'https://api.external-service.com',
changeOrigin: true,
bypass: function(req, res, proxyOptions) {
// 根据条件决定是否代理
if (req.headers.accept.indexOf('html') !== -1) {
return '/index.html';
}
}
}
}
}
};

2. Vite代理配置

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
// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
server: {
port: 3000,
proxy: {
// 字符串简写
'/api': 'https://api.external-service.com',

// 详细配置
'/api/v2': {
target: 'https://api-v2.external-service.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/v2/, ''),
configure: (proxy, options) => {
// 自定义代理配置
proxy.on('proxyReq', (proxyReq, req, res) => {
console.log('Proxying request:', req.url);
});
}
},

// WebSocket代理
'/socket.io': {
target: 'ws://localhost:3001',
ws: true
}
}
}
});

3. Create React App代理

1
2
3
4
5
6
7
8
9
// package.json
{
"name": "my-app",
"version": "0.1.0",
"proxy": "https://api.external-service.com",
"dependencies": {
// ...
}
}
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
// 或者使用setupProxy.js进行高级配置
// src/setupProxy.js
const { createProxyMiddleware } = require('http-proxy-middleware');

module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'https://api.external-service.com',
changeOrigin: true,
pathRewrite: {
'^/api': ''
},
onProxyReq: function(proxyReq, req, res) {
// 修改代理请求
proxyReq.setHeader('X-Forwarded-For', req.ip);
},
onProxyRes: function(proxyRes, req, res) {
// 修改代理响应
proxyRes.headers['Access-Control-Allow-Origin'] = '*';
}
})
);

app.use(
'/auth',
createProxyMiddleware({
target: 'https://auth.external-service.com',
changeOrigin: true,
secure: true
})
);
};

(二)生产环境代理

1. Nginx反向代理

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
40
41
42
# nginx.conf
server {
listen 80;
server_name my-website.com;

# 前端静态文件
location / {
root /var/www/html;
try_files $uri $uri/ /index.html;
}

# API代理
location /api/ {
proxy_pass https://api.external-service.com/;
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
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;

# 预检请求处理
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
add_header 'Content-Type' 'text/plain';
return 204;
}
}

# WebSocket代理
location /ws/ {
proxy_pass https://ws.external-service.com/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}

2. Node.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
40
41
42
43
44
45
46
47
48
// proxy-server.js
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const path = require('path');

const app = express();

// 静态文件服务
app.use(express.static(path.join(__dirname, 'build')));

// API代理中间件
const apiProxy = createProxyMiddleware('/api', {
target: 'https://api.external-service.com',
changeOrigin: true,
pathRewrite: {
'^/api': ''
},
onProxyReq: (proxyReq, req, res) => {
// 添加认证头
if (req.headers.authorization) {
proxyReq.setHeader('Authorization', req.headers.authorization);
}

// 记录请求
console.log(`Proxying ${req.method} ${req.url} to ${proxyReq.path}`);
},
onProxyRes: (proxyRes, req, res) => {
// 添加CORS头
proxyRes.headers['Access-Control-Allow-Origin'] = '*';
proxyRes.headers['Access-Control-Allow-Credentials'] = 'true';
},
onError: (err, req, res) => {
console.error('Proxy error:', err);
res.status(500).json({ error: 'Proxy error' });
}
});

app.use(apiProxy);

// 处理SPA路由
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'build', 'index.html'));
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Proxy server running on port ${PORT}`);
});

六、其他跨域解决方案

(一)PostMessage跨域通信

1. 父子窗口通信

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
// 父窗口代码
class ParentWindowCommunicator {
constructor() {
this.childWindow = null;
this.targetOrigin = 'https://child-domain.com';
this.setupMessageListener();
}

// 打开子窗口
openChildWindow() {
this.childWindow = window.open(
'https://child-domain.com/child.html',
'childWindow',
'width=600,height=400'
);

// 等待子窗口加载完成后发送消息
setTimeout(() => {
this.sendMessageToChild({
type: 'INIT',
data: { message: 'Hello from parent!' }
});
}, 1000);
}

// 发送消息到子窗口
sendMessageToChild(message) {
if (this.childWindow && !this.childWindow.closed) {
this.childWindow.postMessage(message, this.targetOrigin);
}
}

// 监听来自子窗口的消息
setupMessageListener() {
window.addEventListener('message', (event) => {
// 验证消息来源
if (event.origin !== this.targetOrigin) {
console.warn('Received message from untrusted origin:', event.origin);
return;
}

console.log('Received message from child:', event.data);

// 处理不同类型的消息
switch (event.data.type) {
case 'CHILD_READY':
this.handleChildReady(event.data);
break;
case 'DATA_REQUEST':
this.handleDataRequest(event.data);
break;
case 'CLOSE_REQUEST':
this.handleCloseRequest();
break;
default:
console.log('Unknown message type:', event.data.type);
}
});
}

handleChildReady(data) {
console.log('Child window is ready');
this.sendMessageToChild({
type: 'CONFIG',
data: { theme: 'dark', language: 'zh-CN' }
});
}

handleDataRequest(data) {
// 模拟数据获取
const responseData = {
users: [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
]
};

this.sendMessageToChild({
type: 'DATA_RESPONSE',
data: responseData,
requestId: data.requestId
});
}

handleCloseRequest() {
if (this.childWindow) {
this.childWindow.close();
this.childWindow = null;
}
}
}

// 使用示例
const communicator = new ParentWindowCommunicator();
document.getElementById('openChild').addEventListener('click', () => {
communicator.openChildWindow();
});
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
// 子窗口代码 (child.html)
class ChildWindowCommunicator {
constructor() {
this.parentOrigin = 'https://parent-domain.com';
this.setupMessageListener();
this.notifyParentReady();
}

// 通知父窗口子窗口已准备就绪
notifyParentReady() {
this.sendMessageToParent({
type: 'CHILD_READY',
data: { timestamp: Date.now() }
});
}

// 发送消息到父窗口
sendMessageToParent(message) {
window.parent.postMessage(message, this.parentOrigin);
}

// 监听来自父窗口的消息
setupMessageListener() {
window.addEventListener('message', (event) => {
// 验证消息来源
if (event.origin !== this.parentOrigin) {
console.warn('Received message from untrusted origin:', event.origin);
return;
}

console.log('Received message from parent:', event.data);

// 处理不同类型的消息
switch (event.data.type) {
case 'INIT':
this.handleInit(event.data);
break;
case 'CONFIG':
this.handleConfig(event.data);
break;
case 'DATA_RESPONSE':
this.handleDataResponse(event.data);
break;
default:
console.log('Unknown message type:', event.data.type);
}
});
}

handleInit(data) {
document.getElementById('message').textContent = data.data.message;
}

handleConfig(data) {
// 应用配置
document.body.className = data.data.theme;
document.documentElement.lang = data.data.language;
}

handleDataResponse(data) {
// 显示数据
const userList = document.getElementById('userList');
userList.innerHTML = '';

data.data.users.forEach(user => {
const li = document.createElement('li');
li.textContent = `${user.id}: ${user.name}`;
userList.appendChild(li);
});
}

// 请求数据
requestData() {
this.sendMessageToParent({
type: 'DATA_REQUEST',
requestId: Date.now()
});
}

// 请求关闭窗口
requestClose() {
this.sendMessageToParent({
type: 'CLOSE_REQUEST'
});
}
}

// 初始化子窗口通信
const childCommunicator = new ChildWindowCommunicator();

// 绑定事件
document.getElementById('requestData').addEventListener('click', () => {
childCommunicator.requestData();
});

document.getElementById('closeWindow').addEventListener('click', () => {
childCommunicator.requestClose();
});

2. iframe跨域通信

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 主页面代码
class IframeCommunicator {
constructor(iframeId, targetOrigin) {
this.iframe = document.getElementById(iframeId);
this.targetOrigin = targetOrigin;
this.setupMessageListener();
}

// 发送消息到iframe
sendMessageToIframe(message) {
if (this.iframe && this.iframe.contentWindow) {
this.iframe.contentWindow.postMessage(message, this.targetOrigin);
}
}

// 监听来自iframe的消息
setupMessageListener() {
window.addEventListener('message', (event) => {
if (event.origin !== this.targetOrigin) {
return;
}

console.log('Received message from iframe:', event.data);

switch (event.data.type) {
case 'IFRAME_LOADED':
this.handleIframeLoaded();
break;
case 'HEIGHT_CHANGE':
this.handleHeightChange(event.data.height);
break;
case 'NAVIGATION':
this.handleNavigation(event.data.url);
break;
}
});
}

handleIframeLoaded() {
console.log('Iframe loaded successfully');
this.sendMessageToIframe({
type: 'INIT_CONFIG',
data: {
theme: 'light',
apiKey: 'your-api-key'
}
});
}

handleHeightChange(height) {
// 动态调整iframe高度
this.iframe.style.height = height + 'px';
}

handleNavigation(url) {
console.log('Iframe navigated to:', url);
// 可以在这里更新主页面的状态
}
}

// 使用示例
const iframeCommunicator = new IframeCommunicator('myIframe', 'https://iframe-domain.com');
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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<!-- iframe页面代码 -->
<!DOCTYPE html>
<html>
<head>
<title>Iframe Page</title>
</head>
<body>
<div id="content">
<h1>Iframe Content</h1>
<button id="changeHeight">Change Height</button>
<button id="navigate">Navigate</button>
</div>

<script>
class IframeChild {
constructor() {
this.parentOrigin = 'https://parent-domain.com';
this.setupMessageListener();
this.notifyLoaded();
}

notifyLoaded() {
this.sendMessageToParent({
type: 'IFRAME_LOADED'
});
}

sendMessageToParent(message) {
window.parent.postMessage(message, this.parentOrigin);
}

setupMessageListener() {
window.addEventListener('message', (event) => {
if (event.origin !== this.parentOrigin) {
return;
}

switch (event.data.type) {
case 'INIT_CONFIG':
this.handleConfig(event.data.data);
break;
}
});
}

handleConfig(config) {
document.body.className = config.theme;
// 使用API密钥等配置
}

changeHeight() {
const newHeight = Math.random() * 300 + 200;
document.getElementById('content').style.height = newHeight + 'px';

this.sendMessageToParent({
type: 'HEIGHT_CHANGE',
height: newHeight
});
}

navigate() {
this.sendMessageToParent({
type: 'NAVIGATION',
url: window.location.href
});
}
}

const iframeChild = new IframeChild();

document.getElementById('changeHeight').addEventListener('click', () => {
iframeChild.changeHeight();
});

document.getElementById('navigate').addEventListener('click', () => {
iframeChild.navigate();
});
</script>
</body>
</html>

(二)WebSocket跨域通信

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// WebSocket跨域通信示例
class CrossOriginWebSocket {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.reconnectInterval = 1000;
this.messageQueue = [];
}

connect() {
try {
this.ws = new WebSocket(this.url);
this.setupEventHandlers();
} catch (error) {
console.error('WebSocket connection failed:', error);
this.handleReconnect();
}
}

setupEventHandlers() {
this.ws.onopen = (event) => {
console.log('WebSocket connected');
this.reconnectAttempts = 0;

// 发送队列中的消息
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
this.ws.send(JSON.stringify(message));
}
};

this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
this.handleMessage(data);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};

this.ws.onclose = (event) => {
console.log('WebSocket disconnected:', event.code, event.reason);
this.handleReconnect();
};

this.ws.onerror = (error) => {
console.error('WebSocket error:', error);
};
}

handleMessage(data) {
switch (data.type) {
case 'PING':
this.send({ type: 'PONG' });
break;
case 'DATA':
this.onDataReceived(data.payload);
break;
case 'ERROR':
this.onErrorReceived(data.error);
break;
default:
console.log('Unknown message type:', data.type);
}
}

send(message) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(message));
} else {
// 连接未就绪,加入队列
this.messageQueue.push(message);

if (!this.ws || this.ws.readyState === WebSocket.CLOSED) {
this.connect();
}
}
}

handleReconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
this.reconnectAttempts++;
console.log(`Attempting to reconnect (${this.reconnectAttempts}/${this.maxReconnectAttempts})`);

setTimeout(() => {
this.connect();
}, this.reconnectInterval * this.reconnectAttempts);
} else {
console.error('Max reconnection attempts reached');
}
}

onDataReceived(data) {
// 处理接收到的数据
console.log('Received data:', data);
}

onErrorReceived(error) {
// 处理错误
console.error('Received error:', error);
}

close() {
if (this.ws) {
this.ws.close();
}
}
}

// 使用示例
const wsClient = new CrossOriginWebSocket('wss://api.external-service.com/ws');
wsClient.connect();

// 发送消息
wsClient.send({
type: 'SUBSCRIBE',
channel: 'user-updates'
});

七、跨域解决方案对比与选择

(一)解决方案对比表

解决方案适用场景优点缺点浏览器支持
CORS现代Web应用标准化、安全、功能完整需要服务端支持IE10+
JSONP简单数据获取兼容性好、实现简单只支持GET、安全风险所有浏览器
代理服务器开发环境、生产环境透明、无需客户端修改增加服务器复杂度不依赖浏览器
PostMessage窗口间通信安全、灵活只适用于窗口通信IE8+
WebSocket实时通信双向通信、实时性好复杂度高IE10+

(二)选择建议

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// 跨域解决方案选择器
class CrossOriginSolutionSelector {
constructor() {
this.scenarios = {
modernWebApp: {
solution: 'CORS',
reason: '标准化、安全、功能完整',
implementation: 'server-side CORS headers'
},
legacySupport: {
solution: 'JSONP',
reason: '兼容老旧浏览器',
implementation: 'callback-based requests'
},
development: {
solution: 'Proxy',
reason: '开发环境透明代理',
implementation: 'webpack-dev-server proxy'
},
production: {
solution: 'Nginx Proxy',
reason: '生产环境反向代理',
implementation: 'nginx reverse proxy'
},
windowCommunication: {
solution: 'PostMessage',
reason: '窗口间安全通信',
implementation: 'window.postMessage API'
},
realTimeData: {
solution: 'WebSocket',
reason: '实时双向通信',
implementation: 'WebSocket connection'
}
};
}

recommend(requirements) {
const {
browserSupport,
requestTypes,
realTime,
security,
serverControl
} = requirements;

// 实时通信需求
if (realTime) {
return this.scenarios.realTimeData;
}

// 窗口间通信
if (requirements.windowCommunication) {
return this.scenarios.windowCommunication;
}

// 无服务端控制权
if (!serverControl) {
if (browserSupport === 'legacy') {
return this.scenarios.legacySupport;
} else {
return this.scenarios.development; // 使用代理
}
}

// 有服务端控制权
if (serverControl) {
if (security === 'high' && browserSupport === 'modern') {
return this.scenarios.modernWebApp;
} else if (browserSupport === 'legacy') {
return this.scenarios.legacySupport;
} else {
return this.scenarios.modernWebApp;
}
}

// 默认推荐CORS
return this.scenarios.modernWebApp;
}

// 生成实现建议
generateImplementationGuide(solution) {
const guides = {
'CORS': `
1. 服务端设置CORS头:
Access-Control-Allow-Origin: https://your-domain.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization

2. 客户端正常发送请求:
fetch('https://api.example.com/data')
`,
'JSONP': `
1. 服务端支持callback参数:
app.get('/api/data', (req, res) => {
const data = { message: 'Hello' };
res.send(\`\${req.query.callback}(\${JSON.stringify(data)});\`);
});

2. 客户端使用JSONP:
jsonp('https://api.example.com/data', callback);
`,
'Proxy': `
1. 开发环境配置代理:
// webpack.config.js
devServer: {
proxy: {
'/api': 'https://api.example.com'
}
}

2. 生产环境使用Nginx:
location /api/ {
proxy_pass https://api.example.com/;
}
`
};

return guides[solution] || '请参考相应文档';
}
}

// 使用示例
const selector = new CrossOriginSolutionSelector();

const recommendation = selector.recommend({
browserSupport: 'modern',
requestTypes: ['GET', 'POST'],
realTime: false,
security: 'high',
serverControl: true
});

console.log('推荐方案:', recommendation);
console.log('实现指南:', selector.generateImplementationGuide(recommendation.solution));

八、最佳实践与安全考虑

(一)CORS安全最佳实践

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// CORS安全配置示例
class SecureCORSConfig {
constructor() {
this.allowedOrigins = [
'https://trusted-domain.com',
'https://admin.trusted-domain.com'
];
this.allowedMethods = ['GET', 'POST', 'PUT', 'DELETE'];
this.allowedHeaders = ['Content-Type', 'Authorization', 'X-Requested-With'];
}

// 验证Origin
validateOrigin(origin) {
// 生产环境严格验证
if (process.env.NODE_ENV === 'production') {
return this.allowedOrigins.includes(origin);
}

// 开发环境允许localhost
if (process.env.NODE_ENV === 'development') {
const localhostPattern = /^https?:\/\/localhost(:\d+)?$/;
return this.allowedOrigins.includes(origin) || localhostPattern.test(origin);
}

return false;
}

// 生成CORS中间件
createCORSMiddleware() {
return (req, res, next) => {
const origin = req.headers.origin;

// 验证Origin
if (this.validateOrigin(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}

// 设置其他CORS头
res.setHeader('Access-Control-Allow-Methods', this.allowedMethods.join(', '));
res.setHeader('Access-Control-Allow-Headers', this.allowedHeaders.join(', '));
res.setHeader('Access-Control-Allow-Credentials', 'true');
res.setHeader('Access-Control-Max-Age', '86400');

// 处理预检请求
if (req.method === 'OPTIONS') {
res.status(204).end();
return;
}

next();
};
}

// 动态Origin验证
createDynamicOriginValidator() {
return (origin, callback) => {
// 从数据库或配置文件获取允许的域名
this.getAllowedOriginsFromDB()
.then(allowedOrigins => {
if (allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
})
.catch(error => {
callback(error);
});
};
}

async getAllowedOriginsFromDB() {
// 模拟从数据库获取允许的域名
return new Promise(resolve => {
setTimeout(() => {
resolve([
'https://trusted-domain.com',
'https://partner-domain.com'
]);
}, 100);
});
}
}

(二)跨域请求监控与日志

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
// 跨域请求监控
class CrossOriginMonitor {
constructor() {
this.requestLog = [];
this.suspiciousRequests = [];
this.rateLimiter = new Map();
}

// 记录跨域请求
logCrossOriginRequest(req, res, next) {
const origin = req.headers.origin;
const userAgent = req.headers['user-agent'];
const ip = req.ip || req.connection.remoteAddress;

const logEntry = {
timestamp: new Date().toISOString(),
origin,
method: req.method,
url: req.url,
userAgent,
ip,
headers: req.headers
};

this.requestLog.push(logEntry);

// 检测可疑请求
this.detectSuspiciousActivity(logEntry);

// 速率限制
if (this.checkRateLimit(ip, origin)) {
res.status(429).json({ error: 'Too many requests' });
return;
}

next();
}

// 检测可疑活动
detectSuspiciousActivity(logEntry) {
const { origin, ip, userAgent } = logEntry;

// 检测未知Origin
if (origin && !this.isKnownOrigin(origin)) {
this.suspiciousRequests.push({
...logEntry,
reason: 'Unknown origin',
severity: 'medium'
});
}

// 检测异常User-Agent
if (this.isAbnormalUserAgent(userAgent)) {
this.suspiciousRequests.push({
...logEntry,
reason: 'Abnormal user agent',
severity: 'low'
});
}

// 检测频繁请求
if (this.isHighFrequencyRequest(ip)) {
this.suspiciousRequests.push({
...logEntry,
reason: 'High frequency requests',
severity: 'high'
});
}
}

isKnownOrigin(origin) {
const knownOrigins = [
'https://trusted-domain.com',
'https://partner-domain.com',
/^https:\/\/.*\.trusted-domain\.com$/
];

return knownOrigins.some(known => {
if (typeof known === 'string') {
return known === origin;
} else if (known instanceof RegExp) {
return known.test(origin);
}
return false;
});
}

isAbnormalUserAgent(userAgent) {
// 检测爬虫或自动化工具
const suspiciousPatterns = [
/bot/i,
/crawler/i,
/spider/i,
/curl/i,
/wget/i
];

return suspiciousPatterns.some(pattern => pattern.test(userAgent));
}

checkRateLimit(ip, origin) {
const key = `${ip}:${origin}`;
const now = Date.now();
const windowMs = 60000; // 1分钟
const maxRequests = 100;

if (!this.rateLimiter.has(key)) {
this.rateLimiter.set(key, { count: 1, resetTime: now + windowMs });
return false;
}

const limiter = this.rateLimiter.get(key);

if (now > limiter.resetTime) {
limiter.count = 1;
limiter.resetTime = now + windowMs;
return false;
}

limiter.count++;
return limiter.count > maxRequests;
}

isHighFrequencyRequest(ip) {
const recentRequests = this.requestLog.filter(log => {
const timeDiff = Date.now() - new Date(log.timestamp).getTime();
return log.ip === ip && timeDiff < 60000; // 最近1分钟
});

return recentRequests.length > 50;
}

// 生成安全报告
generateSecurityReport() {
const now = new Date();
const last24Hours = new Date(now.getTime() - 24 * 60 * 60 * 1000);

const recentRequests = this.requestLog.filter(log =>
new Date(log.timestamp) > last24Hours
);

const recentSuspicious = this.suspiciousRequests.filter(log =>
new Date(log.timestamp) > last24Hours
);

return {
period: '24 hours',
totalRequests: recentRequests.length,
suspiciousRequests: recentSuspicious.length,
topOrigins: this.getTopOrigins(recentRequests),
suspiciousActivities: recentSuspicious,
recommendations: this.generateRecommendations(recentSuspicious)
};
}

getTopOrigins(requests) {
const originCounts = {};
requests.forEach(req => {
if (req.origin) {
originCounts[req.origin] = (originCounts[req.origin] || 0) + 1;
}
});

return Object.entries(originCounts)
.sort(([,a], [,b]) => b - a)
.slice(0, 10)
.map(([origin, count]) => ({ origin, count }));
}

generateRecommendations(suspiciousRequests) {
const recommendations = [];

const unknownOrigins = suspiciousRequests.filter(req =>
req.reason === 'Unknown origin'
);

if (unknownOrigins.length > 0) {
recommendations.push({
type: 'security',
message: `发现 ${unknownOrigins.length} 个未知来源的请求,建议审查Origin白名单`,
priority: 'medium'
});
}

const highFrequencyRequests = suspiciousRequests.filter(req =>
req.reason === 'High frequency requests'
);

if (highFrequencyRequests.length > 0) {
recommendations.push({
type: 'performance',
message: `发现 ${highFrequencyRequests.length} 个高频请求,建议加强速率限制`,
priority: 'high'
});
}

return recommendations;
}
}

// 使用示例
const monitor = new CrossOriginMonitor();

// 在Express中使用
app.use(monitor.logCrossOriginRequest.bind(monitor));

// 定期生成报告
setInterval(() => {
const report = monitor.generateSecurityReport();
console.log('Security Report:', JSON.stringify(report, null, 2));
}, 24 * 60 * 60 * 1000); // 每24小时

九、总结

(一)核心要点回顾

  1. 同源策略是浏览器的重要安全机制,限制不同源之间的资源访问
  2. CORS是现代Web应用的标准跨域解决方案,提供了安全、灵活的跨域访问控制
  3. JSONP适用于简单的跨域数据获取,但存在安全风险和功能限制
  4. 代理服务器是开发和生产环境的通用解决方案,对客户端透明
  5. PostMessage和WebSocket适用于特定的通信场景

(二)选择建议

  • 现代Web应用:优先选择CORS,配合适当的安全措施
  • 遗留系统支持:考虑JSONP,但要注意安全风险
  • 开发环境:使用构建工具的代理功能
  • 生产环境:使用Nginx等反向代理服务器
  • 实时通信:选择WebSocket或Server-Sent Events

(三)安全注意事项

  1. 严格验证Origin:不要使用通配符*在生产环境中
  2. 最小权限原则:只允许必要的方法和头部
  3. 监控和日志:记录跨域请求,及时发现异常
  4. 定期审查:定期检查和更新跨域配置

跨域问题虽然复杂,但通过理解其本质和掌握各种解决方案,我们可以在保证安全的前提下实现灵活的跨域访问。选择合适的解决方案,并遵循安全最佳实践,是构建现代Web应用的重要技能。

十、参考资料

官方文档

相关文章

技术资源

学习资源