什么是同源策略?
同源策略(Same-Origin Policy,SOP)是浏览器最核心的安全机制之一。它规定:脚本只能访问与当前页面同源的资源。所谓"同源"需同时满足三个条件:
- 协议(Scheme)相同:
https与http视为不同源 - 域名(Host)相同:
api.example.com与example.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)
满足以下所有条件的请求会"直接发送",不触发预检:
- 方法为
GET、HEAD或POST - 仅使用安全头部:
Accept、Accept-Language、Content-Language、Content-Type Content-Type仅限:text/plain、multipart/form-data、application/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-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma)。若需暴露其他头部,用此声明:
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 allowed:Access-Control-Allow-Methods未包含PUT
第二步:检查预检请求
打开 DevTools → Network,筛选 OPTIONS 请求,检查:
- 请求是否携带了
Access-Control-Request-Method和Access-Control-Request-Headers - 响应是否返回 200/204,并包含完整的 CORS 响应头
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
检查响应头是否齐全。
安全注意事项
- 不要无脑使用
Access-Control-Allow-Origin: *,尤其是涉及凭证时。 - 生产环境一定要用白名单,明确列出允许的源。
Access-Control-Allow-Credentials: true与*互斥,浏览器会拒绝。- CORS 不能替代认证授权,它只控制浏览器能否读取响应,攻击者仍可从服务端发起请求。敏感操作必须配合 CSRF Token、SameSite Cookie。
- 注意 CORS 不会阻止
<img>、<script>、<link>标签加载跨域资源——这些不受同源策略约束(但无法读取内容)。
总结
| 要点 | 说明 |
|---|---|
| 同源策略 | 浏览器安全基石,限制跨源脚本访问 |
| CORS | HTTP 头部机制,让服务器声明允许的跨域访问 |
| 简单请求 | 直接发送,响应需带 Allow-Origin |
| 预检请求 | OPTIONS 先行,复杂请求才触发 |
| 凭证模式 | credentials: true 时不能用 * |
| 推荐方案 | Nginx 同源代理 > 后端白名单 > 通配符 |
掌握 CORS 的关键是理解:它由浏览器执行,靠响应头协商。配置时务必遵守最小权限原则,只放行真正需要的源、方法和头部。