ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

CORS配置陷阱:为什么Access-Control-Allow-Origin通配符会坑你?

CORS配置陷阱:为什么Access-Control-Allow-Origin通配符会坑你? 我先说一句大实话作为一个被 CORS 坑过无数次的前后端开发者我第一次在浏览器控制台里看到下面这行红字时整个人是懵的Access to XMLHttpRequest at https://api.example.com/user from origin https://admin.example.com has been blocked by cORS policy: no access-control-allow-origin header is present那会儿组里最流行的“解法”有两个一是让后端在后端框架里加一行注解二是让运维在 Nginx 里加一行add_header Access-Control-Allow-Origin *;。加完之后页面刷新请求通了大家觉得问题解决了。直到后来某个版本联调登录态续期功能前端需要跨域携带 Cookie方案直接崩了而且崩溃方式非常诡异——响应头里明明能看到Access-Control-Allow-Origin请求还是被浏览器拦截。折腾了一整天才发现问题就出在那个看似无害的*上。如果你现在也正在被no access-control-allow-origin折磨或者你的同事正准备“先加个通配符跑通再说”这篇文章建议从头看到尾。我不打算绕弯子直接讲清楚 CORS 到底在保护什么、为什么*是个隐藏陷阱、正确配置应该长什么样以及常见的排错套路。适合要自己对接 API 的前端、写接口的后端还有帮团队调代理和网关的运维同学。1. 跨域问题的根源同源策略与 CORS 的设计逻辑很多人第一反应是“CORS 是后端配置的一个东西”其实它更像是浏览器制定的一套跨域访问规则板子。要真正理解Access-Control-Allow-Origin该怎么配首先得明白浏览器为什么要卡你。1.1 同源策略到底在保护什么浏览器的同源策略Same-Origin Policy是一个默认安全机制。它把“协议 域名 端口”组合起来定义成源Origin比如https://app.example.com:8443。当一个页面里的脚本试图去请求另一个源的资源时浏览器默认不允许页面读取响应内容。为什么要有这个机制最经典的案例是银行钓鱼。假设你登录了网银https://bank.com登录凭证存在 Cookie 里。此时你又打开了一个恶意站点https://evil.com这个站点的脚本偷偷向https://bank.com/api/transfer发起了转账请求。如果没有同源策略浏览器会老老实实带上bank.com的 Cookie 把请求发出去服务器以为是本人在操作转账就完成了。同源策略断掉了这条路——跨源页面发起请求并读取响应的时候浏览器会直接拦截哪怕请求真的到了服务器你的页面也拿不到任何返回同时控制台报错。需要注意同源策略卡的并不仅仅是“能不能发请求”而是“能不能发这个请求” “能不能读回响应”。所以它实际上是给“外部网页读取我的数据”上了一把锁。这把锁是浏览器层面的强制约定不是后端加个白名单就能绕过的——后端要做的是通过正确的 CORS 响应头告诉浏览器“这个外域请求是得到我允许的你可以放它过”。1.2 简单请求与预检请求CORS 的两条通路CORS 请求分两类第一类是简单请求第二类是预检请求Preflight。简单请求需要同时满足几个条件请求方法只能是GET、HEAD、POST请求头只能使用浏览器定义的少数安全字段比如Accept、Accept-Language、Content-Type里的application/x-www-form-urlencoded、multipart/form-data或text/plain不能自定义Authorization等头部。如果请求不满足这些条件比如用fetch发送application/json的 POST、或者携带自定义请求头、或者用了PUT/DELETE等方法浏览器会先发一个OPTIONS请求去“试探”服务器这个过程叫预检。预检请求的响应头里必须有Access-Control-Allow-Methods和Access-Control-Allow-Headers来声明服务器允许的方法和请求头之后浏览器才会发真正的业务请求。我把这两个流程的差异整理成了一张表方便对照对比维度简单请求预检请求触发条件GET/HEAD/POST请求头受限Content-Type 受限使用非简单方法PUT/DELETE等或携带非简单请求头是否有 OPTIONS 请求无有先发预检再发正式请求主要校验响应头Access-Control-Allow-OriginAccess-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers失败表现直接拦截正式请求预检请求被拦截报 preflight 相关错误理解这两条通路对排错很重要。你看到的no access-control-allow-origin header is present有时是正式业务请求被拦更多情况下其实是预检请求先被拦了只是浏览器导致最终呈现的业务请求的报错。1.3 CORS 相关响应头逐个拆解真正参与跨域判断的响应头有四个每个控制的内容完全不同Access-Control-Allow-Origin允许哪些源访问当前资源可以填具体源也可以填*。Access-Control-Allow-Methods允许哪些 HTTP 方法用于预检请求的应答。Access-Control-Allow-Headers允许哪些自定义请求头预检请求里会带上Access-Control-Request-Headers服务器要在这里回应。Access-Control-Allow-Credentials是否允许跨域请求携带 Cookie 和凭证。这四个头缺一不可很多人只配了第一个导致预检总是失败。后面我会拿一个实际例子逐条演示。2. “Access-Control-Allow-Origin: *” 是配置陷阱那么问题来了*明明写起来最简单为什么我不建议你用因为它带来的是指数级放大的安全和兼容性成本。2.1 安全边界通配符等于对全互联网开放读权限Access-Control-Allow-Origin: *的字面意思是任意一个网站都能跨域读取你这个接口的响应。对一个公开的天气接口、新闻接口来说可能还能接受但如果你是在开发一个带用户体系的后台、一个只有自家前端才消费的业务 API这等于把你家大门钥匙复制了一份给所有路过的人。有人会争辩说“反正服务器端也有登录校验别人读不到真实数据”。但你要注意CORS 是浏览器层面的策略它只控制“浏览器标签页里的脚本可以拿到什么”并不代表服务器没有暴露风险。举几个现实里很容易被忽略的场景攻击者在自己的站点上写一个脚本调用你的 API。如果你的鉴权依赖 Cookie并且浏览器会自动携带 Cookie那么用户的会话就会被恶意站点利用。即使你用了 Token 鉴权Token 放在localStorage里通常不会自动带上但攻击者可以从其他漏洞拿到 Token再配合*轻松发起跨域读取。日志和监控层面*会让你分不清请求来自自家站点还是爬虫还是恶意扫描器因为Origin字段五花八门全被放行了。对大多数业务系统来说“允许不明来源读取资源”和“允许不明来源访问服务器”是两件事但前者往往会成为后者的跳板。安全设计讲究最小权限Access-Control-Allow-Origin也应该遵循这个原则。2.2 与 credentials 的组合限制* 会让带 Cookie 的请求直接失效这是*最阴险、也最容易被踩的坑。规范里写得很明确如果响应头里带了Access-Control-Allow-Credentials: true那么Access-Control-Allow-Origin就不允许是*必须指定具体源。为什么这么规定因为*的语义是“允许任何源携带凭证”这在安全上等于放行所有跨站身份伪造。浏览器会直接拒绝这种响应。于是你看到的结果是后端配了Access-Control-Allow-Origin: *前端用了fetch(url, { credentials: include })响应头里明明有Access-Control-Allow-Origin: *请求还是被拦截控制台报的还是no access-control-allow-origin header特别有误导性。我真实遇到过这类报错排查了快一天才明白原来是同时配了*和Access-Control-Allow-Credentials: true浏览器把整个响应按无效处理。只要把*改成具体的源立刻就好了。2.3 什么时候用*也没问题我不是说*完全不能碰。如果你的接口满足下面所有条件用*反而简单省事接口完全公开没有身份认证不含任何用户隐私数据不需要携带 Cookie、不需要Authorization头也不依赖浏览器自动附带的身份信息业务上确实希望任何第三方站点都能调用比如公开的 SDK、开放平台的基础数据接口。即便如此我也建议在服务端把它收敛成“仅对公开接口生效”而不是全局配置一把梭。因为你的系统大概率还有管理接口、内部接口这些最好都走白名单。退一万步讲就算现在所有接口都公开谁能保证三个月后新加的用户中心接口不会从这种宽松配置里面漏出去3. 正确配置的完整实操方案说完了理论接下来上真东西。我先给出一套我推荐的通用思路再分别给出常见技术栈的配置示例。3.1 通用配置思路Origin 白名单 动态回显正确做法的核心就两句话维护一个允许请求的源Origin白名单收到请求时动态判断只在命中白名单的情况下回显对应的 Origin。这样既能让合法的跨域请求拿到正确的Access-Control-Allow-Origin又能对不在白名单里的源直接不返回这个头浏览器就会自动拦截。这套思路的优势是不依赖通配符可以对不同环境、不同子域精确控制配合Access-Control-Allow-Credentials: true依然可用因为回显的是具体源便于扩展将来加域名只需改白名单列表不需要重启改头配置如果希望更严谨还可以在白名单匹配失败时让服务端返回 403而不是继续处理业务请求。配上凭证模式有一个注意事项要特别记牢Access-Control-Allow-Origin必须是具体源且还要加上Vary: Origin响应头。原因很简单同一次服务端响应因为请求里的 Origin 不同返回的Access-Control-Allow-Origin也会不同。如果中间有缓存层比如 CDN 或浏览器缓存它可能把上一次的Allow-Origin缓存住再发给另一个来源那就乱套了。Vary: Origin告诉缓存代理“这个响应是依赖 Origin 的请按 Origin 分别缓存”。3.2 方案一静态白名单配置适合固定域名场景如果前端域名是固定的一个或几个比如https://app.example.com和https://admin.example.com直接用白名单匹配最省心。下面是 Node.js Express 的中间件示例const ALLOWED_ORIGINS new Set([ https://app.example.com, https://admin.example.com, http://localhost:5173 // 开发环境 ]); app.use((req, res, next) { const origin req.headers.origin; if (origin ALLOWED_ORIGINS.has(origin)) { res.setHeader(Access-Control-Allow-Origin, origin); res.setHeader(Vary, Origin); } res.setHeader(Access-Control-Allow-Methods, GET, POST, PUT, DELETE, PATCH, OPTIONS); res.setHeader(Access-Control-Allow-Headers, Content-Type, Authorization, X-Requested-With); res.setHeader(Access-Control-Allow-Credentials, true); res.setHeader(Access-Control-Max-Age, 86400); if (req.method OPTIONS) { return res.sendStatus(204); } next(); });这段代码有几个细节值得说白名单命中时Access-Control-Allow-Origin回显来自请求的Origin而不是写死成某一个值这样多域名场景不用重复配。同时加了Vary: Origin防止代理服务器误缓存。设置Access-Control-Allow-Credentials: true这是携带 Cookie 的跨域请求必需的。Access-Control-Max-Age设置预检请求结果缓存时间为 86400 秒24小时减少浏览器频繁发 OPTIONS 请求的压力。OPTIONS直接返回 204不再进入业务路由避免把预检请求当业务请求处理导致 404 或 405。如果你的框架是 Spring Boot可以用基于配置类的方式实现同样的白名单Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins( https://app.example.com, https://admin.example.com, http://localhost:5173 ) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(86400); } }Spring Boot 会帮你自动处理预检请求和响应头不需要手动写中间件。但要注意.allowedOrigins()不接受*传入因为allowCredentials(true)和*不能同时出现这也是框架替你规避了最常见的坑。3.3 方案二动态 Origin 回显 白名单匹配适合多环境、多域名场景如果你的前端域名时不时会变或者你希望从配置中心拉取白名单动态方案更灵活。核心逻辑是每次请求时获取请求头的Origin值检查是否在动态维护的白名单比如数据库、配置中心里命中了就动态设置响应头。拿 NGINX 举例运维同学最关心这个。在server块里加set $cors_origin ; if ($http_origin https://app.example.com) { set $cors_origin $http_origin; } if ($http_origin https://admin.example.com) { set $cors_origin $http_origin; } if ($request_method OPTIONS) { add_header Access-Control-Allow-Origin $cors_origin; add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS; add_header Access-Control-Allow-Headers Content-Type, Authorization, X-Requested-With; add_header Access-Control-Allow-Credentials true; add_header Access-Control-Max-Age 86400; add_header Content-Type text/plain charsetUTF-8; add_header Content-Length 0; return 204; } if ($request_method ! OPTIONS and $cors_origin ! ) { add_header Access-Control-Allow-Origin $cors_origin; add_header Access-Control-Allow-Credentials true; add_header Vary Origin; }这里有三个关键点白名单没有命中时$cors_origin是空字符串add_header不会添加Access-Control-Allow-Origin浏览器就会正常拦截。不满足条件时不添加Vary: Origin也要注意一旦开始根据 Origin 动态输出响应头就必须带上Vary: Origin否则前面提到的缓存错乱问题会出现。这里用if在 NGINX 里做判断是常见的rewrite模块技巧但如果你对 NGINX 配置特别敏感会更推荐用 Lua 或者把白名单挂到 OpenResty不过大多数中小项目用if足够了。3.4 配置后的验证方法配置完别急着找前端联调先自己在命令行里用curl验证一下。模拟一个从https://app.example.com发起的预检请求curl -i -X OPTIONS https://api.example.com/api/user \ -H Origin: https://app.example.com \ -H Access-Control-Request-Method: POST \ -H Access-Control-Request-Headers: Content-Type, Authorization看响应头你应该看到类似这样的内容HTTP/1.1 204 No Content Access-Control-Allow-Origin: https://app.example.com Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS Access-Control-Allow-Headers: Content-Type, Authorization Access-Control-Allow-Credentials: true Vary: Origin再把Origin换成一个不在白名单里的域名试试比如curl -i https://api.example.com/api/user -H Origin: https://evil.com正常配置下响应头里不应当出现Access-Control-Allow-Origin这样浏览器就会拦截来自evil.com的跨域读取。如果这里依然回显了白名单以外的 Origin就说明配置还有漏洞需要回头检查。4. 常见问题与排查技巧实录技术文章写多了容易“纸上谈兵”这里我把这几年遇到的真实报错和排查过程整理成速查表你遇到类似问题时可以直接对照。4.1 CORS 报错速查表报错信息或现象大概率原因处理建议no access-control-allow-origin header is present后端没有配置 CORS或配置未命中检查响应头里是否真的有Access-Control-Allow-OriginResponse to preflight request doesnt pass access control check预检请求未返回正确的Access-Control-Allow-Methods/Access-Control-Allow-Headers用 curl 模拟 OPTIONS 请求检查预检响应头配置了*还是报no access-control-allow-origin同时设置了Access-Control-Allow-Credentials: true与*冲突改为具体 Origin 回显跨域请求带 Cookie 总是不生效前端没设withCredentials或credentials: include前端同步修改请求配置请求报 401/403但控制台同时报 CORS预检通过但业务鉴权失败先看网络面板里的实际响应和 CORS 解耦排查GET 请求正常POST 带 JSON 不正常触发了预检后端未处理 OPTIONS确认服务器对 OPTIONS 请求返回 204本地开发正常线上报 CORS你的前端域名和线上不一致未加入白名单把线上域名加进白名单上线后偶发 CORS 报错刷新又好了缓存了旧的Access-Control-Allow-Origin检查是否缺少Vary: Origin4.2 排查 CORS 问题的固定套路我总结了一个四年多排错里反复在用的检查顺序基本覆盖大多数情况看 Network 面板打开浏览器开发者工具切到 Network找到被拦截的请求看两个东西——请求方法是不是OPTIONS响应头里有没有access-control-*关键字。区分失败点如果是OPTIONS预检失败问题一定在后端没有正确响应预检如果是业务请求失败则要重点看Access-Control-Allow-Origin是否匹配、是否与credentials冲突。用 curl 模拟复现把浏览器的请求头原样搬到 curl发一个带Origin的请求直接看完整的响应头。这一步能立刻区分“后端没配置”还是“前端使用方式不对”。检查是否被代理层吃掉如果请求经过 Nginx、API 网关、CDN 其中任何一层都要确认这些层没有把响应头过滤掉。很多企业环境里前置网关/防火墙有响应头清洗功能默认会把Access-Control-*头剥离这种时候后端配得再对也没用。确认前端凭证模式如果请求带着 Cookie 或涉及登录态检查前端是否设置了withCredentials/credentials: include以及后端是否返回Access-Control-Allow-Credentials: true。4.3 几个容易被忽视的坑第一个坑发起方是Origin: null。sandbox属性的 iframe、file://协议打开本地页面、某些前端的重定向场景Origin 头会是字面量null不是具体域名。白名单判断时如果直接拿null去includes必然失败。处理方式有两种要么在白名单里显式允许null小心安全成本要么用正则做一个“非空字符串且域名格式合法”的前置判断。第二个坑只给业务接口配置了 CORS却没有给静态资源或下载接口配置。浏览器加载跨域字体、图片时也会触发 CORS 检查如果字体文件的“字体跨域”配置没跟上页面里字体图标全部消失报错却是从 CORS 来的很多人会找错方向。需要静态资源跨域的地方也要加上相应的允许头。第三个坑复杂的多级跨域。前端a.com请求b.comb.com后端又要调c.com的服务。很多人在b.com写死了Access-Control-Allow-Origin: https://a.com结果b.com去调c.com时又触发了c.com的 CORS 策略。如果服务端调用是通过同一个 HTTP 客户端发起的这一步不是浏览器强制拦的而是服务端自己需要配置好对b.com的允许。跨服务调用的 CORS 逻辑和浏览器端是完全独立的两套排查时要分开看。第四个坑响应头被中间层合并覆盖。有些反向代理会把自己的Access-Control-Allow-Origin: *追加到后端响应头之后浏览器读取时如果发现存在多个同名头且内容不一致可能直接判定非法。这时候后端配得再严谨也白搭。用代理层统一管理 CORS 时最好只在一层配置其他层负责透传别做多层叠加。4.4 关于“已经上了生产环境怎么平稳修复”如果正在运行的线上系统用着*不要慌也不要趁大半夜直接改配置。稳妥的做法是第一步先在网关层加一个白名单判断只放你自己前端的 Origin其余源一律不返回Access-Control-Allow-Origin。第二步观察一段时间日志确认没有合法业务被拦截。第三步再把后端代码里的*改成白名单动态回显并把网关层的规则收掉避免两边规则叠加。这样操作的好处是出了问题可以快速回滚不会一次性动到底层导致前端大面积报错。5. 最后分享几个我实践中积累的小建议踩了几年坑有一点体会特别深CORS 不是后端单方面能搞定的东西它是前后端一起协商出来的协议。后端配了允许源前端还得设置正确的凭证模式后端支持了预检前端还得检查自己有没有触发非简单请求。所以每到一个新项目我会先定一个团队内部约定所有 API 统一走/api前缀跨域配置只在这个前缀上做前端默认用fetch并且明确要求所有项目成员在请求时写明credentials是怎么设置的。另外配置完 CORS 后别只顾着“通了”就完事强烈建议在后端加一个自检接口比如/api/cors-test它返回当前请求的Origin和服务器决定响应的Access-Control-Allow-Origin。联调时前端拿这个接口一打是白名单没匹配上、还是后端头配置漏了一眼就能分清。这比每次都在开发者工具里翻半天响应头高效得多。最后再补一个小技巧如果你用的是 Chrome装一个叫CORS Unblock的扩展可以在本地开发时临时绕过跨域限制但千万别在排查生产问题的时候开着它否则你会以为一切正常最后上线才发现该拦的没拦、不该拦的也没拦那种滋味我尝过一次就够了。
返回列表