← 返回博客首页

CORS 跨域资源共享配置完全指南

什么是同源策略?

同源策略(Same-Origin Policy,SOP)是浏览器最核心的安全机制之一。它规定:脚本只能访问与当前页面同源的资源。所谓"同源"需同时满足三个条件:

  • 协议(Scheme)相同:httpshttp 视为不同源
  • 域名(Host)相同:api.example.comexample.com 不同源
  • 端口(Port)相同::8080:3000 不同源

例如,页面 https://example.com/page 访问以下资源时的判定:

目标 URL 是否同源 原因
https://example.com/api 协议、域名、端口均相同
http://example.com/api 协议不同
https://api.example.com/data 域名不同(子域也视为不同源)
https://example.com:8080/api 端口不同

同源策略虽然提升了安全性,但也阻碍了合法的跨域请求——现代 Web 应用前后端分离、调用第三方 API 已成为常态。CORS 正是为解决这一矛盾而诞生的标准。

CORS 是什么?

CORS(Cross-Origin Resource Sharing,跨域资源共享,RFC 6454 / Fetch 标准)是一组 HTTP 头部机制,允许服务器声明哪些外部源可以通过浏览器访问其资源。它由浏览器在客户端执行,而非服务器

理解这一点至关重要:CORS 保护的是浏览器用户,而不是服务器。服务器始终会处理请求,只是浏览器会根据响应头决定是否把结果交给 JavaScript。

简单请求 vs 预检请求

CORS 将跨域请求分为两类,处理流程不同。

简单请求(Simple Request)

满足以下所有条件的请求会"直接发送",不触发预检:

  • 方法为 GETHEADPOST
  • 仅使用安全头部:AcceptAccept-LanguageContent-LanguageContent-Type
  • Content-Type 仅限:text/plainmultipart/form-dataapplication/x-www-form-urlencoded
  • 不使用 ReadableStream 对象
  • 请求中没有事件监听器注册到 XMLHttpRequestUpload

浏览器直接发出请求,服务器返回响应时必须包含 Access-Control-Allow-Origin 头部,否则浏览器拒绝把响应交给脚本。

预检请求(Preflight Request)

不满足简单请求条件的请求(典型场景:POST 携带 application/json、使用 PUT/DELETE 方法、自定义头部如 Authorization)会先触发一次 OPTIONS 预检请求。

预检流程:

浏览器                  服务器
  │                       │
  │── OPTIONS /api ──────▶│   (预检:询问是否允许)
  │                       │
  │◀─ 200 + CORS 头部 ────│
  │                       │
  │── POST /api ─────────▶│   (真实请求)
  │                       │
  │◀─ 201 + CORS 头部 ────│

预检请求会携带以下头部:

OPTIONS /api/users HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type, Authorization

核心响应头详解

Access-Control-Allow-Origin

最关键的头部,指定允许访问的源:

# 允许单个源
Access-Control-Allow-Origin: https://app.example.com

# 允许所有源(慎用,且不能与凭证一起使用)
Access-Control-Allow-Origin: *

注意:如果请求携带 Cookie(credentials: 'include'),则不能使用 *,必须明确指定具体源。

Access-Control-Allow-Methods

预检响应中声明允许的 HTTP 方法:

Access-Control-Allow-Methods: GET, POST, PUT, DELETE, PATCH, OPTIONS

Access-Control-Allow-Headers

预检响应中声明允许的请求头:

Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With

Access-Control-Expose-Headers

默认情况下,JavaScript 只能读取少数几个"安全"响应头(Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma)。若需暴露其他头部,用此声明:

Access-Control-Expose-Headers: X-Total-Count, X-Request-Id

Access-Control-Allow-Credentials

允许请求携带 Cookie、HTTP 认证或客户端 SSL 证书:

Access-Control-Allow-Credentials: true

前端也必须配合设置:

fetch('https://api.example.com/data', {
  credentials: 'include'  // 携带凭证
});

Access-Control-Max-Age

预检结果缓存时间(秒),避免每次都发 OPTIONS:

Access-Control-Max-Age: 86400  // 缓存一天

各浏览器有上限(Chrome 约 2 小时、Firefox 约 24 小时),超出部分会被截断。

常见配置方案

方案一:Nginx 反向代理(推荐)

前后端同源是最简单的解法。通过 Nginx 把 API 请求转发到后端:

server {
    listen 80;
    server_name example.com;

    location / {
        root /var/www/frontend;
        try_files $uri $uri/ /index.html;
    }

    location /api/ {
        proxy_pass http://backend:3000/;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

前端访问 /api/... 与页面同源,无需任何 CORS 配置

方案二:动态白名单(Node.js Express)

const express = require('express');
const cors = require('cors');

const app = express();

const whitelist = [
  'https://app.example.com',
  'https://admin.example.com'
];

const corsOptions = {
  origin: (origin, callback) => {
    // 允许白名单内,或非浏览器请求(origin 为 undefined,如 curl)
    if (!origin || whitelist.includes(origin)) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization'],
  credentials: true,
  maxAge: 86400
};

app.use(cors(corsOptions));

方案三:Spring Boot 全局配置

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/api/**")
                .allowedOrigins("https://app.example.com")
                .allowedMethods("GET", "POST", "PUT", "DELETE")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }
}

方案四:通配符子域

允许所有子域访问:

# 不能直接用通配符,需正则匹配
Access-Control-Allow-Origin: https://*.example.com

实际需要后端读取 Origin 头并用正则校验后回填:

const origin = req.headers.origin;
if (/^https:\/\/[a-z]+\.example\.com$/.test(origin)) {
  res.setHeader('Access-Control-Allow-Origin', origin);
}

调试 CORS 问题

第一步:看浏览器控制台

CORS 错误信息非常明确,常见提示:

  • No 'Access-Control-Allow-Origin' header is present:服务器没返回该头部
  • The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*' when credentials mode is 'include':凭证模式下用了 *
  • Method PUT not allowedAccess-Control-Allow-Methods 未包含 PUT

第二步:检查预检请求

打开 DevTools → Network,筛选 OPTIONS 请求,检查:

  1. 请求是否携带了 Access-Control-Request-MethodAccess-Control-Request-Headers
  2. 响应是否返回 200/204,并包含完整的 CORS 响应头
  3. Access-Control-Allow-Origin 是否匹配请求的 Origin

第三步:常见坑

  • 响应 200 但浏览器报错:CORS 检查发生在响应到达后,即使 HTTP 状态码是 200,浏览器仍会因头部缺失拦截响应。后端日志看不到失败,因为请求"成功"了。
  • Nginx 加了 add_header 但不生效add_header 默认只对 2xx/3xx 响应生效,4xx/5xx 不会加。需用 add_header ... always
  • 预检请求被认证中间件拦截:OPTIONS 请求不应要求认证,需在认证中间件前放行 OPTIONS。
  • CDN 缓存了错误头部:CDN 可能缓存第一个请求的 CORS 头部,导致后续不同源的请求拿到错误的 Access-Control-Allow-Origin。建议 CDN 按 Origin 头部分桶缓存,或加上 Vary: Origin

第四步:用 curl 模拟预检

curl -X OPTIONS https://api.example.com/api/users \
  -H "Origin: https://app.example.com" \
  -H "Access-Control-Request-Method: POST" \
  -H "Access-Control-Request-Headers: Content-Type, Authorization" \
  -i

检查响应头是否齐全。

安全注意事项

  1. 不要无脑使用 Access-Control-Allow-Origin: *,尤其是涉及凭证时。
  2. 生产环境一定要用白名单,明确列出允许的源。
  3. Access-Control-Allow-Credentials: true* 互斥,浏览器会拒绝。
  4. CORS 不能替代认证授权,它只控制浏览器能否读取响应,攻击者仍可从服务端发起请求。敏感操作必须配合 CSRF Token、SameSite Cookie。
  5. 注意 CORS 不会阻止 <img><script><link> 标签加载跨域资源——这些不受同源策略约束(但无法读取内容)。

总结

要点 说明
同源策略 浏览器安全基石,限制跨源脚本访问
CORS HTTP 头部机制,让服务器声明允许的跨域访问
简单请求 直接发送,响应需带 Allow-Origin
预检请求 OPTIONS 先行,复杂请求才触发
凭证模式 credentials: true 时不能用 *
推荐方案 Nginx 同源代理 > 后端白名单 > 通配符

掌握 CORS 的关键是理解:它由浏览器执行,靠响应头协商。配置时务必遵守最小权限原则,只放行真正需要的源、方法和头部。

← 返回博客首页