技术复盘:从一次UAT环境CORS故障看配置冗余的危害与最佳实践
一、 事件概述
时间 : 近期UAT环境发布窗口
人物 : 全体开发及测试同学
问题 : 项目在本地开发环境联调完全正常,但发布至UAT环境后,前端应用无法与后端API正常通信,浏览器控制台抛出跨域错误(CORS error)。
耗时 : 约3小时
根本原因: 后端应用(Application)与网关层(Nginx)均配置了CORS策略,导致浏览器收到重复、冲突的CORS响应头,请求被浏览器拒绝。
二、 问题详述与技术原理分析
1. 什么是跨域(CORS)?
出于安全考虑,浏览器实施了同源策略 ,限制了从脚本内发起的跨源HTTP请求。而 CORS 是一种W3C标准,允许服务器通过一系列特定的HTTP响应头,明确告知浏览器允许哪些外部源来访问资源。
关键响应头包括:
Access-Control-Allow-Origin
: 允许访问的源(如*
或https://your-frontend.com
)Access-Control-Allow-Methods
: 允许的HTTP方法(如GET, POST, PUT
)Access-Control-Allow-Headers
: 允许的请求头Access-Control-Allow-Credentials
: 是否允许发送Cookies
2. 我们的配置与问题现象
在我们的项目中,存在两处CORS配置:
- 配置A(后端) : 在Spring Boot等后端框架中,我们通过
@CrossOrigin
注解或全局配置,添加了CORS头。这在本地开发时是必需的 ,因为前端localhost
地址访问后端localhost:8080
属于跨域。 - 配置B(网关) : 在UAT环境的Nginx配置文件中,我们也添加了
add_header
指令来处理CORS问题。
3. 根源剖析:重复的响应头
问题就出在两次配置上。一个API请求的完整生命周期如下:
- 浏览器请求
https://uat-api.example.com/api/v1/user
- 请求首先到达Nginx。
- Nginx处理完毕,添加第一套CORS头,然后将请求代理给后端应用。
- 后端应用处理业务逻辑,再次添加第二套完全相同的CORS头,然后响应给Nginx,最终返回给浏览器。
- 浏览器收到的响应头变为:
yaml
HTTP/1.1 200 OK
Server: nginx/1.18.0
...
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Access-Control-Allow-Headers: *
Access-Control-Allow-Origin: * # 重复!
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS # 重复!
Access-Control-Allow-Headers: * # 重复!
- 浏览器发现
Access-Control-Allow-Origin
等关键CORS头出现了多次,认为服务器的CORS配置是非法且不明确的,出于安全考虑,直接拦截了响应,导致前端请求失败。
结论:CORS策略应该在请求链路的某一层被统一处理,多层同时处理反而会引发冲突。
三、 解决方案与修复过程
解决方案非常明确:只保留一处CORS配置,移除另一处。
根据我们的架构(Nginx作为网关入口),我们选择了保留Nginx层的CORS配置,移除后端应用的CORS配置。
原因如下:
- 关注点分离: Nginx作为网关/反向代理,其职责就包括处理通用流量策略,如SSL、路由、缓存、CORS等。让后端应用更专注于业务逻辑。
- 灵活性: 在Nginx中修改CORS策略(例如允许的源列表改变)无需重新构建和部署后端应用,只需 reload nginx 配置,更加灵活。
- 性能 : 对于
OPTIONS
预检请求,Nginx可以直接处理并返回,无需转发到后端应用,减少了不必要的性能开销。
修复步骤:
- 清理后端代码中的所有CORS配置(如移除
@CrossOrigin
、WebMvcConfigurer
中的全局配置等)。 - 确认Nginx配置中的CORS头添加正确无误。
- 重新构建后端应用并部署至UAT环境。
- 验证问题已解决。
四、 经验教训与最佳实践
这次事件给我们带来了宝贵的经验,为避免类似问题,我们应遵循以下最佳实践:
- 环境差异意识 : 深刻理解开发环境 、测试环境 、生产环境的差异。本地能跑通不代表上线就成功。环境配置(尤其是网络、网关、安全策略)是至关重要的一环。
- CORS配置唯一性原则 : 在整个请求链路中(浏览器 -> CDN -> 网关 -> 后端服务),只应在某一层处理CORS。通常是网关层(Nginx/Kong)或后端最外层入口(Spring Boot Filter),切忌多层配置。
- 本地开发代理的正确使用:
-
- 前端框架 : 充分利用
webpack-dev-server
或vite
的proxy
功能。它会在本地启动一个代理服务器,将/api
的请求转发到后端服务,从而完美规避浏览器的跨域限制。 - 配置示例 (vite.config.js):
- 前端框架 : 充分利用
php
export default defineConfig({
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
// rewrite: (path) => path.replace(/^/api/, '')
}
}
}
})
-
- 这样做的巨大好处 : 后端同学在本地开发时,完全无需开启CORS。这从根源上避免了将仅为开发便利的配置不小心带入生产环境的风险。
- 配置清单化: 将环境配置(如CORS、数据库连接、第三方密钥等)进行清单化管理,明确记录每个环境各项配置的负责人和值,在发布前进行核对。
五、 总结
本次UAT环境发布故障,表面上是"跨域配置冲突"的技术问题,深层次则反映了我们对环境差异 和配置职责边界的认识不足。
幸运的是,我们通过团队协作成功定位并解决了问题。希望本次复盘能让我们整个团队引以为戒,建立起更规范的开发、配置和部署流程,让未来的发布之路更加顺畅。