同源策略:浏览器的安全基石
浏览器的同源策略(Same-Origin Policy)限制了不同源之间的资源交互,作为Web安全的第一道防线。当协议、域名或端口任一不同时,浏览器将阻止跨域请求,防止恶意网站窃取数据。
// 同源示例
// 当前页面URL: https://example.com/page.html
// ✅ 同源 - 相同协议、域名和端口
fetch('https://example.com/api/data')
// ❌ 非同源 - 不同协议
fetch('http://example.com/api/data')
// ❌ 非同源 - 不同域名
fetch('https://api.example.com/data')
// ❌ 非同源 - 不同端口
fetch('https://example.com:8080/api/data')
CORS:跨域资源共享机制
CORS(Cross-Origin Resource Sharing)是W3C标准,通过HTTP头部字段扩展同源策略,允许服务器声明哪些源可以访问其资源。
简单请求与预检请求
简单请求满足特定条件(如GET、HEAD或特定Content-Type的POST请求)时,浏览器直接发送带有Origin头的请求:
// 客户端发起简单请求
fetch('https://api.example.com/data', {
method: 'GET',
headers: {
'Content-Type': 'text/plain'
}
});
// 请求头包含
// Origin: https://example.com
服务器响应需包含允许跨域的头信息:
// 服务器响应头
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Credentials: true // 允许携带凭证
预检请求处理复杂情况,浏览器先发送OPTIONS请求确认服务器是否允许实际请求:
// 客户端发起包含自定义头的请求
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value'
},
body: JSON.stringify({ key: 'value' })
});
浏览器自动发送预检请求:
OPTIONS /data HTTP/1.1
Host: api.example.com
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, X-Custom-Header
服务器必须响应允许的方法和头部:
HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-Custom-Header
Access-Control-Max-Age: 86400 // 预检请求缓存时间(秒)
服务端实现CORS
Node.js (Express)实现
const express = require('express');
const app = express();
// CORS中间件
app.use((req, res, next) => {
// 允许特定源访问,或使用*允许所有源(不推荐)
res.header('Access-Control-Allow-Origin', 'https://example.com');
// 允许携带凭证
res.header('Access-Control-Allow-Credentials', 'true');
// 允许的请求方法
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
// 允许的请求头
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
// 预检请求缓存时间
res.header('Access-Control-Max-Age', '86400');
// 预检请求直接返回成功
if (req.method === 'OPTIONS') {
return res.status(204).end();
}
next();
});
// 或使用cors包
// const cors = require('cors');
// app.use(cors({
// origin: 'https://example.com',
// credentials: true,
// methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
// allowedHeaders: ['Origin', 'X-Requested-With', 'Content-Type', 'Accept', 'Authorization']
// }));
app.get('/api/data', (req, res) => {
res.json({ message: '这是跨域数据' });
});
app.listen(3000, () => {
console.log('服务器运行在 http://localhost:3000');
});
JSONP:历史遗留的跨域方案
在CORS出现前,JSONP利用<script>标签不受同源策略限制的特性实现跨域:
// 客户端JSONP实现
function jsonp(url, callback) {
const script = document.createElement('script');
const callbackName = 'jsonp_' + Math.random().toString(36).substr(2, 5);
// 将回调函数挂载到全局
window[callbackName] = function(data) {
callback(data);
document.body.removeChild(script);
delete window[callbackName];
};
// 添加回调参数
script.src = `${url}${url.includes('?') ? '&' : '?'}callback=${callbackName}`;
document.body.appendChild(script);
}
// 使用JSONP获取数据
jsonp('https://api.example.com/data', function(data) {
console.log('获取的数据:', data);
});
服务端实现:
// Node.js JSONP响应
app.get('/api/data', (req, res) => {
const data = { message: '这是JSONP返回的数据' };
const callback = req.query.callback || 'callback';
// 返回可执行的JavaScript代码
res.type('text/javascript');
res.send(`${callback}(${JSON.stringify(data)})`);
});
JSONP与CORS对比
| 特性 | JSONP | CORS |
|---|---|---|
| 支持的请求方法 | 仅GET | 所有HTTP方法 |
| 错误处理 | 有限 | 完善 |
| 安全性 | 较低,容易受XSS攻击 | 较高,有完整控制机制 |
| 实现复杂度 | 低 | 中等 |
| 现代应用推荐度 | 不推荐,仅作兼容 | 强烈推荐 |
跨域安全风险与防御策略
CSRF攻击与防御
跨站请求伪造(CSRF)利用用户已认证身份发起恶意请求:
<!-- 恶意网站的钓鱼链接 -->
<img src="https://bank.example.com/transfer?to=attacker&amount=1000" style="display:none">
防御策略:
- CSRF令牌:每个表单包含服务器生成的唯一令牌
// 前端实现
fetch('https://api.example.com/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
credentials: 'include',
body: JSON.stringify({ to: 'friend', amount: 100 })
});
// 服务端验证
function verifyCSRFToken(req, res, next) {
const token = req.headers['x-csrf-token'];
if (!token || !validateToken(token, req.session.csrfToken)) {
return res.status(403).json({ error: 'CSRF验证失败' });
}
next();
}
- SameSite Cookie:限制第三方网站使用Cookie
Set-Cookie: sessionId=abc123; SameSite=Strict; Secure; HttpOnly
- 验证Referer/Origin:检查请求来源
function checkReferer(req, res, next) {
const referer = req.headers.referer || req.headers.referrer;
if (!referer || !referer.startsWith('https://example.com')) {
return res.status(403).json({ error: '非法请求来源' });
}
next();
}
XSS与内容安全策略
对抗跨站脚本(XSS)攻击,通过内容安全策略(CSP)限制资源加载:
<!-- 在HTML中设置CSP -->
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' https://trusted.cdn.com; img-src 'self' https://trusted-images.com">
服务器设置:
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; connect-src 'self' https://api.example.com
生产环境中的最佳实践
合理配置CORS
- 精确控制允许的源:避免使用
*通配符
// 动态控制允许的源
const allowedOrigins = ['https://example.com', 'https://admin.example.com'];
app.use((req, res, next) => {
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
// ...其他CORS配置
next();
});
-
合理设置预检缓存时间:减少OPTIONS请求频率
-
优先使用子域而非完全不同域:简化CORS配置
代理服务器模式
通过同源服务器代理请求,完全规避跨域限制:
// 前端配置(Vue CLI)
// vue.config.js
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}
// 前端请求
fetch('/api/data') // 实际请求 https://api.example.com/data
Node.js后端代理:
const express = require('express');
const { createProxyMiddleware } = require('http-proxy-middleware');
const app = express();
app.use('/api', createProxyMiddleware({
target: 'https://api.example.com',
changeOrigin: true,
pathRewrite: {
'^/api': '',
},
}));
app.listen(3000);
边缘情况处理
-
复杂自定义请求头:确保预检响应正确配置
-
上传文件的跨域处理:文件上传常需特殊配置
// 前端文件上传
const formData = new FormData();
formData.append('file', document.querySelector('#fileInput').files[0]);
fetch('https://api.example.com/upload', {
method: 'POST',
credentials: 'include',
body: formData
});
// 服务端处理
// 确保响应包含正确的CORS头
app.post('/upload', (req, res) => {
// ...文件处理逻辑
res.header('Access-Control-Allow-Origin', 'https://example.com');
res.header('Access-Control-Allow-Credentials', 'true');
res.json({ success: true });
});
- WebSocket跨域:WebSocket也需遵循CORS
// WebSocket客户端
const socket = new WebSocket('wss://api.example.com/ws');
// WebSocket服务器(Node.js)
const WebSocket = require('ws');
const http = require('http');
const server = http.createServer();
const wss = new WebSocket.Server({ server });
wss.on('connection', function connection(ws, req) {
// 验证Origin
const origin = req.headers.origin;
if (!['https://example.com', 'https://admin.example.com'].includes(origin)) {
ws.close();
return;
}
ws.on('message', function message(data) {
console.log('received: %s', data);
});
});
server.listen(8080);
实战案例:单页应用与API交互
下面展示React应用调用不同源API的完整解决方案:
// React组件
import { useState, useEffect } from 'react';
function CrossOriginDemo() {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
// 获取CSRF令牌
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content;
async function fetchData() {
try {
const response = await fetch('https://api.example.com/data', {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
credentials: 'include' // 发送跨域Cookie
});
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
console.error('获取数据失败:', err);
} finally {
setLoading(false);
}
}
fetchData();
}, []);
// 数据提交
const handleSubmit = async (event) => {
event.preventDefault();
const formData = new FormData(event.target);
const payload = Object.fromEntries(formData.entries());
try {
const response = await fetch('https://api.example.com/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content
},
credentials: 'include',
body: JSON.stringify(payload)
});
if (!response.ok) {
throw new Error(`提交失败: ${response.status}`);
}
const result = await response.json();
alert('提交成功!');
} catch (err) {
alert(`错误: ${err.message}`);
}
};
if (loading) return <div>加载中...</div>;
if (error) return <div>错误: {error}</div>;
return (
<div className="cross-origin-demo">
<h2>跨域数据获取演示</h2>
<div className="data-display">
<pre>{JSON.stringify(data, null, 2)}</pre>
</div>
<form onSubmit={handleSubmit}>
<h3>提交数据</h3>
<div>
<label>
名称:
<input type="text" name="name" required />
</label>
</div>
<div>
<label>
消息:
<textarea name="message" required></textarea>
</label>
</div>
<button type="submit">提交</button>
</form>
</div>
);
}
export default CrossOriginDemo;
未来发展与浏览器趋势
随着Web平台安全要求提高,跨域安全机制不断演进:
-
Fetch Metadata请求头:提供更多请求背景信息
Sec-Fetch-Site: cross-site Sec-Fetch-Mode: cors Sec-Fetch-Dest: image -
隐私沙盒(Privacy Sandbox):第三方Cookie逐步淘汰后的替代方案
-
Cross-Origin-Opener-Policy (COOP)和Cross-Origin-Embedder-Policy (COEP):增强跨域隔离
总结
跨域资源共享是现代Web应用不可避免的挑战。合理配置CORS、代理服务器和安全防御措施,能有效平衡跨域需求与安全风险。前端工程师需全面理解这些机制,确保应用既可互通数据,又能抵御恶意攻击。随着技术演进,持续关注跨域安全最佳实践,将成为开发高质量Web应用的关键能力。
参考资源
官方文档和规范
- MDN Web Docs - 跨源资源共享(CORS)
- W3C Cross-Origin Resource Sharing规范
- MDN Web Docs - 同源策略
- MDN Web Docs - 内容安全策略(CSP)
- Fetch标准规范
- Cookie规范 - SameSite属性
开发者指南和教程
- Google Web Fundamentals - CORS指南
- web.dev - SameSite Cookie解释
- Authentication & Authorization 认证与授权最佳实践
- OWASP跨站请求伪造(CSRF)防御备忘单
- OWASP跨站脚本(XSS)防御备忘单
- Fetch Metadata请求头说明
框架和库文档
- Express CORS中间件文档
- Axios拦截器和CORS配置
- React中处理CORS问题的最佳实践
- ws: Node.js WebSocket库
- http-proxy-middleware文档
博客和社区文章
如果你觉得这篇文章有帮助,欢迎点赞收藏,也期待在评论区看到你的想法和建议!👇
终身学习,共同成长。
咱们下一期见
💻