第 10 章:跨域资源共享 CORS
第 10 章:跨域资源共享 CORS
跨域资源共享(CORS)是浏览器安全模型的重要组成部分。理解 CORS 的工作原理,是前后端分离开发的必备技能。
10.1 同源策略
什么是同源
两个 URL 的 协议、主机、端口 完全相同即为同源。
| URL | 当前页面 | 同源? | 原因 |
|---|---|---|---|
https://example.com/page | https://example.com/home | ✓ | 同协议、主机、端口 |
https://example.com/page | http://example.com/page | ✗ | 协议不同 |
https://example.com/page | https://api.example.com | ✗ | 主机不同 |
https://example.com:443 | https://example.com:8080 | ✗ | 端口不同 |
同源策略限制
// 同源策略禁止以下操作:
// 1. 读取跨源响应
fetch('https://api.other.com/data'); // ❌ 被阻止
// 2. 访问跨源 DOM
document.querySelector('iframe').contentDocument; // ❌ 被阻止
// 3. 读取跨源 Cookie
// 不同域名的 Cookie 互相隔离
10.2 CORS 概述
CORS(Cross-Origin Resource Sharing)允许服务器声明哪些源可以访问其资源。
两种请求类型
| 类型 | 条件 | 是否预检 |
|---|---|---|
| 简单请求 | 特定方法 + 特定头部 | 否 |
| 预检请求 | 其他情况 | 是 |
10.3 简单请求
条件
同时满足以下条件:
- 方法:GET、HEAD、POST
- 头部限制:
- Accept
- Accept-Language
- Content-Language
- Content-Type(仅限以下值)
application/x-www-form-urlencodedmultipart/form-datatext/plain
流程
浏览器 服务器
│ │
│── GET /api/data ─────────────────→│
│ Origin: https://frontend.com │
│ │
│←── 200 OK ────────────────────── │
│ Access-Control-Allow-Origin: │
│ https://frontend.com │
服务端配置
// Express.js — 简单 CORS
const cors = require('cors');
// 允许所有来源(仅开发环境)
app.use(cors());
// 允许特定来源
app.use(cors({
origin: 'https://frontend.example.com',
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type']
}));
# Nginx CORS 配置
location /api/ {
# 允许的来源
add_header 'Access-Control-Allow-Origin' 'https://frontend.example.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type';
# 处理预检请求
if ($request_method = 'OPTIONS') {
return 204;
}
proxy_pass http://backend;
}
10.4 预检请求(Preflight)
触发条件
当请求不是简单请求时,浏览器会先发送 OPTIONS 预检请求:
- 方法不是 GET/HEAD/POST
- 包含自定义头部(Authorization 等)
- Content-Type 不是简单值
流程
浏览器 服务器
│ │
│── OPTIONS /api/data ─────────────→│ (预检请求)
│ Origin: https://frontend.com │
│ Access-Control-Request-Method: │
│ POST │
│ Access-Control-Request-Headers: │
│ Content-Type, Authorization │
│ │
│←── 204 No Content ────────────── │ (预检响应)
│ Access-Control-Allow-Origin: │
│ https://frontend.com │
│ Access-Control-Allow-Methods: │
│ GET, POST, PUT, DELETE │
│ Access-Control-Allow-Headers: │
│ Content-Type, Authorization │
│ Access-Control-Max-Age: 86400 │
│ │
│── POST /api/data ───────────────→│ (实际请求)
│ Content-Type: application/json │
│ Authorization: Bearer ... │
│ │
│←── 200 OK ────────────────────── │
完整配置
const cors = require('cors');
const corsOptions = {
origin: function(origin, callback) {
const allowedOrigins = [
'https://frontend.example.com',
'https://admin.example.com'
];
// 允许无 origin(如 curl、Postman)
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('不允许的跨域请求'));
}
},
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
allowedHeaders: [
'Content-Type',
'Authorization',
'X-Request-ID'
],
exposedHeaders: [
'X-Total-Count',
'X-Page-Count'
],
credentials: true, // 允许携带 Cookie
maxAge: 86400, // 预检缓存 24 小时
optionsSuccessStatus: 204
};
app.use(cors(corsOptions));
10.5 凭据(Credentials)
携带 Cookie
// 客户端
fetch('https://api.example.com/data', {
credentials: 'include', // 携带 Cookie
headers: {
'Content-Type': 'application/json'
}
});
# 服务端响应
Access-Control-Allow-Origin: https://frontend.example.com # 不能用 *
Access-Control-Allow-Credentials: true
📝 注意:使用凭据时,
Access-Control-Allow-Origin不能是*。
10.6 常见 CORS 错误
| 错误 | 原因 | 解决方案 |
|---|---|---|
No 'Access-Control-Allow-Origin' | 服务端未设置 CORS 头 | 添加 CORS 配置 |
Origin not allowed | 来源不在白名单 | 更新允许的来源列表 |
Method not allowed | 方法未在 Allow-Methods 中 | 添加所需方法 |
Header not allowed | 头部未在 Allow-Headers 中 | 添加所需头部 |
Credentials not supported | Allow-Origin 为 * | 改为具体来源 |
10.7 Nginx 完整 CORS 配置
server {
listen 443 ssl;
server_name api.example.com;
# CORS 配置
set $cors_origin "";
if ($http_origin ~* "^https://(app|admin)\.example\.com$") {
set $cors_origin $http_origin;
}
# 通用 CORS 头
add_header 'Access-Control-Allow-Origin' $cors_origin always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE, PATCH, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization' always;
add_header 'Access-Control-Expose-Headers' 'X-Total-Count,X-Page-Count' always;
add_header 'Access-Control-Max-Age' 86400 always;
# 预检请求处理
if ($request_method = 'OPTIONS') {
return 204;
}
location /api/ {
proxy_pass http://backend;
}
}
10.8 JSONP(已废弃)
JSONP 是 CORS 之前的跨域方案,利用 <script> 标签不受同源策略限制的特性。
<!-- JSONP 原理 -->
<script>
function handleData(data) {
console.log(data);
}
</script>
<script src="https://api.example.com/data?callback=handleData"></script>
<!-- 服务端返回 -->
<!-- handleData({"users": [...]}) -->
⚠️ 注意:JSONP 只支持 GET,存在 XSS 风险,已被 CORS 取代。
10.9 业务场景:微前端跨域
// 微前端架构中的 CORS 配置
const corsOptions = {
origin: [
'https://main-app.example.com',
'https://micro-app-1.example.com',
'https://micro-app-2.example.com'
],
credentials: true,
maxAge: 86400
};
// 或使用动态白名单
const corsOptions = {
origin: function(origin, callback) {
// 从数据库或配置服务读取允许的域名
getAllowedOrigins().then(origins => {
if (!origin || origins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('CORS not allowed'));
}
});
}
};
⚠️ 注意事项
- 不要使用
Access-Control-Allow-Origin: *配合凭据 - 预检请求有性能开销:使用
Access-Control-Max-Age缓存 - CORS 仅浏览器强制:curl、Postman 不受 CORS 限制
- 不要在前端代理解决 CORS:应在服务端正确配置
- HTTPS 混合内容:HTTPS 页面不能请求 HTTP 资源
🔗 扩展阅读
下一章:第 11 章:内容压缩 — gzip/br/zstd、Transfer-Encoding、配置优化