ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

前端必会:Axios跨域十种解决方案与踩坑清单

前端必会:Axios跨域十种解决方案与踩坑清单 做前端这几年Axios 跨域可以说是每个项目都绕不开的一道坎。明明接口地址写对了、参数也传了浏览器控制台却甩给你一条No Access-Control-Allow-Origin header is present后端同事说接口没问题你自己本地 Postman 一调也是好的问题几乎都出在“浏览器同源策略”和 Axios 请求的细节上。这篇文章把我这些年用过的十种跨域解决方案整理成一套可直接上手的清单从开发代理、Nginx 转发、后端 CORS 配置到 Axios 二次封装、Content-Type 陷阱全部给出具体配置和踩坑记录。不管是刚入行的前端、被接口问题折磨的全栈还是要处理联调环境的后端同学都可以按图索骥找到适合自己项目的那一招。1. 先从根源讲明白跨域到底是怎么发生的1.1 同源策略与“源”的定义很多人把“跨域”挂在嘴边但真正能把“源”的概念讲清楚的不多。浏览器所谓的“源”是由协议 主机 端口三部分组成的完整标识只有这三者完全一致两个地址才算“同源”。换句话说http://localhost:5173和http://localhost:8080虽然主机名相同但端口不同它们就是不同的源http://example.com和https://example.com协议不同也是跨域api.example.com和www.example.com主机不同同样是跨域。这个设计不是为了刁难开发者而是浏览器的一种安全机制叫“同源策略”。它默认不允许页面中的脚本去读取另一个源的响应内容因为如果没有这个限制你在自己的站点上随便放一段脚本就能把用户在别的网站上的数据偷偷读走。可以用快递来类比同源策略相当于一个门卫只允许快递员把包裹送进同一个小区跨小区派件就得先经过门卫审批。而这个“门卫审批”的流程就是浏览器发起的跨域检查。1.2 Axios 与浏览器拦截过程Origin 与 preflight当你的页面通过 Axios 发起一个跨域请求时浏览器会自动在请求头里加上Origin字段标明当前页面所属的源。后端拿到请求后如果响应里没有包含允许该源的Access-Control-Allow-Origin头浏览器就会把响应拦截下来控制台报错但请求实际上已经到达服务器了。这也是联调时最容易被误解的一点后端明明收到了请求为什么前端报跨域因为问题出在浏览器端而不是服务器端。更复杂的情况是“预检请求”。Axios 默认发送 JSON 格式的application/json请求这种请求携带了自定义请求头属于“非简单请求”。浏览器不会直接发送真实请求而是先发一个OPTIONS请求问服务器“我要用 POST、带 Content-Type 和 Authorization你们允许吗” 服务器如果对OPTIONS请求返回 204并带上正确的 CORS 响应头浏览器才会继续发送真正的 POST 请求。这个流程我见过很多后端踩坑只给普通接口加了 CORS 头但没有单独处理OPTIONS预检导致前端报错一直报“preflight request didnt pass access control check”。要理解跨域问题必须先把这个预检过程刻在脑子里。2. 十招速览与场景匹配2.1 十招一张表看明白先给一张总览表方便你按图索骥。后面会针对每一招展开聊配置细节和注意事项。招数方案适用场景改动位置推荐度第1招Vite/Webpack 开发代理本地开发环境前端工程配置最高第2招Nginx 反向代理测试/生产环境服务器配置最高第3招后端直接开启 CORS前后端能同时改动后端响应头高第4招JSONP 兜底仅 GET 请求、老系统前端 后端配合低第5招处理 preflight 预检所有非简单请求后端/中间件高第6招网关统一注入 CORS微服务、多接口团队网关层高第7招Axios 二次封装所有 Axios 项目前端代码高第8招Content-Type 与 multipart 处理文件上传、表单提交前端代码中第9招本地浏览器关闭安全策略本地快速测试浏览器参数低第10招WebSocket 连接长连接、实时通信前后端切换协议中从这张表能看出没有任何一套方案是“银弹”。开发环境用代理是最省心的因为浏览器根本感受不到跨域生产环境更常见的是 Nginx 转发或后端 CORS。选招数的核心逻辑是能让浏览器不发起跨域请求就别让它发起不得不跨域时再考虑 CORS 和相关细节。2.2 场景选型开发、测试、生产该用哪一招按环境来选思路会清晰很多。本地开发阶段第一优先是 Vite 或 Webpack 的 devServer 代理。你只需要把 Axios 的 baseURL 指向一个本地前缀比如/api然后让开发服务器把所有/api开头的请求转发到真实后端地址。此时浏览器发出的请求是同源的自然就没有跨域问题。这个方案几乎零成本强烈推荐。到了测试或生产环境如果前端和服务端不在同一个域名下我一般优先考虑 Nginx 反向代理。在 Nginx 配置里把/api路径转发到后端服务由 Nginx 充当“中间人”。前后端代码都不用改运维侧配置一次就行。如果你们是前后端同仓部署、或者后端同学愿意配合那直接在服务端加上 CORS 响应头也很省事。但要注意一旦涉及携带 Cookie 或 Authorization 头Access-Control-Allow-Origin就不能写成*必须回显请求方的具体 Origin否则浏览器照样拦截。3. 前后端联调最常用的 6 招服务器与配置派3.1 第1招Vite/Webpack 开发代理用 Vite 开发时我习惯在vite.config.js里这样配export default { server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } } }这段配置的意思是浏览器请求/api/user的时候Vite 开发服务器会转发到http://localhost:8080/user。changeOrigin: true可以让转发后的请求头里Host变成目标域名避免后端做了域名校验时出现“Host 不匹配”。Webpack 配置类似在webpack.config.js里devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true, pathRewrite: { ^/api: } } } }这套方案背后有一个值得记住的原理代理转发是在服务器之间完成的浏览器从头到尾只和本地开发服务器通信所以同源策略完全不会被触发。理解了这点你就知道为什么推荐开发环境优先用代理了。我踩过的坑是只配了代理、但 Axios 的 baseURL 写成了完整的后端地址比如http://localhost:8080这样代理根本不生效。正确做法是让 baseURL 写成/api这种相对路径把跨域问题完全丢给 devServer 处理。3.2 第2招Nginx 反向代理到了生产环境Nginx 反向代理是最常见的解法。配置模板如下server { listen 80; server_name frontend.example.com; location /api/ { proxy_pass http://backend-server:8080/; proxy_set_header Host $host; } }这里有个细节很多人搞错proxy_pass结尾是否带/效果完全不同。如果写proxy_pass http://backend-server:8080;那么请求/api/user会被原样转发到后端如果写成proxy_pass http://backend-server:8080/;/api/前缀会被去掉转发成/user。具体用哪种要看后端接口设计时有没有/api前缀。生产环境用 Nginx 还有一个好处可以在同一层直接处理预检请求。浏览器发OPTIONS过来时Nginx 直接在代理转发前返回 204这样后端的压力会更小。3.3 第3招后端直接开 CORS 响应头如果前后端能一起改最直接的方式是在后端统一设置 CORS 头。我用 Node.js Express 写过这样的中间件app.use((req, res, next) { res.header(Access-Control-Allow-Origin, *); res.header(Access-Control-Allow-Methods, GET,POST,PUT,DELETE,OPTIONS); res.header(Access-Control-Allow-Headers, Content-Type, Authorization); if (req.method OPTIONS) { return res.sendStatus(204); } next(); });PHP 场景也类似在接口入口处统一追加响应头header(Access-Control-Allow-Origin: *); header(Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS); header(Access-Control-Allow-Headers: Content-Type, Authorization); if ($_SERVER[REQUEST_METHOD] OPTIONS) { http_response_code(204); exit; }这里的*表示允许所有源访问。但请注意如果请求需要携带 Cookie也就是 Axios 配了withCredentials: true后端就不能再使用*了必须显式返回当前请求的 Origin 来源比如Access-Control-Allow-Origin: http://localhost:5173同时设置Access-Control-Allow-Credentials: true。浏览器对这两个条件的校验非常严格少一个都会报错。3.4 第4招JSONP 兜底GET 请求专用JSONP 是一种“上古神技”核心原理是利用script标签加载跨域脚本不受同源策略限制这一特性。它只能处理 GET 请求而且后端必须配合返回一段 JavaScript 回调代码。现在除了对接特别老的外部接口我一般不建议主动用 JSONP但作为兜底方案值得掌握。Axios 本身不支持 JSONP需要手动封装。我写过一段极简实现function jsonpRequest(url, params) { return new Promise((resolve, reject) { const callbackName jsonp_${Date.now()}; const script document.createElement(script); window[callbackName] (data) { delete window[callbackName]; script.remove(); resolve(data); }; const query new URLSearchParams({ ...params, callback: callbackName }); script.src ${url}?${query}; script.onerror reject; document.body.appendChild(script); }); }后端的返回格式必须是jsonp_169999999999({ok: true})这种形式。它的优点是不需要处理预检请求也不用设置 CORS 头缺点是只能 GET且无法上传文件出错排查也麻烦。因此我基本都是把它留作“最后一根救命稻草”。3.5 第5招提前处理 preflight 请求很多时候接口跨域报错不是因为没有Access-Control-Allow-Origin而是因为没有处理OPTIONS预检请求。Axios 发送 POST 请求并且携带 JSON 数据时浏览器会先发一个OPTIONS请求如果这个预检请求得不到正确响应后续真实请求根本不会发出。处理方式有两种。第一种是后端接口入口统一判断if (req.method OPTIONS) { res.header(Access-Control-Allow-Origin, req.headers.origin || *); res.header(Access-Control-Allow-Methods, GET,POST,PUT,DELETE,OPTIONS); res.header(Access-Control-Allow-Headers, Content-Type, Authorization); return res.sendStatus(204); }第二种是在 Nginx 层直接拦截让预检请求不进入后端逻辑location /api/ { if ($request_method OPTIONS) { add_header Access-Control-Allow-Origin *; add_header Access-Control-Allow-Methods GET,POST,PUT,DELETE,OPTIONS; add_header Access-Control-Allow-Headers Content-Type,Authorization; return 204; } proxy_pass http://backend-server:8080/; }这里我特别想提醒一句Access-Control-Request-Headers中列出什么字段Access-Control-Allow-Headers就必须包含对应的字段否则预检同样失败。比如 Axios 在请求头里加了X-Custom-Header你却只允许了Content-Type浏览器照样拦截。3.6 第6招网关层统一注入 CORS团队规模稍微大一点接口分散在多个后端服务里时最怕每个服务各配一套 CORS 规则而且配置还不一致。我的做法是在网关或统一入口层把 CORS 处理掉让所有后端服务都不再关心跨域问题。如果是基于 Nginx 的网关可以单独抽一个公共配置文件然后被各个 server 引用。如果是 Spring Cloud Gateway、Kong 这类网关通常有现成的 CORS 中间件。统一处理的好处是规则只维护一份以后调整白名单、允许的请求头只需要改一处。但也要注意网关层统一注入 CORS 并不是万能的。如果后端服务之间互相调用时也出现“跨域”那就不是浏览器层面的问题因为服务端之间的请求不受同源策略限制不需要在网关处理。要区分清楚别把服务端到服务端的鉴权失败误当成跨域。4. 客户端侧灵活应对的 4 招Axios 细节派4.1 第7招Axios 二次封装统一配置Axios 二次封装最大的价值是把跨域相关的配置收敛到一个文件里避免每个页面各自为战。我会先创建一个统一的实例import axios from axios; const service axios.create({ baseURL: /api, timeout: 15000, withCredentials: true, headers: { Content-Type: application/json } }); service.interceptors.request.use( (config) { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }, (error) Promise.reject(error) ); service.interceptors.response.use( (response) response.data, (error) { if (error.response error.response.status 401) { window.location.href /login; } return Promise.reject(error); } ); export default service;封装之后业务页面里的请求会变得非常清爽而且如果将来后端接口前缀调整或者需要在全局追加某个请求头只需要改这一个文件。特别提醒如果接口需要携带 Cookie一定要把withCredentials设置为true同时后端不能使用Access-Control-Allow-Origin: *否则浏览器会把请求拦下来。4.2 第8招Content-Type 与 multipart/form-data 的跨域陷阱文件上传和表单提交场景里Axios 的Content-Type是最容易出问题的地方。很多人看到上传报错第一反应是自己手动加一个Content-Type: multipart/form-data结果反而更糟。原因在于浏览器在发送真正的multipart/form-data请求时会自动生成一组boundary分割线并写在Content-Type里比如Content-Type: multipart/form-data; boundary----WebKitFormBoundaryxxx。如果你手动设置了Content-Type: multipart/form-data就相当于把边界信息弄丢了后端解析不到文件字段自然就失败。正确做法是直接把FormData对象交给 Axios并且不要手动设置Content-Type让它自动生成const formData new FormData(); formData.append(file, fileInput.files[0]); formData.append(name, 测试文件); axios.post(/api/upload, formData, { headers: { Content-Type: multipart/form-data } });等等这里我故意留了一个反例。刚才说不要手动设置但这行代码里还是写了这是我早期踩过的坑。后来我改成这样axios.post(/api/upload, formData);或者至少这样让浏览器自己处理axios.post(/api/upload, formData, { headers: { Content-Type: undefined } });还有一个常见坑是 Axios 版本升级。有些项目从axios0.x升级到axios1.x之后默认的序列化行为发生变化原本不发预检的请求可能因为Content-Type或请求头变化突然开始报跨域。如果你遇到“升级前是 JSON升级后却不行了”的情况优先检查请求头是否变化或者是否因为默认 data 序列化方式改变导致预检条件不同。所以我的经验是能交给浏览器处理的头尽量不手动干预。不确定时可以用浏览器开发者工具 Network 面板对比真实发出去的请求头一眼就能看出 Content-Type 有没有被改坏。4.3 第9招仅限本地的浏览器跨域放行调试有些场景下你只是想快速验证前端代码逻辑是否正常但后端接口各种问题导致联调一直卡着。这时候可以临时用浏览器的跨域放行模式。Chrome 可以这样启动一个独立调试窗口chrome --disable-web-security --user-data-dir/tmp/chrome-debug注意一定要带--user-data-dir指定一个新的临时用户目录否则启动参数不生效。这个模式下浏览器会关闭同源策略跨域请求不会拦截。这个方法只适合本地调试千万不要用它来日常上网也不要用它去访问需要登录的正式环境否则安全风险很大。我在很多项目里用这招来验证“到底是前端问题还是后端 CORS 问题”。如果关掉安全策略后请求通了那说明后端接口本身没问题问题出在跨域配置如果关掉之后依然失败那就是接口地址、参数或鉴权的问题和跨域无关。这能帮你快速缩小排查范围。4.4 第10招WebSocket 与跨域socket 也要跨域吗最后这招严格来说不算是 Axios 的“同域替代方案”但它和跨域强相关而且经常有人问socket 有跨域吗WebSocket 协议本身不受浏览器同源策略的限制跨域建立 WebSocket 连接是允许的。但服务器端可以主动校验请求头里的Origin字段决定是否接受这个连接所以实际能不能连上要看服务端策略。如果你用 Axios 做实时轮询会被跨域和请求频率问题搞得很难受这时可以考虑换掉 HTTP 轮询改用 WebSocket。Axios 不支持 WebSocket需要引入socket.io-client或其他 WebSocket 库。在客户端代码里WebSocket 的连接地址通常是ws://或wss://开头服务端只要允许对应来源即可。很多人在socket握手环节遇到的“403”或“跨域”问题其实不是浏览器拦截而是服务端安全策略拒绝这一点需要区分清楚。如果你的项目里既有 Axios 请求又有 WebSocket建议在封装层做区分HTTP 请求统一走 Axios 实例实时通道统一走 WebSocket 连接不要混在一起。这样后续维护和问题排查都会省力很多。5. 常见问题与排查清单5.1 高频报错速查表下面这些报错是我在联调中见得最多的整理成速查表方便你直接对照。报错信息可能原因处理动作No Access-Control-Allow-Origin header is present后端未设置 CORS 头后端设置响应头或改用代理Response to preflight request doesnt pass access control check预检请求未正确处理后端/网关处理OPTIONS并返回 204Request header field authorization is not allowed by Access-Control-Allow-Headers允许的请求头列表缺少字段在Access-Control-Allow-Headers加上对应字段The value of the Access-Control-Allow-Origin header in the response must not be the wildcard *带了 Cookie 但后端使用了*后端改为返回具体 Origin并开启 CredentialsNetwork Error控制台有时显示ERR_FAILED请求未发出、证书问题或接口地址不可达先确认后端服务通不通再看跨域配置升级 Axios 后请求格式变了导致跨域版本变化影响Content-Type/序列化对比升级前后的请求头重新设置 data 格式这张表不能解决所有问题但它可以帮助你把“跨域报错”和“后端接口问题”分开。很多“Network Error”其实根本不是跨域而是网络不通这时不要死磕 CORS。5.2 排查思路与实战心得我的排查顺序是先看 Network 面板确认请求是否真的发出去了再看请求头里有没有Origin然后看响应头里有没有Access-Control-Allow-Origin最后看是否触发OPTIONS预检。如果后端同事坚持说接口没问题你可以用 curl 模拟一个带Origin的请求看看服务端实际返回的响应头curl -X OPTIONS http://localhost:8080/api/user \ -H Origin: http://localhost:5173 \ -H Access-Control-Request-Method: POST \ -H Access-Control-Request-Headers: Content-Typecurl 不受浏览器同源策略限制但你可以手动加上Origin观察响应中是否包含正确的Access-Control-Allow-*头。用这个方式可以快速判断是后端没配 CORS还是浏览器做了额外校验。另外有一个很容易被忽略的细节后端配置 CORS 白名单时写的是http://localhost:8080但前端实际运行在http://localhost:5173两个端口不一致源自然不匹配浏览器照样拦截。联调时前后端约定的 Origin 一定要和真实运行环境完全一致。再分享一个我自己的习惯跨域问题一旦出现我先不急着改代码而是打开浏览器 Network 面板开启“Preserve log”刷新页面后按请求耗时排序重点看红色的OPTIONS请求。因为只要预检亮了红灯后面真实请求一定不会成功。把预检请求的响应头看明白问题基本就解决了一半。踩过几次坑之后我现在的原则是开发环境能走代理就走代理生产环境能走 Nginx 就走 Nginx能不让浏览器发起跨域请求就尽量不发起。实在需要后端配合时再把 CORS 头和预检处理用标准流程写清楚最后用 curl 验证一遍再交付联调。这套流程跑下来跨域问题基本不会卡你太久。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进