引言
在现代Web开发中,安全始终是一个无法回避的核心议题。当我们构建单页应用(SPA)、微服务架构或前后端分离的项目时,跨域请求几乎无处不在。浏览器作为用户的第一道防线,内置了多种安全机制来保护用户数据不受恶意脚本的侵害。其中,同源策略(Same-Origin Policy, SOP)和跨域资源共享(Cross-Origin Resource Sharing, CORS)是最基石也是最重要的两个安全概念。
本文将从实际开发者的视角,深入剖析同源策略的设计原理、CORS的工作机制,并通过大量实战案例帮助你彻底理解这些概念,避免在日常开发中陷入安全陷阱。
一、同源策略(SOP):浏览器的安全基石
1.1 什么是"源"?
在Web安全中,"源"由三个要素组成:协议(Protocol)、域名(Domain)和端口(Port)。只有当两个URL的这三个要素完全一致时,它们才被认为是"同源"的。
| URL | 与 https://example.com:443/page 比较 | 原因 |
|---|---|---|
| https://example.com:443/other | ✅ 同源 | 协议、域名、端口完全一致 |
| http://example.com:80/page | ❌ 跨源 | 协议不同(https vs http) |
| https://api.example.com:443/page | ❌ 跨源 | 域名不同(子域名不同) |
| https://example.com:8443/page | ❌ 跨源 | 端口不同(443 vs 8443) |
| https://example.com/page2 | ✅ 同源 | 路径不同不影响同源判断 |
1.2 同源策略限制的行为
同源策略并非禁止所有跨源操作,而是精细地限制了以下三类行为:
- 数据访问:禁止跨源读取DOM、Cookie、LocalStorage、IndexedDB等数据
- 请求限制:对跨源的XMLHttpRequest和Fetch请求进行限制(尤其是可能产生副作用的写入操作)
- 资源嵌入:允许跨源的资源嵌入(如<img>、<script>、<link>标签),但限制脚本对嵌入内容的访问
1.3 为什么需要同源策略?
假设没有同源策略,攻击者可以轻易构造以下攻击场景:
- 你在浏览器中登录了银行网站 bank.com
- 你另一个标签页打开了恶意网站 evil.com
- evil.com 的JavaScript直接读取 bank.com 的DOM,获取你的账户余额
- evil.com 发起跨源AJAX请求,以你的身份转账
同源策略从根本上阻断了这类攻击,是Web安全的第一道防线。
二、CORS:合法的跨域之道
2.1 CORS的基本原理
CORS(跨域资源共享)是W3C标准,它允许服务器声明哪些外部源可以访问自己的资源。其核心思想是:通过HTTP头部信息,让服务器告诉浏览器"我信任这些源"。
2.2 简单请求 vs 预检请求
浏览器将跨源请求分为两类,采用不同的处理策略:
简单请求(Simple Request)
同时满足以下条件才是简单请求:
- 请求方法为 GET、HEAD 或 POST
- Content-Type 为 text/plain、multipart/form-data 或 application/x-www-form-urlencoded
- 没有设置任何自定义请求头
对于简单请求,浏览器直接发起请求,并附加Origin头部,然后检查响应中的Access-Control-Allow-Origin是否匹配。
预检请求(Preflight Request)
对于不满足简单请求条件的请求(如使用PUT/DELETE方法、Content-Type为application/json、携带自定义头等),浏览器会先发送一个OPTIONS方法的预检请求:
OPTIONS /api/users HTTP/1.1 Host: api.example.com Origin: https://frontend.example.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: Content-Type, X-Custom-Header
服务器需要回应以下头部来授权实际请求:
HTTP/1.1 204 No Content Access-Control-Allow-Origin: https://frontend.example.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE Access-Control-Allow-Headers: Content-Type, X-Custom-Header Access-Control-Max-Age: 86400 Access-Control-Allow-Credentials: true
2.3 关键CORS响应头详解
| 响应头 | 说明 | 注意事项 |
|---|---|---|
| Access-Control-Allow-Origin | 允许访问的源 | 值为*或具体域名,与Credentials互斥时不能用* |
| Access-Control-Allow-Methods | 允许的HTTP方法 | 预检请求的响应 |
| Access-Control-Allow-Headers | 允许的请求头 | 预检请求的响应 |
| Access-Control-Allow-Credentials | 是否允许携带凭证 | 值为true时,Allow-Origin不能为* |
| Access-Control-Max-Age | 预检请求缓存时间 | 单位为秒,减少重复预检的开销 |
| Access-Control-Expose-Headers | 允许前端访问的响应头 | 默认只有Cache-Control等安全头可见 |
三、实战:在主流框架中配置CORS
3.1 Node.js Express配置
const express = require('express');
const cors = require('cors');
const app = express();
// 方式1:允许所有源(开发环境)
app.use(cors());
// 方式2:精细控制(生产环境推荐)
app.use(cors({
origin: ['https://www.example.com', 'https://app.example.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
}));
// 方式3:动态白名单验证
const allowedOrigins = new Set([
'https://www.example.com',
'https://app.example.com'
]);
app.use(cors({
origin: (origin, callback) => {
// 允许无origin的请求(如移动端、Postman)
if (!origin || allowedOrigins.has(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true
}));
3.2 Spring Boot配置
@Configuration
@EnableWebMvc
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://www.example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
// 或者使用注解方式
@RestController
@CrossOrigin(
origins = "https://www.example.com",
methods = {RequestMethod.GET, RequestMethod.POST},
allowedHeaders = "*",
allowCredentials = "true"
)
@RequestMapping("/api")
public class ApiController {
// ...
}
3.3 Nginx反向代理
location /api/ {
# 处理预检请求
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '$http_origin';
add_header 'Access-Control-Allow-Methods' 'GET,POST,PUT,DELETE,OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type,Authorization';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Length' 0;
return 204;
}
add_header 'Access-Control-Allow-Origin' '$http_origin' always;
add_header 'Access-Control-Allow-Credentials' 'true' always;
proxy_pass http://backend:8080;
}
四、常见CORS错误与排错指南
4.1 错误信息速查
| 浏览器控制台错误 | 常见原因 | 解决方案 |
|---|---|---|
| No 'Access-Control-Allow-Origin' header | 服务器未配置CORS或配置错误 | 检查服务器端CORS配置是否正确返回A头部 |
| The value of the 'Access-Control-Allow-Origin' header must not be the wildcard '*' when credentials flag is set | 带credentials时使用了*通配符 | 使用具体域名白名单,不要使用* |
| Method PUT is not allowed | 预检请求未允许PUT方法 | 在Allow-Methods中添加PUT |
| Request header field X-Custom is not allowed | 自定义头未在Allow-Headers中声明 | 在Allow-Headers中添加对应头部 |
| Preflight response is not successful | 预检请求返回非2xx状态码 | 确保OPTIONS请求返回200/204 |
4.2 排错技巧
- 使用Network面板:在Chrome DevTools中勾选"Preserve log",观察OPTIONS预检请求的请求头和响应头
- curl模拟预检:使用
curl -X OPTIONS -H "Origin: https://frontend.com" -H "Access-Control-Request-Method: PUT" -I https://api.com/endpoint快速测试 - 检查代理层:确认Nginx/CDN层是否会过滤或覆盖CORS头部
- 日志追踪:在服务端添加CORS相关的日志输出,确认中间件正确处理了请求
五、安全最佳实践
5.1 严格的白名单策略
永远不要使用Access-Control-Allow-Origin: *来信任所有源,尤其是在需要携带Cookie或Authorization头部的场景中。应该维护一个严格的域名白名单。
5.2 验证Origin而非反射
不要简单地将请求的Origin反射到响应头中,这样等同于允许所有来源。应该通过白名单验证:
// ❌ 危险:反射Origin等于允许所有来源
response.headers['Access-Control-Allow-Origin'] = request.headers['Origin']
// ✅ 安全:白名单验证
if (allowedOrigins.includes(request.headers['Origin'])) {
response.headers['Access-Control-Allow-Origin'] = request.headers['Origin']
}
5.3 配合其他安全措施
- 使用CSRF Token防止跨站请求伪造
- 设置SameSite=Strict/Lax的Cookie属性
- 实施CSP(内容安全策略)限制脚本执行
- 敏感操作增加二次验证
- 实施Rate Limiting防止暴力攻击
六、其他跨域方案对比
| 方案 | 原理 | 优点 | 缺点 |
|---|---|---|---|
| CORS | HTTP头部声明权限 | 标准方案,支持所有请求类型 | 需要服务端支持,老浏览器不兼容 |
| JSONP | 利用script标签无跨源限制 | 兼容老浏览器 | 仅GET请求,存在XSS风险,已被废弃 |
| 反向代理 | 前后端同域,Nginx转发 | 无需修改后端,对前端透明 | 增加架构复杂度,运维成本 |
| WebSocket | 独立的通信协议,不受SOP限制 | 全双工实时通信 | 协议不同,需专门处理 |
| postMessage | 跨窗口/iframe消息传递 | 安全的跨源通信 | 仅限窗口间通信,需双方配合 |
总结
同源策略和CORS是Web安全体系中最基础也是最重要的部分。理解它们的原理,不仅能帮助我们正确配置跨域请求,更能让我们从根本上理解浏览器如何保护用户数据。在实际开发中,始终坚持"最小权限原则"——只开放必要的源、方法和头部,配合CSRF防护、CSP等多层安全策略,才能构建真正安全的Web应用。

发表评论 取消回复