Appearance
CORS 策略
跨源资源共享(Cross-Origin Resource Sharing,CORS)是一种机制,它使用额外的 HTTP 头部来告诉浏览器,允许一个域上的 Web 应用程序访问另一个域上的资源。
CORS 基础概念
什么是同源策略
同源策略是浏览器的一种安全机制,它限制一个源(协议 + 域名 + 端口)的文档或脚本与另一个源的资源进行交互。
同源示例:
https://example.com与https://example.com/page- 同源https://example.com:8080与https://example.com- 不同源(端口不同)
什么时候会发生跨源请求
- 从
https://site-a.com向https://site-b.com发起 AJAX 请求 - 从
https://example.com加载来自https://cdn.example.com的资源 - 嵌入不同源的 iframe
CORS 请求类型
简单请求
满足以下条件的请求被视为简单请求:
使用以下方法之一:
- GET
- POST
- HEAD
HTTP 头部仅包含:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type(值为 application/x-www-form-urlencoded、multipart/form-data 或 text/plain)
预检请求
对于不满足简单请求条件的请求,浏览器会先发送预检请求(OPTIONS 请求):
javascript
// 这样的请求会触发预检
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Custom-Header': 'value'
},
body: JSON.stringify({data: 'example'})
});
预检请求的示例:
OPTIONS /data HTTP/1.1
Host: api.example.com
Origin: https://example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-Custom-Header
带凭证的请求
使用 credentials: 'include' 时的特殊处理:
javascript
fetch('https://api.example.com/data', {
credentials: 'include', // 包含 cookies
headers: {
'Content-Type': 'application/json'
}
});
CORS 响应头部
Access-Control-Allow-Origin
指定哪些源可以访问资源:
Access-Control-Allow-Origin: https://example.com
Access-Control-Allow-Origin: *
注意: 当使用 Access-Control-Allow-Credentials: true 时,Access-Control-Allow-Origin 不能设为 *。
Access-Control-Allow-Methods
指定允许的 HTTP 方法:
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers
指定允许的请求头部:
Access-Control-Allow-Headers: X-Custom-Header, Content-Type
Access-Control-Allow-Credentials
指定是否允许发送凭据(cookies):
Access-Control-Allow-Credentials: true
Access-Control-Max-Age
指定预检请求的缓存时间(秒):
Access-Control-Max-Age: 86400 // 24小时
Access-Control-Expose-Headers
指定哪些头部可以被响应访问:
Access-Control-Expose-Headers: X-Total-Count, X-My-Custom-Header
服务器端实现
Node.js (Express)
javascript
// 手动设置 CORS 头部
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', 'https://example.com');
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-Allow-Credentials', true);
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
});
// 或使用 cors 中间件
const cors = require('cors');
const corsOptions = {
origin: ['https://example.com', 'https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true
};
app.use(cors(corsOptions));
Apache 配置
在 .htaccess 或虚拟主机配置中:
apache
# 允许特定源
Header set Access-Control-Allow-Origin "https://example.com"
Header set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS"
Header set Access-Control-Allow-Headers "Content-Type, Authorization"
# 或允许所有源(不推荐用于生产环境)
Header set Access-Control-Allow-Origin "*"
Nginx 配置
nginx
location /api/ {
add_header 'Access-Control-Allow-Origin' 'https://example.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization';
add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
proxy_pass http://backend;
}
安全考虑
避免通配符
对于需要凭据的请求,不要使用通配符:
javascript
// 不安全 - 使用通配符和凭据
app.use((req, res) => {
res.header('Access-Control-Allow-Origin', '*'); // 不安全!
res.header('Access-Control-Allow-Credentials', true); // 与上面冲突
});
// 安全 - 指定特定源
app.use((req, res) => {
const origin = req.headers.origin;
if (['https://trusted-site.com', 'https://another-trusted.com'].includes(origin)) {
res.header('Access-Control-Allow-Origin', origin);
}
res.header('Access-Control-Allow-Credentials', true);
});
验证请求源
始终验证请求的来源:
javascript
function validateOrigin(req, res, next) {
const allowedOrigins = [
'https://example.com',
'https://app.example.com',
'https://staging.example.com'
];
const origin = req.headers.origin;
if (allowedOrigins.includes(origin)) {
res.setHeader('Access-Control-Allow-Origin', origin);
} else {
res.status(403).send('Forbidden');
return;
}
next();
}
限制暴露的头部
只暴露必要的响应头部:
javascript
// 不安全 - 暴露过多头部
res.header('Access-Control-Expose-Headers', '*');
// 安全 - 只暴露必要的头部
res.header('Access-Control-Expose-Headers', 'X-Total-Count, X-RateLimit-Remaining');
常见错误和解决方案
错误:CORS header 'Access-Control-Allow-Origin' does not match
原因: 同一请求返回了多个不同的 Origin 值
解决方案:
javascript
// 错误的方式
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Origin', 'https://example.com'); // 冲突!
// 正确的方式
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);
}
错误:The value of the 'Access-Control-Allow-Origin' header in the response must not be '*' when the request's credentials mode is 'include'
解决方案:
javascript
// 不要使用通配符,指定具体源
res.header('Access-Control-Allow-Origin', 'https://example.com');
res.header('Access-Control-Allow-Credentials', true);
错误:Request header field X-Requested-With is not allowed by Access-Control-Allow-Headers
解决方案:
javascript
// 在响应中包含请求的头部
const requestedHeaders = req.headers['access-control-request-headers'];
res.header('Access-Control-Allow-Headers', requestedHeaders || 'Content-Type, Authorization');
最佳实践
- 明确指定允许的源 - 避免使用通配符,除非绝对必要
- 限制允许的方法 - 只允许必要的 HTTP 方法
- 验证请求头 - 只允许必要的请求头部
- 设置适当的缓存时间 - 避免不必要的预检请求
- 记录和监控 - 记录 CORS 相关的错误和异常
- 定期审查 - 定期检查 CORS 策略的有效性
调试 CORS 问题
浏览器开发者工具
在 Network 标签中查看请求和响应头部:
Request Headers:
- Origin: https://example.com
Response Headers:
- Access-Control-Allow-Origin: https://example.com
使用在线工具
- CORS Test Tool
- Postman(支持 CORS 模拟)
临时解决方案(仅用于开发)
javascript
// 开发环境的宽松 CORS 设置
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.header('Access-Control-Allow-Headers', '*');
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
});
注意: 上面的配置仅适用于开发环境,绝不能用于生产环境!
总结
CORS 是 Web 安全的重要组成部分,正确配置 CORS 策略可以保护应用程序免受跨站请求攻击,同时允许合法的跨域通信。关键是要平衡功能需求和安全要求,避免过度宽松的配置。