资讯动态

跨域问题深度解析:CORS、代理与Nginx反向代理实战指南

发布时间:2026/10/2 2:40:55 来源:尧图企业网站定制
1. 跨域报错到底在说什么先把原理搞清楚先说结论这个报错不是服务器炸了也不是你的接口写错了而是浏览器主动拦截了响应。你在控制台看到的红字本质上是浏览器作为一个“安全管理员”发现你当前页面所在的源Origin和它请求的目标地址不是同一个源于是把服务器返回的数据扣在了门外并且在控制台打出了一段警告。我最早遇到这个报错的时候第一反应是拿这个链接直接在浏览器地址栏里打开结果发现接口数据明明能正常返回。后来才意识到直接打开地址栏访问和在页面里用 XMLHttpRequest 发起请求完全是两套逻辑。地址栏访问是“你本人拿着证件进大楼”页面里的 AJAX 请求是“你让朋友拿着你的证件去另一栋楼里取文件”保安自然不会放行。这就是浏览器同源策略Same-Origin Policy的基本逻辑。同源策略要求协议、域名、端口三者完全一致才算同源。以报错信息里的http://localhost:8080为例如果你的页面跑在http://localhost:8080请求却发往http://localhost:9090那么端口不同跨域成立。如果页面跑在http://localhost:8080请求发往http://127.0.0.1:8080域名不同localhost 和 127.0.0.1 在浏览器看来是两个域名同样跨域。很多人忽略了这一点明明端口号看着一样却仍然报跨域往往就是 localhost 和 127.0.0.1 混用导致的。再拆一下这段报错Access to XMLHttpRequest at ‘http://localhost:8080/xxx’ from origin ‘http://localhost:3000’ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.它其实包含三层信息。第一XMLHttpRequest是本次请求的发起者也就是 AJAX 请求第二at http://localhost:8080/xxx是实际请求的接口地址第三from origin http://localhost:3000是当前页面所在的源。后面的No Access-Control-Allow-Origin header is present则是浏览器给出的直接原因服务器返回的响应里没有携带名为Access-Control-Allow-Origin的响应头。所以解决这个问题的核心思路就一句话让服务器在响应里明确告诉浏览器“这个源可以读我的数据”。而具体怎么做取决于你的项目结构、技术栈以及部署环境。接下来我会从原理讲到实操把几种最常用的方案完整过一遍。2. 前后端项目里最常见的跨域成因跨域报错的成因在前后端分离的开发模式下非常集中。我接手过的项目里十次有八次逃不出下面这几类情况。2.1 开发环境端口不一致这是最典型的现象前端开发服务器默认跑在 3000、5173、8080后端接口服务跑在 8080、9090、7001只要两边端口不一致就必然触发跨域。而且很多前端项目里接口地址是写死在代码里的比如axios.get(http://localhost:8080/api/user)。这种写法在开发阶段最容易让浏览器报警因为你的页面来源是http://localhost:3000目标却是http://localhost:8080端口都不一样同源策略直接判定跨域。这里有一个非常容易踩的坑很多人以为把前端页面打包成静态文件扔到后端服务的 public 目录里就不会跨域了。如果你的页面确实是从http://localhost:8080这个地址打开的同时接口也请求http://localhost:8080/xxx那么源和目标一致确实不跨域。但一旦前端资源被单独部署到 CDN、OSS 或者另一个服务器而接口还在原来的地方跨域又会出现。所以判断是否跨域永远看“当前页面所在的源”与“接口请求目标的源”而不是看代码在哪个目录下。2.2 后端压根没有配置 CORS 响应头或者配置了但没生效后端没有配置 CORS是最常见也最好解决的情况。你打开浏览器的 Network 面板点开那条失败的请求在 Response Headers 里看不到任何Access-Control-Allow-*开头的字段基本就能确认是这个原因。还有一种情况比没配置更让人恼火配置了但没生效。比如在 Spring Boot 里写了CrossOrigin(origins http://localhost:3000)结果前端实际来源是http://127.0.0.1:3000这种字符串不完全匹配的情况浏览器也一样拦截。再比如在 Nginx 里配了add_header Access-Control-Allow-Origin *;但是放在了错误的location块里导致某些接口路径走了别的规则响应头压根没加进去。2.3 预检请求OPTIONS被拦截这类报错最隐蔽如果你的请求不是简单请求比如设置了Content-Type: application/json或者带了自定义的请求头Authorization等浏览器会先自动发送一个OPTIONS请求也就是预检请求用来询问服务器“我打算这样请求你允许吗”。服务器需要对这个OPTIONS请求返回正确的Access-Control-Allow-Methods和Access-Control-Allow-Headers预检才算通过。问题在于很多时候后端的权限拦截器、过滤器如 Spring Security、Shiro、JWT 鉴权过滤器会把OPTIONS请求当成普通请求拦截下来直接返回 401 或者 403。还有一种情况是后端路由压根没有匹配OPTIONS方法导致返回 404。预检请求都没通过后面的真实请求自然被拦。这种报错的迷惑性很强因为在 Network 面板里你可能只看到一条失败记录或者看到一条 401/404 的 OPTIONS 记录一时反应不过来它跟跨域有什么关系。2.4 打包部署后的跨域陷阱和开发环境完全是两码事开发环境能用 Vite 代理、Webpack devServer 代理来解决跨域但一旦npm run build打包之后前端变成一堆静态文件没有任何代理服务器在工作了。这时候如果前端代码里请求的还是相对路径/api/xxx那么请求会发到静态文件所在的服务器如果静态文件放在 Nginx 上而接口在另外一台服务器就必须靠 Nginx 反向代理来转发否则必然报错。我见过不少 Vue Django 项目开发环境用 Vite 代理一切正常部署到服务器之后Nginx 只配置了前端静态文件的访问没有把/api路径转发到 Django 服务结果前端根本无法访问后端接口。这时候报的错往往也是Access-Control-Allow-Origin缺失因为请求压根没到达 Django。所以排查看似复杂的部署跨域问题第一步永远是确认请求到底走到了哪一层。3. 三种主流的跨域解决方案与实操配置跨域问题的解决方案没有银弹每个方案都有它的适用场景。我习惯把方案分成三类后端直接配置 CORS、前端开发环境代理、生产环境网关反向代理。你可以根据自己项目的阶段和环境来选择。3.1 方案一后端配置 CORS 响应头一劳永逸的基础方案只要后端能控制响应头这个方案就是最根本的。不同语言和后端框架的配置方式不同但核心都是让服务器在响应里加上Access-Control-Allow-Origin这个头。下面是我实际用过的几种配置。Spring Boot 两种方式任选其一第一种是用注解适合单体接口或者临时调试CrossOrigin(origins http://localhost:3000) GetMapping(/api/user) public User getUser() { // ... }第二种是全局配置适合统一管理Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:3000) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意allowedOrigins写法在高版本 Spring Boot 里如果你同时设置了allowCredentials(true)那allowedOrigins就不能写*必须写具体域名列表否则启动会报错。这一点是很多新手会踩的坑。Node.js Express 使用 cors 中间件几乎零配置npm install corsconst cors require(cors); const express require(express); const app express(); app.use(cors({ origin: [http://localhost:3000], methods: [GET, POST, PUT, DELETE, OPTIONS], allowedHeaders: [Content-Type, Authorization], credentials: true }));Express 的 cors 中间件封装得很完整默认情况下origin: *是允许所有来源但只要开了credentials: true就不能用通配符这一点和 Spring Boot 规则一致。Django 使用 django-cors-headerspip install django-cors-headers在settings.py中INSTALLED_APPS [ ... corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ... ] CORS_ALLOWED_ORIGINS [ http://localhost:3000, ] CORS_ALLOW_CREDENTIALS True注意CorsMiddleware要放在最上层尽量靠近MIDDLEWARE列表的开头因为它需要优先于能够生成响应的中间件执行这样才有机会往响应里注入 CORS 头。PHP 手动设置响应头古老但有效header(Access-Control-Allow-Origin: http://localhost:3000); header(Access-Control-Allow-Methods: GET, POST, OPTIONS); header(Access-Control-Allow-Headers: Content-Type, Authorization); if ($_SERVER[REQUEST_METHOD] OPTIONS) { http_response_code(204); exit; }PHP 项目中用 JSONP 解决跨域的年代已经过去了现在推荐直接设置这些头。PHP 老项目尤其是原生 PHP 或多套框架混杂的项目我一般建议在入口文件比如index.php或者公共的初始化文件里统一加这三行比在每个接口里单独写要省事得多。3.2 方案二前端开发环境代理Vite 和 Webpack 的正确用法如果你不想动后端代码或者后端暂时没法改那前端开发环境代理是开发阶段最舒服的方案。它做的事情是前端页面请求一个相同源的地址开发服务器收到请求后转发到真正的后端服务再把响应返回给浏览器。因为浏览器看到的请求是同源的所以不会触发跨域拦截。Vite 项目的代理配置Vue 3、React 均适用在vite.config.js里export default defineConfig({ server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } });这个配置的意思是当前端代码发出以/api开头的请求时Vite 开发服务器会把请求转发到http://localhost:8080同时将路径里的/api前缀去掉。比如前端请求/api/user后端实际收到的是http://localhost:8080/user。如果你的后端路由本身就带有/api前缀那就不需要rewrite了去掉那行即可。changeOrigin: true很关键它会修改请求头里的Host字段为目标地址的 Host。有些后端服务会根据 Host 做校验比如 Nginx 按域名区分虚拟主机如果changeOrigin没开启转发过去 Host 还是localhost:3000后端可能会认为请求不合法。Vue CLIWebpack项目的配置vue.config.js里module.exports { devServer: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } } };和 Vite 的写法本质一样只是字段名略有不同。这里我要强调一个实际开发中很常见的问题代理只对开发服务器生效打包后完全失效。很多人开发环境一切正常一部署就崩原因就是把代理当成了生产环境的解决方案。生产环境必须用 Nginx 反代或者后端网关这一点务必记清楚。3.3 方案三生产环境用 Nginx 反向代理统一解决生产环境里前端静态文件、后端接口服务往往部署在不同地址最稳妥的跨域方案是在 Nginx 层面做反向代理。好处是对外只暴露一个域名和端口浏览器看到的请求都是同源从根本上绕开了跨域问题也顺带隐藏了后端服务器的真实地址接口地址不直接暴露在浏览器里。一个典型的 Nginx 配置server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /var/www/html; index index.html; try_files $uri $uri/ /index.html; } # 接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # CORS 相关响应头 add_header Access-Control-Allow-Origin $http_origin always; add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS always; add_header Access-Control-Allow-Headers Content-Type, Authorization always; add_header Access-Control-Allow-Credentials true always; # 预检请求直接返回 if ($request_method OPTIONS) { return 204; } } }这里有两个细节值得单独说明。第一个add_header里我写的是$http_origin而不是*。这是因为当请求携带 Cookie 凭证时Access-Control-Allow-Origin不能是通配符必须回显真实的来源。$http_origin是 Nginx 内置变量表示请求头里的Origin字段这样就能动态回显既安全又能支持带凭证请求。第二个add_header后面加了always关键字。如果不加当响应状态码是 4xx、5xx 时Nginx 默认不会把add_header配置的头加进去。而跨域场景里接口出错时浏览器仍然需要看到Access-Control-Allow-Origin否则你只会在控制台看到一个被 CORS 拦截的错误而真正的业务错误信息却被浏览器吞了排查起来非常难受。加上always之后即使后端返回 500前端也能读到响应头和响应体对排查问题帮助极大。if ($request_method OPTIONS) { return 204; }这段是直接处理预检请求。如果后端框架已经能正常处理 OPTIONS 请求这段可以省略。但如果后端有鉴权过滤器拦截了 OPTIONS那这一段几乎是必须的。Nginx 在收到 OPTIONS 后直接返回 204请求根本不会进入后端鉴权拦截自然也就管不到它。3.4 带凭证请求Cookie、Authorization的特殊处理如果你需要在前端请求里携带 Cookie或者后端接口依赖 Session那情况会特殊一点。前端必须显式开启凭证模式后端和代理层也要配合。前端axios 开启 withCredentialsaxios.defaults.withCredentials true;或者单个请求axios.get(/api/user, { withCredentials: true });如果你用的是 fetchfetch(/api/user, { credentials: include });后端和代理层注意事项Access-Control-Allow-Origin不能是*必须明确指定来源域最好动态回显Origin头。必须设置Access-Control-Allow-Credentials: true。Access-Control-Allow-Headers里必须包含前端实际会用到的自定义头比如Authorization。三个条件缺一不可否则浏览器会在控制台提示你“跨域请求被拦截”而且有时候提示信息非常隐晦比如The value of the Access-Control-Allow-Origin header in the response must not be the wildcard * when the requests credentials mode is include。看到这句提示不用怀疑多半就是*和withCredentials打架了。3.5 JSONP老方案但没完全过时JSONP 曾经是跨域的主流方案它的原理是利用script标签不受同源策略限制的特性通过动态注入 Script 标签来加载带回调参数的接口。它只支持 GET 请求而且不支持带凭证所以在今天的前后端分离项目里已经逐渐退出主流了。但如果你的后端是 PHP 老项目或者你只是想临时验证一下跨域数据能否取回JSONP 仍然是一个可以快速验证的思路。前端示例function jsonp(url, callbackName) { return new Promise((resolve, reject) { const script document.createElement(script); script.src ${url}?callback${callbackName}; window[callbackName] (data) { resolve(data); delete window[callbackName]; document.body.removeChild(script); }; script.onerror reject; document.body.appendChild(script); }); }后端需要把返回内容包装成callbackName({...})的形式比如 PHP$data [name 张三]; echo $_GET[callback] . ( . json_encode($data) . );这种方式在需要跨域的古老系统对接里还能见到但凡是能用 CORS 解决的场景我都不建议再用 JSONP毕竟它不支持 POST、报错信息不直观、调试体验也差。4. 常见问题排查与避坑实录这一节我把自己实际踩过的坑和帮别人排过的案列整理成了一份速查表你可以直接照着排查。报错现象可能原因排查方向Network 里看不到 CORS 相关响应头后端未配置 CORS或配置未生效检查后端代码/中间件检查响应头OPTIONS 请求返回 404后端路由没有处理 OPTIONS 方法在网关层拦截 OPTIONS 直接返回 204OPTIONS 请求返回 401/403权限拦截器拦截了预检请求放行 OPTIONS 请求或 Nginx 直接返回 204配置了 CORS 还是报错代理层或网关覆盖了后端响应头检查 Nginx/网关配置排查 header 被覆盖带 Cookie 请求报错Origin 为*且凭证模式开启改为回显具体 Origin设置 Allow-Credentials开发环境正常部署后跨域开发代理在生产不生效在 Nginx 加反向代理配置接口地址没变但感觉跨域失效代码里写死了绝对地址或被中介层改写检查 Network 里的实际请求 URL4.1 明明加了 CORS 配置为什么还是报错这个问题的排查方向我建议按顺序来。第一步先在浏览器 Network 面板里找到那条请求看 Response Headers 里到底有没有Access-Control-Allow-Origin。如果根本没有说明配置没加到这条响应上可能是配置的作用域不对比如只配了某个 Controller而实际请求走了另一个 Controller。第二步如果头存在但值和你预期的不一致比如你配的是http://localhost:3000响应里却是http://123.45.67.89:3000那就是来源不匹配。要留意浏览器当前的地址栏到底是什么留意localhost还是127.0.0.1这两个虽然指同一台机器但在 CORS 规则里算不同源字符串比对也必然不相等。第三步如果响应头里有两个Access-Control-Allow-Origin比如一个来自后端应用一个来自 Nginx浏览器会判定为非法配置并拦截。这种情况下要检查是否在 Nginx 和后端同时配置了 CORS建议只在一层做不要层层都加。4.2 预检请求 404 或 403 的排查思路预检失败的特征是Network 面板里能看到一条OPTIONS请求状态码可能是 404、401、403或者 200 但响应头里缺少Access-Control-Allow-Methods和Access-Control-Allow-Headers。如果状态码是 404优先检查后端路由是否匹配OPTIONS方法。很多后端框架默认只给显式声明的路由匹配了 GET/POST如果前端请求的路径对应不上任何路由OPTIONS 预检就会 404。处理方式是在网关层统一拦截 OPTIONS 请求直接返回 204或者在 Nginx 层用if ($request_method OPTIONS) { return 204; }。如果状态码是 401/403就是鉴权层拦截了 OPTIONS。常见于 Spring Security、JWT 过滤器处理办法是写一个过滤规则对OPTIONS方法直接放行不进入鉴权逻辑。在 Spring Security 里类似这样http.authorizeRequests() .antMatchers(HttpMethod.OPTIONS, /**).permitAll() .anyRequest().authenticated();4.3 代理配置了但请求地址没变化是什么原因这种情况多出在“代理规则没匹配上”。比如你在 Vite 里配了/api代理但代码里实际请求的是http://localhost:8080/api/user也就是用了绝对的完整地址。代理规则只会对不带域名、以路径开头的相对地址生效。解决方式是把请求地址改成/api/user让浏览器先请求前端开发服务器再由开发服务器转发。另一种情况是代理目标地址写错了。我曾经在 Vite 代理里把target写成http://localhost:8080/api而代码里请求的也是/api/user结果转发后目标 URL 变成了/api/api/user后端自然 404然后又被浏览器当成跨域错误展示。为了避免这种路径拼接问题建议把代理目标写成后端根地址路径部分由rewrite或pathRewrite来控制逻辑就会清爽很多。4.4 打包部署后的跨域问题核心就三板斧线上跨域和开发环境最大的区别是开发代理彻底失效生产环境需要考虑的只有两件事。第一件确认前端静态资源和后端接口服务之间的关系。如果它们不在同一个域名和端口下就必须有反向代理把两者“拉到同一个屋檐下”。最常用的就是 Nginxlocation /api/的代理配置也可以在后端网关里做路由转发。第二件确认跨域配置不能重复叠加。我遇到过前端部署在http://example.com后端 API 在http://api.example.comNginx 里给 API 加的 CORS 头和后端框架里加的 CORS 头重复响应里出现两个相同的头浏览器直接报错。最后我的处理习惯是后端负责业务逻辑代理层负责 CORS 头职责尽量单一。4.5 一个容易忽略的细节浏览器缓存了 CORS 响应如果你改了后端 CORS 配置刷新页面后发现还是不生效先别急着怀疑配置写错很可能是浏览器缓存了之前的响应。特别是后端设置了Access-Control-Max-Age的时候浏览器会在指定时间内复用预检结果不重新发起 OPTIONS。此时可以直接打开一个新窗口或者用无痕窗口访问同时打开 Network 面板的 “Disable cache” 选项再测一次。这个坑经常让人白折腾半天。5. 一点实操体会最后聊点我自己的真实感受。跨域这事绕来绕去其实就三个层面如果你能改后端那就后端配置 CORS这是最根本的解法如果后端暂时动不了开发环境用代理生产环境用 Nginx 反代这是工程化最推荐的玩法至于 JSONP 这类老方案知道有这个选项就行不到万不得已别往新项目里引。我在排查实际问题时最常用的一个动作就是打开浏览器开发者工具切到 Network 面板先看请求有没有发出去再看有没有 OPTIONS 请求最后看响应头里有没有 CORS 相关的字段。这条链路走一遍90% 的问题都能定到位。别一上来就瞎改代码先搞清楚请求到底被拦在哪一层是后端没返回还是返回了被代理覆盖还是直接被预检拦住了。方向对了解决只是时间问题。如果哪一天你被这个报错整得焦头烂额不妨回到最原始的问题上想一想浏览器要的只是一个名为Access-Control-Allow-Origin的响应头。不管前端、后端、网关哪一层能把它正确加到响应里问题就结束了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑