强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

HTTP 协议详解教程 / 第 10 章:跨域资源共享 CORS

第 10 章:跨域资源共享 CORS

跨域资源共享(CORS)是浏览器安全模型的重要组成部分。理解 CORS 的工作原理,是前后端分离开发的必备技能。


10.1 同源策略

什么是同源

两个 URL 的 协议、主机、端口 完全相同即为同源。

URL当前页面同源?原因
https://example.com/pagehttps://example.com/home同协议、主机、端口
https://example.com/pagehttp://example.com/page协议不同
https://example.com/pagehttps://api.example.com主机不同
https://example.com:443https://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 简单请求

条件

同时满足以下条件:

  1. 方法:GET、HEAD、POST
  2. 头部限制:
    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type(仅限以下值)
      • application/x-www-form-urlencoded
      • multipart/form-data
      • text/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)

// 客户端
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 supportedAllow-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'));
            }
        });
    }
};

⚠️ 注意事项

  1. 不要使用 Access-Control-Allow-Origin: * 配合凭据
  2. 预检请求有性能开销:使用 Access-Control-Max-Age 缓存
  3. CORS 仅浏览器强制:curl、Postman 不受 CORS 限制
  4. 不要在前端代理解决 CORS:应在服务端正确配置
  5. HTTPS 混合内容:HTTPS 页面不能请求 HTTP 资源

🔗 扩展阅读


下一章第 11 章:内容压缩 — gzip/br/zstd、Transfer-Encoding、配置优化