一、同源策略:浏览器安全的基石
1.1 什么是同源策略
同源策略(Same-Origin Policy)是现代浏览器最基本、最重要的安全策略之一。它规定了一个源(协议、域名、端口号完全相同的URL)的文档或脚本如何与另一个源的资源进行交互。当两个URL的协议、域名、端口号中有任何一项不同时,就属于不同的源,浏览器会默认阻止跨源请求。
同源判断示例:
https://www.example.com/index.html→https://www.example.com/api/data✅ 同源https://www.example.com/index.html→http://www.example.com/api/data❌ 协议不同https://www.example.com/index.html→https://api.example.com/data❌ 域名不同https://www.example.com/index.html→https://www.example.com:8080/data❌ 端口不同
1.2 同源策略的限制范围
同源策略主要限制以下三类行为:
- DOM访问限制:禁止不同源页面之间的DOM访问
- AJAX请求限制:禁止XMLHttpRequest或Fetch API发送跨域请求
- Cookie、LocalStorage、IndexedDB等存储限制:禁止访问不同源的存储数据
1.3 为什么需要同源策略
如果没有同源策略,恶意网站可能会:
- 通过iframe嵌入银行网站,读取用户的账户信息
- 发送AJAX请求到其他网站,使用用户的登录凭证进行操作
- 读取其他网站的本地存储数据,获取敏感信息
二、JSONP:早期的跨域解决方案
2.1 JSONP工作原理
JSONP(JSON with Padding)利用了<script>标签不受同源策略限制的特性。浏览器允许通过<script>标签加载任何来源的JavaScript文件。
核心原理:
- 前端创建一个
<script>标签,将请求URL作为src属性 - URL中包含一个回调函数名作为参数
- 服务器返回的不是JSON,而是JavaScript代码:
callbackFunction(jsonData) - 浏览器执行返回的JavaScript代码,调用前端定义的回调函数
2.2 完整实现示例
前端实现:
javascript
复制下载
function jsonp(url, callbackName) {
return new Promise((resolve, reject) => {
// 创建script标签
const script = document.createElement('script');
// 定义全局回调函数
window[callbackName] = (data) => {
resolve(data);
document.body.removeChild(script);
delete window[callbackName];
};
// 设置超时处理
script.timeout = 5000;
script.onerror = () => {
reject(new Error('JSONP request failed'));
document.body.removeChild(script);
delete window[callbackName];
};
// 设置src并添加到文档
script.src = `${url}${url.includes('?') ? '&' : '?'}callback=${callbackName}`;
document.body.appendChild(script);
});
}
// 使用示例
jsonp('https://api.example.com/data', 'handleData')
.then(data => console.log(data))
.catch(err => console.error(err));
服务器端实现(Node.js):
javascript
复制下载
const http = require('http');
const url = require('url');
const server = http.createServer((req, res) => {
const parsedUrl = url.parse(req.url, true);
if (parsedUrl.pathname === '/api/data') {
const callback = parsedUrl.query.callback;
const data = JSON.stringify({ message: 'Hello from JSONP' });
// 设置响应头
res.writeHead(200, {
'Content-Type': 'application/javascript'
});
// 返回JavaScript代码
res.end(`${callback}(${data})`);
}
});
server.listen(3000);
2.3 JSONP的优缺点分析
优点:
- 兼容性好,支持老旧浏览器
- 实现简单,无需服务器特殊配置
- 可以绕过CORS限制
缺点:
- 只支持GET请求,无法使用POST、PUT等HTTP方法
- 安全性问题:容易受到XSS攻击
- 错误处理困难:无法使用HTTP状态码进行错误处理
- 缺乏标准化:需要前后端约定回调函数名
三、CORS:现代跨域解决方案的标准
3.1 CORS工作原理
CORS(Cross-Origin Resource Sharing)是一个W3C标准,它允许服务器声明哪些源可以访问资源。当浏览器检测到跨域请求时,会自动添加Origin头部,服务器通过响应头决定是否允许该请求。
3.2 简单请求与预检请求
3.2.1 简单请求(Simple Request)
满足以下所有条件的请求为简单请求:
- 使用GET、HEAD、POST方法之一
- 仅包含以下头部:Accept、Accept-Language、Content-Language、Content-Type
- Content-Type为以下之一:text/plain、multipart/form-data、application/x-www-form-urlencoded
简单请求流程:
javascript
复制下载
// 浏览器自动添加Origin头部
GET /api/data HTTP/1.1
Origin: https://www.example.com
Host: api.example.com
// 服务器响应
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.example.com
Content-Type: application/json
{"data": "some data"}
3.2.2 预检请求(Preflight Request)
不满足简单请求条件的请求会先发送OPTIONS预检请求:
预检请求示例:
javascript
复制下载
// 预检请求
OPTIONS /api/data HTTP/1.1
Origin: https://www.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
// 预检响应
HTTP/1.1 200 OK
Access-Control-Allow-Origin: https://www.example.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 86400
// 实际请求
PUT /api/data HTTP/1.1
Origin: https://www.example.com
X-Custom-Header: value
3.3 完整的CORS配置
Node.js Express服务器配置:
javascript
复制下载
const express = require('express');
const app = express();
// CORS中间件
const corsMiddleware = (req, res, next) => {
// 允许的来源
const allowedOrigins = [
'https://www.example.com',
'https://admin.example.com'
];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
}
// 允许的HTTP方法
res.setHeader('Access-Control-Allow-Methods',
'GET, POST, PUT, DELETE, OPTIONS, PATCH');
// 允许的请求头
res.setHeader('Access-Control-Allow-Headers',
'Content-Type, Authorization, X-Requested-With, X-Custom-Header');
// 允许携带凭证(cookies、HTTP认证等)
res.setHeader('Access-Control-Allow-Credentials', 'true');
// 预检请求缓存时间(秒)
res.setHeader('Access-Control-Max-Age', '86400');
// 暴露给前端的响应头
res.setHeader('Access-Control-Expose-Headers',
'X-Total-Count, X-Custom-Header');
// 如果是预检请求,直接返回200
if (req.method === 'OPTIONS') {
return res.status(200).end();
}
next();
};
app.use(corsMiddleware);
// 路由示例
app.post('/api/login', (req, res) => {
// 处理登录逻辑
res.json({ success: true, token: 'jwt-token' });
});
app.listen(3000);
Spring Boot配置:
java
复制下载
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://www.example.com",
"https://admin.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE",
"OPTIONS", "PATCH")
.allowedHeaders("*")
.exposedHeaders("X-Total-Count", "X-Custom-Header")
.allowCredentials(true)
.maxAge(86400L);
}
}
3.4 带凭证的CORS请求
当请求需要携带cookies或HTTP认证信息时:
前端配置:
javascript
复制下载
// Fetch API
fetch('https://api.example.com/protected-data', {
method: 'GET',
credentials: 'include', // 必须设置为include
headers: {
'Content-Type': 'application/json'
}
});
// XMLHttpRequest
const xhr = new XMLHttpRequest();
xhr.withCredentials = true;
xhr.open('GET', 'https://api.example.com/protected-data');
xhr.send();
服务器端必须响应:
text
复制下载
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://www.example.com // 不能使用通配符*
四、服务器代理:开发环境的跨域解决方案
4.1 正向代理 vs 反向代理
正向代理:代理客户端向服务器发出请求,隐藏客户端身份
反向代理:代理服务器接收客户端请求,隐藏服务器身份
4.2 开发环境代理配置
Webpack Dev Server配置:
javascript
复制下载
// webpack.config.js
module.exports = {
// ...其他配置
devServer: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
secure: false,
pathRewrite: {
'^/api': '' // 移除路径中的/api前缀
},
onProxyReq: (proxyReq, req, res) => {
// 可以在这里修改请求头
proxyReq.setHeader('X-Forwarded-For', req.ip);
},
onProxyRes: (proxyRes, req, res) => {
// 可以在这里修改响应头
proxyRes.headers['X-Proxy'] = 'webpack-dev-server';
}
}
}
}
};
Node.js代理服务器实现:
javascript
复制下载
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
// 静态文件服务
app.use(express.static('public'));
// 代理配置
const apiProxy = createProxyMiddleware({
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: {
'^/proxy/api': '/api' // /proxy/api/users -> /api/users
},
onError: (err, req, res) => {
console.error('Proxy error:', err);
res.status(500).json({ error: 'Proxy error' });
},
// 自定义请求头
headers: {
'X-Proxy-Server': 'node-proxy'
}
});
app.use('/proxy/api', apiProxy);
// 健康检查端点
app.get('/health', (req, res) => {
res.json({ status: 'healthy' });
});
app.listen(8080, () => {
console.log('Proxy server running on http://localhost:8080');
});
4.3 Nginx反向代理配置
生产环境Nginx配置:
nginx
复制下载
server {
listen 80;
server_name www.example.com;
# 前端静态文件
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
# API代理配置
location /api/ {
# 反向代理到后端服务器
proxy_pass http://backend-server:3000/;
# CORS相关配置
add_header 'Access-Control-Allow-Origin' 'https://www.example.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
# 处理预检请求
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Type' 'text/plain charset=UTF-8';
add_header 'Content-Length' 0;
return 204;
}
# 代理配置
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;
# 超时设置
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
# WebSocket代理配置
location /ws/ {
proxy_pass http://backend-server:3000/ws/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
}
}
五、安全考虑与最佳实践
5.1 安全风险
- CSRF攻击:CORS不会防止CSRF攻击,需要配合CSRF Token
- 信息泄露:错误的CORS配置可能导致敏感信息泄露
- DNS重绑定攻击:攻击者可能利用DNS重绑定绕过同源策略
5.2 安全配置建议
javascript
复制下载
// 安全的CORS配置
const corsOptions = {
// 严格限制允许的来源
origin: (origin, callback) => {
const allowedOrigins = [
'https://www.example.com',
'https://app.example.com'
];
// 不允许没有origin的请求(如curl、postman)
if (!origin) return callback(new Error('Not allowed'), false);
if (allowedOrigins.indexOf(origin) === -1) {
return callback(new Error('Not allowed by CORS'), false);
}
return callback(null, true);
},
// 限制允许的方法
methods: ['GET', 'POST', 'PUT', 'DELETE'],
// 限制允许的头部
allowedHeaders: ['Content-Type', 'Authorization', 'X-Request-ID'],
// 限制暴露的头部
exposedHeaders: ['X-Total-Count'],
// 启用凭证
credentials: true,
// 设置预检缓存
maxAge: 86400,
// 设置预检响应状态码
optionsSuccessStatus: 204
};
5.3 生产环境最佳实践
- 使用白名单机制:不要使用
Access-Control-Allow-Origin: * - 限制HTTP方法:只允许必要的HTTP方法
- 实施速率限制:防止API被滥用
- 使用HTTPS:确保所有跨域请求都通过HTTPS
- 监控和日志:记录所有跨域请求,便于审计
- 定期安全审计:检查CORS配置是否符合安全要求
七、面试重点总结
7.1 必考知识点
-
同源策略的定义和目的
- 协议、域名、端口相同即为同源
- 主要目的是防止恶意网站窃取数据
-
JSONP的原理和局限性
- 利用
<script>标签不受同源策略限制 - 只支持GET请求,安全性较差
- 利用
-
CORS的工作机制
- 简单请求 vs 预检请求
- 服务器通过响应头控制访问权限
credentials: 'include'与Access-Control-Allow-Credentials
-
代理服务器的应用场景
- 开发环境解决跨域
- 生产环境的负载均衡和安全防护
7.2 常见面试题
- "CORS中简单请求和预检请求的区别是什么?"
- "如何实现安全的CORS配置?"
- "JSONP和CORS各有什么优缺点?"
- "什么情况下应该使用代理服务器?"
- "如何防止CORS配置不当导致的安全问题?"
7.3 实际应用建议
- 现代应用首选CORS:大多数场景下,CORS是最佳选择
- 遗留系统考虑JSONP:仅用于支持老旧浏览器
- 开发环境使用代理:方便前端开发调试
- 生产环境结合使用:CORS + 反向代理提供更好的安全性和性能
通过深入理解同源策略和各种跨域解决方案的原理、实现方式及安全考虑,开发者可以根据具体场景选择最合适的跨域方案,既保证功能实现,又确保应用安全。在现代Web开发中,掌握这些知识不仅是解决跨域问题的基础,更是构建安全、可靠Web应用的重要保障。