1. 跨域问题背景与核心原理
作为一名经历过多次前后端分离项目的老手,我深知跨域问题是每个开发者都会遇到的"拦路虎"。当Vue前端项目运行在http://localhost:8080,而ThinkPHP后端API服务部署在http://api.example.com时,浏览器出于安全考虑会阻止这种跨域请求。
跨域问题的本质是浏览器的同源策略(Same-Origin Policy)限制。这个策略要求:
- 协议相同(http/https)
- 域名相同
- 端口相同
任何一项不满足就会触发跨域限制。现代项目中前后端分离是常态,所以必须解决这个问题。常见的解决方案有:
- JSONP(已过时,仅支持GET)
- 前端代理(开发环境常用)
- 后端CORS(最规范的解决方案)
提示:CORS(Cross-Origin Resource Sharing)是W3C标准,通过在服务端设置响应头来告诉浏览器该请求允许跨域。
2. ThinkPHP中间件解决方案详解
2.1 中间件核心代码解析
我提供的MyCrossDomain中间件是经过多个项目验证的稳定方案。让我们拆解关键部分:
php复制protected $header = [
'Access-Control-Allow-Credentials' => 'true', // 允许携带凭证(如cookies)
'Access-Control-Max-Age' => 1800, // 预检请求缓存时间(秒)
'Access-Control-Allow-Methods' => 'GET, POST, PATCH, PUT, DELETE, OPTIONS', // 允许的HTTP方法
'Access-Control-Allow-Headers' => 'token,Content-Type...', // 允许的自定义请求头
];
这段配置定义了:
Allow-Credentials:当需要传输cookies时必须设为true,且Allow-Origin不能为*Max-Age:减少OPTIONS预检请求次数,提升性能Allow-Methods:根据实际需求调整,不要盲目开放所有方法Allow-Headers:必须包含前端可能发送的所有自定义header
2.2 动态Origin处理逻辑
php复制if (!isset($header['Access-Control-Allow-Origin'])) {
$origin = $request->header('origin');
if ($origin && ('' == $this->cookieDomain || str_contains($origin, $this->cookieDomain))) {
$header['Access-Control-Allow-Origin'] = $origin;
} else {
$header['Access-Control-Allow-Origin'] = '*';
}
}
这段代码实现了:
- 优先读取请求中的Origin头
- 检查是否匹配配置的cookie域名(安全考虑)
- 动态设置
Access-Control-Allow-Origin,比写死*更安全
注意:如果前端需要传cookie,这里不能返回*,必须返回具体的origin值
3. 完整配置与部署流程
3.1 中间件注册配置
在config/middleware.php中:
php复制return [
// 其他中间件...
\app\middleware\MyCrossDomain::class
];
建议的注册位置:
- 全局中间件:适合所有接口都需要跨域的情况
- 路由中间件:仅对特定路由开启跨域(更安全)
3.2 生产环境增强配置
对于正式环境,建议增加以下安全措施:
php复制protected $header = [
// 原有配置...
'X-Frame-Options' => 'DENY', // 防止点击劫持
'X-Content-Type-Options' => 'nosniff', // 禁止MIME嗅探
'X-XSS-Protection' => '1; mode=block' // XSS防护
];
4. 常见问题与解决方案
4.1 预检请求(OPTIONS)处理
当请求满足以下条件时,浏览器会先发送OPTIONS预检请求:
- 使用了PUT/DELETE等非简单方法
- 包含自定义Headers
- Content-Type不是
application/x-www-form-urlencoded,multipart/form-data或text/plain
解决方案:
- 确保中间件正确处理OPTIONS请求
- 在Nginx/Apache层直接处理OPTIONS(性能更优):
nginx复制location / {
if ($request_method = 'OPTIONS') {
add_header 'Access-Control-Allow-Origin' '*';
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With...';
add_header 'Access-Control-Max-Age' 1728000;
add_header 'Content-Type' 'text/plain; charset=utf-8';
add_header 'Content-Length' 0;
return 204;
}
}
4.2 带凭证(Credentials)的请求
当前端需要发送cookies时:
- 前端axios需要设置:
javascript复制axios.defaults.withCredentials = true
- 后端必须:
- 设置
Access-Control-Allow-Credentials: true Access-Control-Allow-Origin不能为*,必须是具体域名- 可能需要设置
SameSite=None; Secure的cookie属性
4.3 缓存控制问题
如果发现跨域设置不生效,可能是浏览器缓存了错误的CORS响应。解决方法:
- 开发时使用隐身模式
- 明确设置
Cache-Control: no-store头 - 给请求URL添加随机参数避免缓存
5. 性能优化建议
5.1 减少OPTIONS预检请求
- 适当增大
Access-Control-Max-Age(默认1800秒) - 尽量使用简单请求(GET/POST/HEAD + 简单Content-Type)
- 合并自定义Headers减少触发预检
5.2 Nginx层直接处理CORS
对于高并发场景,建议在Nginx配置中直接处理CORS,减少PHP处理开销:
nginx复制server {
location / {
add_header 'Access-Control-Allow-Origin' $http_origin;
add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
add_header 'Access-Control-Allow-Headers' 'Content-Type, Authorization';
add_header 'Access-Control-Allow-Credentials' 'true';
if ($request_method = 'OPTIONS') {
return 204;
}
}
}
6. 安全最佳实践
- 生产环境不要使用
Access-Control-Allow-Origin: * - 白名单验证Origin头:
php复制$allowedOrigins = [
'https://yourdomain.com',
'https://app.yourdomain.com'
];
if (in_array($origin, $allowedOrigins)) {
$header['Access-Control-Allow-Origin'] = $origin;
}
- 限制允许的HTTP方法
- 记录异常的Origin请求用于安全审计
7. 测试与验证方法
7.1 使用curl测试CORS
bash复制# 测试简单请求
curl -H "Origin: http://test.com" -I http://api.example.com/user
# 测试预检请求
curl -X OPTIONS -H "Origin: http://test.com" \
-H "Access-Control-Request-Method: PUT" \
-H "Access-Control-Request-Headers: content-type" \
-I http://api.example.com/user
检查响应头是否包含正确的CORS头信息。
7.2 浏览器开发者工具检查
- 查看Network面板请求的Response Headers
- 确认没有CORS相关的错误提示
- 对于复杂请求,确保OPTIONS预检请求返回204
8. 替代方案比较
8.1 前端代理方案(开发环境推荐)
在vue.config.js中配置:
javascript复制module.exports = {
devServer: {
proxy: {
'/api': {
target: 'http://api.example.com',
changeOrigin: true,
pathRewrite: {
'^/api': ''
}
}
}
}
}
优点:
- 开发时无需后端配合
- 避免CORS问题
缺点:
- 仅适用于开发环境
- 生产环境仍需后端配置
8.2 Nginx反向代理
统一域名下通过路径区分:
nginx复制server {
location /api {
proxy_pass http://backend-server;
}
location / {
root /path/to/frontend;
}
}
优点:
- 彻底避免跨域问题
- 统一域名便于管理
9. ThinkPHP6特别注意事项
- 中间件执行顺序很重要,CORS中间件应该尽可能早执行
- 对于文件上传接口,需要额外处理:
php复制'Access-Control-Allow-Headers' => 'Content-Type, Authorization, X-File-Name'
- 如果使用JWT等认证方式,确保暴露Authorization头:
php复制header('Access-Control-Expose-Headers', 'Authorization');
10. 历史问题与兼容性
- IE10/11对CORS的支持有限,可能需要特殊处理
- 老版本Android WebView的兼容性问题
- 某些浏览器扩展可能干扰CORS头
解决方案:
- 检测User-Agent提供降级方案
- 对于特殊环境考虑JSONP备用方案
- 明确告知用户使用现代浏览器
在实际项目中,这个中间件方案已经稳定运行了3年多时间,处理过各种复杂的跨域场景。关键是要理解CORS规范的本质,而不是简单地复制粘贴配置。每个项目的安全需求不同,需要根据实际情况调整配置参数。