ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

前后端分离跨域问题全解析:从开发代理到生产CORS与Nginx反代

前后端分离跨域问题全解析:从开发代理到生产CORS与Nginx反代 前几天刚帮人处理了一个“本地好好的一上生产就跨域”的典型问题前端 Vue 打包后扔到 Nginx后端 Django 跑在 8000 端口页面打开后所有接口全红浏览器控制台齐刷刷一串 CORS 报错。这个问题在前后端分离项目里太常见了。很多人默认“开发环境怎么通的生产环境就应该怎么通”但跨域恰恰是最不能这么想当然的。开发环境之所以很少让你感受到跨域不是后端做了什么神级配置而是开发服务器在中间做了一层“偷梁换柱”生产环境没有这层中转真实用户的浏览器会直接面对协议、域名、端口之间的差异问题一下子就全暴露出来。要彻底理解这件事先记住一句话同源策略是浏览器的规则不是 HTTP 协议的规则。服务器和服务器之间从来不存在跨域只有浏览器在加载完页面后发现页面所在源和 XHR/fetch 请求的目标源不一致时才会按同源策略拦截响应结果。所谓同源指协议、域名、端口三者完全一致任何一项不同都属于跨域。理解了这一点开发环境和生产环境的处理方式为什么不同就成功了一半。1. 同一个项目本地好好的一部署就跨域1.1 开发环境其实“骗过了”浏览器本地开发时前端页面跑在http://localhost:5173Vite 默认或http://localhost:8080Webpack 默认后端接口跑在http://localhost:8000或http://localhost:8080之类。只要端口不一样浏览器就会判定为跨域。但你在本地几乎没有感知原因在于开发服务器替你做了代理。拿 Vue 3 Vite 举例配置通常是这样的// vite.config.js export default defineConfig({ server: { proxy: { /api: { target: http://localhost:8000, changeOrigin: true, }, }, }, });你的前端代码里请求的是/api/user/info浏览器看到的是它发给http://localhost:5173/api/user/info同源不会触发跨域拦截。然后 Vite 的开发服务器在 Node 层把这个请求转发到http://localhost:8000/api/user/info拿到结果再返回给浏览器。关键就在这里浏览器以为自己在访问同源地址真正跨域的请求发生在 Node 开发服务器与后端之间。而服务器与服务器之间没有同源策略所以怎么转发都不报错。开发代理本质上就是“替浏览器跑腿”把浏览器可能遇到的跨域问题提前消化在中间层。1.2 vue django 场景的典型事故流程用我最常被问到的 Vue Django 场景举个例子。本地开发时Vite 代理配好后前端页面请求/api/tasksDjango 后端在127.0.0.1:8000正常处理一切岁月静好。上线时把 Vuenpm run build打包成静态文件丢进 Nginx 的/usr/share/nginx/htmlDjango 跑在服务器上的 8000 端口Nginx 只配置了静态文件服务server { listen 80; server_name www.example.com; location / { root /usr/share/nginx/html; index index.html; } }这个部署形态下前端静态文件由 Nginx 提供浏览器加载页面后JS 里的请求还是指向/api/tasks。此时页面源是http://www.example.com请求目标也是http://www.example.com/api/tasks看起来是同源。但如果你的前端代码里写的是绝对地址比如http://server-ip:8000/api/tasks或者生产包里的环境变量VITE_API_BASE配成了http://server-ip:8000浏览器就会直接向 8000 端口发请求页面源与接口源立即不同跨域报错就出现了。另一个隐蔽情况是前端没用代理而是把请求写成了http://localhost:8000/api。本地开发时你人就在自己电脑上localhost 没问题部署到服务器后浏览器在用户电脑上执行 JS用户电脑上的 localhost 指向用户自己根本不可能访问到服务器的 Django 服务。这种“代码里写死 localhost”的问题属于开发环境骗过了你生产环境立刻还以颜色。2. 开发环境为什么要选代理而不是让后端直接放行2.1 dev server 代理到底干了什么很多人对代理的理解就是“配几行代码能用就行”实际上它有非常具体的行为。Vite/Webpack 的 dev server 代理在工作时收到浏览器请求/api/login后会把请求转发给配置的 target。changeOrigin: true的作用是改写请求头里的Host字段让后端认为请求来自 target 地址而不是来自localhost:5173。如果你不开启changeOrigin部分后端框架在校验Host头时会拒绝请求常见表现就是后端日志里收到请求但返回 403 或 400。还有个小坑后端拿不到浏览器的真实 IP。本地开发时Django 的日志里看到的客户端 IP 永远是127.0.0.1因为请求是 Vite 转发出去的。如果想看真实来源需要在代理层透传X-Forwarded-For但本地开发一般不值得折腾。生产环境的 Nginx 反代也会遇到类似问题。所以我在生产环境配置时会同时加上proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme;让后端能拿到原始域名、真实 IP 和协议。否则用户登录、审计日志、生成绝对链接这些功能都会出问题。2.2 开发环境不建议直接开 CORS 的三个理由有些后端同学图省事直接在开发环境给所有接口加上Access-Control-Allow-Origin: *或者用 Django 的django-cors-headers配置CORS_ALLOW_ALL_ORIGINS True。前端本地确实不报跨域了但我不推荐这个做法有实际教训。第一它会掩盖真正的跨域问题。本地不过是因为浏览器被允许了前端代码里写死请求地址的问题、把localhost硬编码进请求 URL 的问题全都发现不了。部署到生产环境才炸那时候排查成本高得多。第二开发环境的“宽松放行”很容易被复制到生产。人都倾向于把“本地能跑的配置”原样搬到线上如果开发配置是Allow-Origin: *生产环境很大概率也是*。对于一个需要用户凭证的项目这就是安全事故。第三开发代理配置成本几乎为零还能模拟生产环境“所有请求走统一网关”的形态。前后端联调时前端把路径写对后端不需要为跨域做任何特殊处理两边职责清晰。2.3 那些“临时绕过”浏览器的土办法浏览器的跨域拦截只是浏览器行为所以网上流传着一些“管理员绕行方案”比如 Chrome 加启动参数--disable-web-security或者安装 Allow CORS 之类的浏览器插件。这类方法只适合临时调试时看一眼页面效果不适合作为开发常态。我之前遇到过有同事长期开着一个关闭安全策略的 Chrome 用户目录结果某天排查问题接口明明 400 了页面上却不报跨域错误他以为是后端逻辑问题折腾了一下午。最后发现是浏览器关闭了同源校验把后端错误全吞了。这类工具会破坏浏览器对响应结果的正常展示反而掩盖了 HTTP 状态码和响应体里的业务错误。我的建议是日常开发必须用正常浏览器跨域问题交给 dev server 代理处理。如果非要临时调试跨域请求可以用 curl 加-H Origin: http://example.com手动模拟比关闭浏览器安全策略干净得多。3. 生产环境的世界完全不一样同源策略没变但架构变了3.1 生产环境的前后端到底是怎么部署的生产环境的架构比本地复杂得多。前端静态文件通常放在 Nginx、CDN 或对象存储上后端接口跑在独立的应用服务器或容器集群里两者大概率不在同一个域名下。常见的部署拓扑有这些部署形态前端来源后端接口是否存在跨域前后端同一域名Nginx 反代https://www.example.comhttps://www.example.com/api否前后端不同子域名https://www.example.comhttps://api.example.com是前端在 CDN后端独立域名来自 CDN 的随机节点域名https://api.example.com是前端本地 build 后供内网访问内网某台机器的 Nginx另一个端口或服务器是只要前端页面源与接口源在协议、域名、端口上有一个不同浏览器就会触发跨域拦截。所以生产环境的跨域不是“要不要处理”的问题而是“在哪个环节处理”的问题。3.2 三类最常见的跨域入口第一类是前端请求绝对地址。很多项目的生产包是通过环境变量控制接口地址的比如.env.production里写VITE_API_BASE https://api.example.com前端代码用axios.create({ baseURL: import.meta.env.VITE_API_BASE })。如果.env.production写错成http://localhost:8000或者忘了配置生产环境就一定会跨域。第二类是后端接口本身要跨域。比如开放平台 API、第三方对接或者前端确实部署在独立域名下请求必然跨域。这时候后端就要正确配置 CORS而不是写在某个开发专用的过滤器里。第三类是网关层误拦截。现在很多项目前端请求会先打到 Nginx、Kong、Spring Cloud Gateway 之类的网关再转发到后端。网关在处理 OPTIONS 预检请求时如果姿势不对也会导致跨域失败。3.3 CORS 头的关键字段与预检机制生产环境最常用、也最容易被配置错的就是 CORS 响应头。浏览器发起跨域请求前会对“非简单请求”先发一个 OPTIONS 预检请求然后根据响应头决定是否放行真实请求。CORS 相关响应头就这几个Access-Control-Allow-Origin允许的来源可以是具体源也可以是*。Access-Control-Allow-Methods允许的 HTTP 方法如 GET、POST、PUT、DELETE。Access-Control-Allow-Headers允许的自定义请求头如 Authorization、Content-Type。Access-Control-Allow-Credentials是否允许携带 Cookie。Access-Control-Max-Age预检请求结果缓存时间单位秒。预检请求的流程是浏览器先发一个OPTIONS带上Access-Control-Request-Method和Access-Control-Request-Headers服务器返回允许的 CORS 头浏览器再发真正的 GET/POST 请求。如果OPTIONS没有被正确处理或者返回的 CORS 头不匹配真实请求直接失败。有一个最常见的坑Access-Control-Allow-Origin设置为*的同时又设置Access-Control-Allow-Credentials: true。按规范浏览器会直接拒绝这种响应。Cookie 跨域必须要指定明确的来源域名不能是星号。4. 生产环境两条主路反向代理还是后端 CORS4.1 Nginx 反向代理从根上消灭跨域如果你能控制前端静态文件和接口的域名最推荐的生产方案是 Nginx 反向代理让前后端处于同源状态。比如 Vue 打包后放在 NginxDjango 接口跑在服务器的 8000 端口Nginx 配置可以这样写server { listen 80; server_name www.example.com; # 前端静态资源 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } # 后端接口反代 location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } }这个配置的核心思想是浏览器访问https://www.example.com/api/xxx时目标源和页面源完全一致不触发跨域。Nginx 再把请求转发给 Django完成数据返回。注意proxy_pass后面是否带路径是有讲究的。上面写的是proxy_pass http://127.0.0.1:8000;不带 URI所以请求原样保留/api/xxx转发给 Django后端路由必须包含/api前缀。如果你想让后端不用管/api前缀可以写location /api/ { proxy_pass http://127.0.0.1:8000/; }这样/api/user会被转发为/user。这两种写法在团队协作时容易产生分歧一定要提前约定清楚否则后端按/user配路由、前端按/api/user请求 部署后会多出很多无意义的 404 排查。4.2 后端 CORS面向跨域 API 的标准解法如果前端和 API 必须分开部署在不同域名比如前端在www.example.comAPI 在api.example.com那只能后端配合 CORS。Spring Boot 项目里常用过滤器统一配置import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import org.springframework.web.filter.CorsFilter; Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); // 线上必须写具体域名不能用 * config.addAllowedOrigin(https://www.example.com); config.addAllowedMethod(*); config.addAllowedHeader(*); // 如果需要携带 Cookie必须为 true同时 AllowedOrigin 不能是 * config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }如果是 Django通常用django-cors-headers# settings.py INSTALLED_APPS [ corsheaders, ... ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ... ] CORS_ALLOWED_ORIGINS [ https://www.example.com, ] CORS_ALLOW_CREDENTIALS TrueFlask 用flask-corsfrom flask import Flask from flask_cors import CORS app Flask(__name__) CORS(app, resources{ r/api/*: {origins: [https://www.example.com]} })注意origins里如果只写*且同时允许 Cookie 时浏览器同样会拒绝。所以生产环境要按实际来源域名精确配置。4.3 开发代理和生产反代、CORS 的本质区别开发代理、生产反向代理、后端 CORS 三者解决的问题相同但发生位置完全不同。我画过一张对比关系团队里新人看一遍就明白方案生效位置请求目标对浏览器来说dev server 代理前端开发服务器代理转发到后端同源Nginx 反代生产网关/Web 服务器代理转发到后端同源后端 CORS后端服务直接请求 API 域名跨域但被允许开发代理只存在于你本地生产环境不可能有“开发服务器”替用户转发。如果你不配置生产 Nginx 反代又不让后端支持 CORS用户浏览器就会直接撞上同源策略报错不可避免。5. 生产环境跨域配置的常见事故与排查清单5.1 预检请求 403 的全链路排查跨域报错里最经典的是浏览器控制台提示Access to XMLHttpRequest at https://api.example.com/api/user from origin https://www.example.com has been blocked by CORS policy: Response to preflight request doesnt pass access control check: No Access-Control-Allow-Origin header is present on the requested resource.这句话信息量很大。它告诉你请求被 preflight预检拦了而且响应里没有 CORS 头。排查思路应该是固定的第一步确认真实请求是否真的跨域。看 Network 面板里请求的 URL 和页面 URL 是否同协议、同域名、同端口。第二步确认预检请求发出去没有。Network 面板里看有没有一条OPTIONS请求。如果没有 OPTIONS可能是请求本身就是简单请求也可能是网关/浏览器缓存了预检结果。第三步看 OPTIONS 请求的响应状态码和响应头。我遇到的情况多半是后端网关把 OPTIONS 直接返回 403或者 Nginx 拦截了 OPTIONS 没转发给后端。比如 Nginx 的location只允许 GET、POST 方法OPTIONS 直接被拒绝就会造成跨域失败。第四步检查响应头里有没有Access-Control-Allow-*那一串。如果没有说明后端接口没走 CORS 过滤器如果有但是和预检请求携带的方法、头对不上比如前端带Authorization但Access-Control-Allow-Headers里没加上浏览器照样拦截。5.2 我遇到过的几个真实生产事故第一个事故Nginx 和后端同时配置了 CORS浏览器收到了两个Access-Control-Allow-Origin头。Nginx 里加了一行add_header Access-Control-Allow-Origin *;后端又配置了精确的 CORS 来源。浏览器看到多个同名响应头时会不会直接报错取决于具体浏览器的处理方式但 Chrome 通常只取第一个所以表现时好时坏。处理方式是同一链路里只保留一层 CORS 配置要么在最外层 Nginx 统一加要么交给后端。第二个事故允许任意来源却带了 Cookie。开发环境测试时后端配的Access-Control-Allow-Origin: *没改前端登录页要跨域带 Cookie浏览器郑重拒绝。报错信息还会明确提示 “The value of the Access-Control-Allow-Origin header in the response must not be the wildcard * when the requests credentials mode is include”。第三个事故Spring Boot 里同时使用了自定义拦截器和 CORS 过滤器拦截器先处理请求由于没有读取到跨域来源相关信息直接返回了未授权导致预检失败。这种问题很隐蔽排查时我会在拦截器里对OPTIONS请求做放行比如if (OPTIONS.equals(request.getMethod())) { return true; }第四个事故就是开头说的 Vue Django 部署后无法跨域。本地开发时有 Vite 代理兜底打包后前端直接请求 8000 端口后端又没有安装django-cors-headers跨域自然失败。后来前端改用相对路径/apiNginx 统一反代到 Django才彻底解决。5.3 上线前 5 分钟必做的跨域自检每次发版前我都会让团队在预发布环境跑一遍跨域自检。用 curl 模拟是最快的curl -i -X OPTIONS https://api.example.com/api/user \ -H Origin: https://www.example.com \ -H Access-Control-Request-Method: GET \ -H Access-Control-Request-Headers: authorization,content-type看响应头里是否包含Access-Control-Allow-Origin: https://www.example.com Access-Control-Allow-Methods: GET,POST,PUT,DELETE,OPTIONS Access-Control-Allow-Headers: authorization,content-type Access-Control-Allow-Credentials: true再用实际请求验证curl -i https://api.example.com/api/user \ -H Origin: https://www.example.com \ -H Authorization: Bearer your_token除了看响应头还要确认几个隐藏问题项目里有没有写死localhost的接口地址生产包的VITE_API_BASE是否指向了正确的域名要不要支持用户携带 CookieCDN 是否对OPTIONS请求做了响应头剥离或超时限制。浏览器缓存的预检结果最长可以到Access-Control-Max-Age设定的时间我在生产环境通常设 600 到 3600 秒既减少预检次数又不至于改完配置后用户要等太久才能生效。6. 跨域策略的本质以及我在团队里怎么定规矩6.1 三层拓扑下的跨域选择一个正规的前后端分离项目从开发到上线会经历三层拓扑本地开发、测试/预发布、生产。每一层的跨域策略都应该明确不能靠“碰运气”。本地开发一律用开发服务器代理后端不做 CORS 特殊处理。测试环境我建议和后端 API 保持独立域名专门暴露跨域问题。很多团队为了测试环境省事把前端和后端塞进同一个域名结果生产环境第一次遇到跨域就手足无措。测试环境模拟生产的域名结构是成本最低的排雷手段。生产环境先画一张请求链路图用户浏览器到 CDN/NginxNginx 到应用网关网关到后端服务。在哪一层终结跨域取决于域名拓扑和后端能否统一配置。如果项目没有独立 API 域名优先用 Nginx 反代消灭跨域如果 API 已经独立域名或者要对第三方开放那就后端统一配置 CORS网关层保证 OPTIONS 请求能正常穿透。6.2 我的最终建议我基于这些经验给团队定了几条简单规矩适合大多数前后端分离项目第一所有前端请求在代码里只使用相对路径比如/api/xxx不要写http://ip:port的绝对地址。环境差异全部通过部署配置解决而不是写死在代码里。第二是否跨域要由架构决定而不是由“后端有没有空配”决定。新项目启动前先定好前端域名、API 域名、部署拓扑再决定用 Nginx 反代还是 CORS。不要等到代码写完了上线前一晚再来想。第三CORS 配置要有环境区分。开发环境可以宽松一点测试环境可以允许测试域名但生产环境必须是精确来源并且禁用Allow-Origin: *与 Cookie 同时使用的组合。CORS 配置在业务代码里属于“影响范围大日常不触发”的一类配置必须纳入代码评审范围。第四每次部署前用 curl 把跨域自检跑一遍检查 Nginx 配置和后端 CORS 头是否匹配检查 OPTIONS 请求是否被网关拦截。跨域的故障现场往往非常难看全链路日志很难定位每次发版前多花五分钟检查强过线上事故后排查两小时。跨域本身不是一个复杂的技术点难点在于它横跨前端部署、后端配置、网关策略、域名管理多个环节。我这几年的经验是只要把开发环境和生产环境分开想把“谁在替浏览器转发”和“浏览器最终请求的源是什么”这两个问题想明白大部分跨域问题都能在十分钟内定位。上面的配置可以直接抄但更希望你把背后的判断逻辑带走下次再遇到跨域事故能清楚地告诉同事这一层该由谁来处理为什么。
返回列表