"为什么我的前端代码在本地运行得好好的,一上线就报CORS错误?"——这是无数开发者首次遭遇跨域问题时共同的困惑。上周我帮团队排查一个诡异的生产环境Bug:前端Vue应用在Chrome中向Java后端发送带Bearer Token的API请求时,控制台突然爆出红色错误。而用Postman测试接口却完全正常。这个看似简单的现象背后,隐藏着浏览器安全机制与开发者认知之间的鸿沟。
2005年,当XMLHttpRequest被纳入IE7实现时,微软工程师可能没想到这个对象会引发后续如此多的跨域问题。现代浏览器执行的同源策略(Same-Origin Policy)就像小区门禁——默认只允许与当前页面同协议、同域名、同端口的请求自由通行。举个例子:
https://api.example.com/data 请求 https://api.example.com/info ✅ 同源http://shop.example.com 请求 https://shop.example.com ❌ 协议不同https://app.example.com 请求 https://api.example.com ❌ 域名不同这种设计源于真实的安全威胁。假设没有同源策略:
下表展示了同源策略限制的主要场景:
| 操作类型 | 跨域限制 | 典型场景 |
|---|---|---|
| AJAX请求 | 默认禁止 | 前端调用第三方API |
| Cookie/LocalStorage | 禁止读取 | 单点登录(SSO)实现 |
| DOM访问 | 禁止跨域iframe操作 | 嵌入第三方组件 |
| Web字体/图片 | 部分允许 | CDN资源加载 |
实际踩坑提示:即使子域名之间(如
app.example.com和api.example.com)也会触发同源限制。我曾见过团队花费两天排查的"神秘404",最终发现是www.前缀导致的跨域问题。
当浏览器发现跨域请求时,CORS(Cross-Origin Resource Sharing)机制就开始运作。不同于JSONP这种"曲线救国"的方案,CORS是W3C标准化的跨域解决方案。其核心在于服务端通过特定的HTTP头部告诉浏览器:"我允许哪些外部来源访问我的资源"。
符合以下条件的请求会被归类为简单请求(Simple Request):
javascript复制// 典型简单请求示例
fetch('https://api.example.com/data', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'key=value'
})
浏览器对简单请求的处理流程:
Origin头部表明来源Access-Control-Allow-Origin头部当我们的请求需要携带自定义头部或使用特殊方法时,事情就变得复杂了。比如开发中常见的场景:
javascript复制// 触发预检的非简单请求
fetch('https://api.example.com/user', {
method: 'PUT',
headers: {
'Authorization': 'Bearer xxxxx',
'Content-Type': 'application/json',
'X-Custom-Header': 'value'
},
body: JSON.stringify({name: 'John'})
})
此时浏览器会先发起OPTIONS预检请求(Preflight Request),其请求头包含:
code复制Access-Control-Request-Method: PUT
Access-Control-Request-Headers: authorization,content-type,x-custom-header
Origin: https://yourdomain.com
服务端必须正确响应这些头部才能通过预检:
code复制Access-Control-Allow-Origin: https://yourdomain.com
Access-Control-Allow-Methods: PUT
Access-Control-Allow-Headers: Authorization,Content-Type,X-Custom-Header
Access-Control-Max-Age: 86400
性能优化点:合理设置
Access-Control-Max-Age可以避免重复预检。在我们的电商项目中,设置为86400秒(24小时)后,API调用性能提升了15%。
以Express为例,典型的CORS中间件配置:
javascript复制const cors = require('cors');
app.use(cors({
origin: ['https://production.com', 'https://staging.com'],
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization'],
credentials: true,
maxAge: 86400
}));
Spring Boot中的等效配置:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("https://production.com", "https://staging.com")
.allowedMethods("*")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
当无法修改后端代码时,Nginx配置是最佳选择:
nginx复制server {
listen 80;
server_name api.example.com;
location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' 'https://web.example.com';
add_header 'Access-Control-Allow-Methods' 'GET, POST, PUT, DELETE';
add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type';
add_header 'Access-Control-Max-Age' 86400;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
add_header 'Access-Control-Allow-Origin' 'https://web.example.com';
add_header 'Access-Control-Allow-Credentials' 'true';
proxy_pass http://backend-service;
}
}
对于本地开发,可以配置更宽松的策略:
javascript复制// webpack-dev-server配置示例
module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://localhost:3000',
changeOrigin: true,
pathRewrite: { '^/api': '' }
}
}
}
}
或者在Chrome启动时临时禁用安全策略(仅限开发):
bash复制# MacOS
open -n -a "Google Chrome" --args --user-data-dir=/tmp/chrome --disable-web-security
当需要发送Cookie时,必须满足三个条件:
credentials: includejavascript复制fetch('https://api.example.com', {
credentials: 'include'
});
code复制Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://exact.domain.com # 不能是*
SameSite=None; Secure某次我们接入第三方API时遇到诡异问题:明明配置了CORS,但自定义头部仍然被拦截。原因在于:
错误配置:
code复制Access-Control-Allow-Headers: Authorization, Content-Type, X-Requested-With
正确配置:
code复制Access-Control-Allow-Headers: authorization,content-type,x-requested-with
在一次生产环境部署后,部分用户持续报CORS错误。最终发现:
解决方案是在修改CORS配置后,通过版本号强制刷新缓存:
code复制Access-Control-Max-Age: 86400
Vary: Origin
javascript复制// axios配置示例
const api = axios.create({
baseURL: process.env.API_BASE_URL,
withCredentials: true,
headers: {
'Content-Type': 'application/json'
}
});
// 请求拦截器处理授权
api.interceptors.request.use(config => {
const token = store.getters['auth/token'];
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
当使用Apollo Client时,需要特殊配置:
javascript复制const httpLink = createHttpLink({
uri: 'https://api.example.com/graphql',
credentials: 'include'
});
const client = new ApolloClient({
link: httpLink,
cache: new InMemoryCache()
});
即使配置了HTTP CORS,WebSocket连接仍需单独处理:
javascript复制const socket = new WebSocket('wss://api.example.com');
// 服务端需要检查Origin头部
const WebSocketServer = require('ws').Server;
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', function connection(ws, req) {
const origin = req.headers.origin;
if (!allowedOrigins.includes(origin)) {
return ws.close();
}
// ...处理正常连接
});
在微服务架构下,我们为每个前端应用维护了一个CORS配置中心,通过ETCD动态更新允许的源列表。当新功能上线时,CI/CD流水线会自动同步CORS配置到所有相关服务,避免了人工操作可能导致的遗漏。