1. 跨域问题的本质与产生场景
跨域(Cross-Origin Resource Sharing,简称CORS)是现代Web开发中最常见的安全限制机制之一。当我在2013年第一次遇到跨域问题时,控制台那个红色的"Access-Control-Allow-Origin"错误让我困惑了整整两天。本质上,跨域限制是浏览器实施的安全策略,它阻止一个源的JavaScript代码与另一个源的资源进行交互。
这种限制主要出现在以下典型场景:
- 前端部署在https://example.com,但需要调用https://api.example.com的接口
- 本地开发时(http://localhost:3000)访问测试环境API(https://test-api.com)
- 使用CDN资源时,主站与CDN域名不一致
- 第三方服务集成(如支付、地图等SDK)
关键点:跨域限制是浏览器的行为,不是服务器的限制。通过Postman等工具直接请求接口能成功,但通过浏览器就会失败,这正是浏览器安全策略在起作用。
2. CORS工作原理深度解析
2.1 同源策略基础
同源策略要求"协议+域名+端口"三者完全相同。例如:
- https://example.com/index.html 与 https://example.com/api/users → 同源
- http://example.com 与 https://example.com → 不同源(协议不同)
- example.com 与 api.example.com → 不同源(域名不同)
- localhost:3000 与 localhost:8080 → 不同源(端口不同)
2.2 CORS核心机制
CORS通过HTTP头部来实现跨域控制,主要涉及以下头部:
| 请求头 | 响应头 | 作用 |
|---|---|---|
| Origin | Access-Control-Allow-Origin | 声明允许的源 |
| Access-Control-Request-Method | Access-Control-Allow-Methods | 声明允许的方法 |
| Access-Control-Request-Headers | Access-Control-Allow-Headers | 声明允许的头部 |
| - | Access-Control-Allow-Credentials | 是否允许携带凭证 |
| - | Access-Control-Max-Age | 预检请求缓存时间 |
2.3 简单请求与预检请求
根据请求的复杂性,CORS处理分为两种模式:
简单请求需同时满足:
- 方法为GET/HEAD/POST之一
- 仅含以下头部:
- Accept
- Accept-Language
- Content-Language
- Content-Type(仅限application/x-www-form-urlencoded、multipart/form-data、text/plain)
预检请求(Preflight)触发条件:
- 使用PUT/DELETE等非简单方法
- 包含自定义头部(如X-Auth-Token)
- Content-Type为application/json等非简单值
3. 服务端CORS配置实战
3.1 Node.js示例(Express)
const express = require('express'); const app = express(); // 基础CORS中间件 app.use((req, res, next) => { const allowedOrigins = ['https://example.com', 'http://localhost:3000']; const origin = req.headers.origin; if (allowedOrigins.includes(origin)) { res.setHeader('Access-Control-Allow-Origin', origin); } res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE'); res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization'); res.header('Access-Control-Allow-Credentials', 'true'); res.header('Access-Control-Max-Age', '86400'); if (req.method === 'OPTIONS') { return res.sendStatus(200); } next(); }); // 你的路由...3.2 Nginx配置
location /api/ { if ($http_origin ~* (https://example.com|http://localhost:3000)) { add_header 'Access-Control-Allow-Origin' '$http_origin'; add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization'; add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range'; add_header 'Access-Control-Max-Age' 1728000; add_header 'Access-Control-Allow-Credentials' 'true'; } if ($request_method = 'OPTIONS') { return 204; } proxy_pass http://backend; }3.3 Spring Boot配置
@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("https://example.com", "http://localhost:3000") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }4. 前端处理跨域的实用技巧
4.1 Fetch API的正确用法
// 带凭证的请求 fetch('https://api.example.com/data', { credentials: 'include', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` } }) .then(response => { if (!response.ok) throw new Error('Network response was not ok'); return response.json(); }) .catch(error => { console.error('Fetch error:', error); });4.2 常见问题解决方案
- 开发环境代理配置(webpack/vite):
// vite.config.js export default { server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: path => path.replace(/^\/api/, '') } } } }- 图片跨域处理:
<img crossorigin="anonymous" src="https://cdn.example.com/image.jpg">- 字体跨域问题:
@font-face { font-family: 'MyFont'; src: url('https://cdn.example.com/font.woff2') format('woff2'); font-display: swap; }5. 高级场景与疑难排查
5.1 携带Cookie的跨域请求
必须满足三个条件:
- 服务端设置
Access-Control-Allow-Credentials: true Access-Control-Allow-Origin不能为*,必须明确指定域名- 前端设置
credentials: 'include'
5.2 预检请求缓存优化
通过设置Access-Control-Max-Age可以减少预检请求次数:
Access-Control-Max-Age: 86400 # 缓存1天5.3 常见错误排查
| 错误信息 | 可能原因 | 解决方案 |
|---|---|---|
| Missing CORS header | 服务端未返回CORS头 | 检查服务端中间件 |
| Credential not supported | 使用通配符*但需要凭证 | 指定具体域名 |
| Preflight channel failed | 预检请求未返回204 | 确保OPTIONS请求正确处理 |
| Invalid CORS request | 请求头包含非法字符 | 检查自定义头格式 |
6. 替代方案与安全考量
当无法修改服务端配置时,可考虑:
- JSONP(仅限GET请求):
function handleResponse(data) { console.log('Received:', data); } const script = document.createElement('script'); script.src = 'https://api.example.com/data?callback=handleResponse'; document.body.appendChild(script);- 反向代理:
location /external-api/ { proxy_pass https://target-api.com/; proxy_set_header Host target-api.com; proxy_set_header X-Real-IP $remote_addr; }安全注意事项:
- 不要随意设置
Access-Control-Allow-Origin: * - 敏感操作需要验证Origin头防止CSRF
- 生产环境应限制允许的HTTP方法
- 定期审查CORS策略,避免过度宽松
在实际项目中,我遇到过一个典型案例:某电商网站因为CDN域名未加入CORS白名单,导致所有字体文件加载失败。通过Chrome开发者工具的Network面板,可以清晰看到被拦截的请求和具体的CORS错误信息,这种可视化调试方式极大提高了排查效率。