ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CORS 多域名跨域:Allow-Origin 白名单反射实战

CORS 多域名跨域:Allow-Origin 白名单反射实战 1. 一个响应头为什么装不下三个域名Access-Control-Allow-Origin这个响应头我在过去几年里被问到的次数大概仅次于 404。问法几乎一模一样我们前端有www、app、m三个站点还有几个合作方的页面要调我们的接口为什么这个头不能直接写三个域名用逗号隔开行不行这个问题本身没错错的是提问的默认前提——它假设Access-Control-Allow-Origin是一个列表型的头而它从设计之初就是个单值的头。这篇就围绕 CORS 跨域里 Access-Control-Allow-Origin 面对多域名需求时的处理方式把协议层面的约束、常见的错误写法、动态反射的正确姿势、以及各个技术栈的落地代码一次讲透。适合已经跑通过最简单的跨域配置、但一遇到多域名和带 Cookie 就抓瞎的同学。1.1 先把跨域这个词的歧义说清楚搜索跨域的时候你会看到一堆看起来毫不相干的结果BGP 跨域对接、跨时钟域处理、PCIe 弹性缓存怎么搞定跨时钟域、跨时钟域握手。这些和浏览器安全模型里的跨域完全是两码事。硬件和网络工程里的跨域指的是跨越两个时钟域或两个自治系统解决的是信号同步和路由可达性而前端和后端天天吵的跨域指的是同源策略下的资源访问限制解决的是浏览器凭什么允许 A 站点的脚本读取 B 站点的响应。之所以要先花一段把这个歧义点破是因为我见过不止一个团队在搜索资料时被带偏把时钟域同步的方案当成了 CORS 的解决方案。你只需要记住一句话本文讨论的跨域全程是浏览器同源策略语境下的 CORS跟证书、跟路由协议、跟时钟频率都没关系。同源策略判定的三要素是协议、域名、端口三者任意一个不同就算跨域。https://www.example.com和https://api.example.com不同源http和https不同源8080和8000不同源。1.2 Access-Control-Allow-Origin 的取值只有一个位置按照 Fetch 标准的定义Access-Control-Allow-Origin的值只能是下面三种之一一个单独的*或者一个单独的 origin形如https://www.example.com注意不带路径、不带结尾斜杠或者字面量null。标准里没有任何列表的语法。你写https://a.com, https://b.com浏览器解析这个值的时候会把它当成一个完整的字符串去和当前页面的 origin 做逐字符比对比对结果必然是不相等于是判定为不允许。这里有个细节很多人没意识到浏览器不是取第一个或者取最后一个它是整串比较。所以逗号拼接的写法在某些老版本浏览器里可能碰巧能用纯粹是因为解析实现的历史差异不是规范允许的行为换一个浏览器就崩。有一类特殊的场景是响应头重复出现多次比如Access-Control-Allow-Origin: https://a.example.com Access-Control-Allow-Origin: https://b.example.com用 curl 看确实能看到两行但浏览器在做 CORS 校验时对于单值头会把重复的值按逗号合并成一个字符串结果还是https://a.example.com, https://b.example.com回到上面那个逐字符比对的死路。所以这条路也是走不通的。1.3 浏览器收到不匹配的值之后做了什么理解浏览器的处理顺序对排查问题非常关键。一次跨域请求大致是这样走的浏览器发现请求跨域先判断是不是简单请求如果是简单请求就直接发出去拿到响应后检查Access-Control-Allow-Origin是否和当前 origin 精确匹配不匹配的话响应体虽然已经到了浏览器但不会交给 JS控制台报错fetch直接 reject。如果是非简单请求比如带Content-Type: application/json、带自定义头、或者用了 PUT/DELETE浏览器会先发一个OPTIONS预检请求。这个预检请求本身也要通过 CORS 校验校验不通过的话真正的业务请求压根就不会发出去。这就解释了为什么有些同学在服务端日志里只看到 OPTIONS 记录看不到 POST 记录——请求被浏览器拦在了预检阶段。我在实际排查时常用的判断方法是看服务端访问日志。如果日志里有 OPTIONS 但没有后续的真实请求问题一定在预检响应头如果真实请求发出了、响应也 200 了但前端还是报错问题就在实际响应的头或者中间的缓存层。2. 四种看起来能行的写法实测全都不行每次有人问多域名怎么办我通常会先让他把已经试过的写法讲一遍。有意思的是绝大多数人试过的坑是高度重合的基本就那么四种。把它们逐一拆开讲清楚为什么不成立比直接甩一个正确方案有用得多因为你知道边界在哪里下次就不会再绕回去。2.1 逗号拼接最像答案的错误答案Access-Control-Allow-Origin: https://a.com,https://b.com这种写法之所以流行是因为 HTTP 里确实有大量头是允许逗号分隔列表的比如Accept、Cache-Control、Access-Control-Allow-Headers、Access-Control-Allow-Methods。很多人凭直觉认为Allow-Origin也一样。但规范明确把它定义成单值。我实测过的表现是Chrome 和 Firefox 都会直接拒绝控制台给出的提示是The Access-Control-Allow-Origin header contains multiple values https://a.com, https://b.com, but only one is allowed.。注意这句话的措辞——contains multiple values浏览器已经明确告诉你它把这串东西当成了多个值而多个值是不允许的。有意思的是同样的问题在Access-Control-Expose-Headers上就是合法的因为那个头本来就支持列表。这种同一组头里有的支持列表有的不支持的不一致是 CORS 规范里最容易让人翻车的地方之一。2.2 重复设置响应头curl 能看到浏览器不认这个坑的迷惑性更强因为你在本地用 curl 调试的时候确实能看到两行Access-Control-Allow-Origin看起来配置生效了。于是以为是浏览器缓存问题清了缓存重试还是不行。原因就是上面说的合并规则。另外很多 Web 服务器和框架在处理重复头时的行为并不一致Nginx 的add_header如果写在嵌套的location里可能只在某一层生效某些中间件在设置已经存在的头时会追加而不是覆盖。这就导致你本地测出来和线上表现不一样。我一般建议在排查阶段用curl -i看原始响应一旦发现有重复的Access-Control-Allow-Origin先别管值对不对先把重复的去掉确保只有一个来源在设置这个头。多来源叠加设置同一个 CORS 头是多域名场景下极常见的隐性故障源——比如网关设了一遍、应用里 Spring 的CrossOrigin又设了一遍。2.3 无脑返回*简单但会丢掉 CookiesAccess-Control-Allow-Origin: *是所有方案里最省事的一行搞定所有域名。如果你们的接口是纯公开的、不需要携带用户身份那这就是最优解没必要搞复杂。但只要涉及登录态这条路立刻断掉。当请求带了credentialsfetch的credentials: include、XHR 的withCredentials true浏览器会强制要求Access-Control-Allow-Origin不能是*而且Access-Control-Allow-Credentials必须是true。此时如果服务端返回*浏览器直接拒绝报错信息大致是The value of the Access-Control-Allow-Origin header in the response must not be the wildcard * when the requests credentials mode is include。这个限制的动机很直白*表示任何站点都可以读这个响应。如果不加限制你带着用户 Cookie 请求银行接口攻击者的页面也能读到响应内容那就是 CSRF 的加强版了。所以规范把*和credentials设计成互斥逼你显式列白名单。顺带说一个安全提醒搜索里那条cors 配置错误反射 origin credentialstrue说的就是这个场景。如果你的服务端把请求里的Origin原样反射回去不做任何校验同时credentials又是true那效果等同于把用户登录态暴露给任意站点。这个配置错误在扫描器眼里是一个非常明确的高危信号。白名单校验不是可选项。2.4 前端一个域名一个域名地改还有一种方案是每次有新前端域名就去后端加一个常量重新发版。单域名或者域名很少且极少变更的团队这么干也不是不行至少没有安全风险。但它的问题在于组织成本。域名一多谁来维护这个列表测试环境、预发环境、生产环境各有一批域名本地开发还有http://localhost:5173这种随机的端口。每次加域名都要走一遍发版流程时间长了列表必然会乱出现这个域名删了但没人知道它在用的情况。一旦误删线上就是整个站点跨域失败。我的经验是把允许的来源做成可配置的白名单配置中心、环境变量、配置文件都行代码里的匹配逻辑保持固定加域名变成改配置而不是改代码这样维护成本会低很多。3. 白名单 动态反射多域名场景的标准答案绕了一圈多域名场景下的正确做法其实只有一个词反射。服务端读取请求头里的Origin拿它去和你维护的白名单做比对比对通过就把这个 origin 原样写回Access-Control-Allow-Origin比对不通过就不写这个头或者返回 403。这样每次响应里始终只有一个值符合规范同时支持任意多个域名。3.1 反射的完整逻辑完整流程拆成四步第一步取Origin请求头注意它可能是空的同源请求、服务端到服务端调用、部分特殊场景下会是null第二步做归一化处理比如统一转小写、去掉末尾斜杠避免因为大小写差异导致误判第三步用白名单做精确匹配第四步匹配成功后设置三个头——Access-Control-Allow-Origin写成该 origin、Access-Control-Allow-Credentials: true、Vary: Origin。关键点在于精确匹配。我见过太多人出于方便写成origin.includes(example.com)这一行代码就足以让白名单形同虚设因为https://evil-example.com和https://example.com.attacker.net都能通过。正确做法是把白名单里的每个域名做一次标准化然后用全等比较或者如果确实需要通配子域名用严格锚定的正则比如^https://[a-z0-9-]\.example\.com$确保^和$都在。还有个小细节如果你要支持本地开发白名单里通常会加http://localhost:5173这类条目。建议把这类条目和环境绑定只在开发/测试环境的配置里出现生产环境的白名单保持干净。我见过生产配置里躺着http://localhost:3000的虽然不是致命问题但审代码的人看到会问一句。3.2 Vary: Origin 为什么不能省这是整个多域名方案里最容易被漏掉、又最容易在生产环境炸掉的一行。Vary: Origin的作用是告诉所有的中间缓存CDN、反向代理、Nginx 的 proxy_cache——这份响应内容会随请求头Origin的不同而不同缓存时要把 Origin 也算进缓存键。如果你不写这一行会发生什么设想https://a.example.com第一个发请求服务端反射回来Access-Control-Allow-Origin: https://a.example.comCDN 把这份完整响应缓存下来。紧接着https://b.example.com发同样的请求CDN 直接命中缓存把带着https://a.example.com的响应吐给它。B 站点的浏览器一看Allow-Origin不匹配跨域失败。这种故障最坑的地方是它是间歇性的、跟访问顺序相关的。本地开发永远复现不出来因为本地没有缓存层。上线之后偶发而且换个顺序访问又好了排查起来能耗掉一整天。我第一次遇到这个问题的时候硬是先把 CDN 缓存清了一遍问题消失以为解决了过了半天又出现才想起来查响应头里有没有Vary。3.3 校验代码放在哪一层放在哪一层取决于你们的架构。如果有统一的 API 网关放网关层最省事一次配置管住所有后端服务而且后端服务不用关心 CORS。如果没有网关就用各框架自带的 CORS 中间件放在所有业务路由之前。最不建议的做法是在每个业务 Controller 或者每个处理函数里手写header(Access-Control-Allow-Origin: ...)。这样做的后果是响应头散落在几十个地方改白名单要全局搜索而且很容易漏掉某个分支路径比如异常处理路径忘了加导致错误响应没有 CORS 头前端看到的是一个没有 CORS 头的 500误以为是跨域问题。另外要注意顺序问题CORS 中间件必须在鉴权中间件之前执行。因为预检请求OPTIONS通常是不带 Cookie 的如果鉴权中间件拦在 CORS 前面预检请求会先被 401 掉浏览器拿不到 CORS 头业务请求根本发不出去。这个顺序问题在 FastAPI、Express、Spring 里都踩过表现形式完全一样所有需要预检的接口全部跨域失败但 GET 简单请求正常。4. 带上 Cookie 之后规则会更严带凭证的跨域请求是另一个量级的复杂度。前面提到*和credentials互斥但实际开发中还有几个细节值得单独拎出来讲因为它们在文档里往往一笔带过在实践里却经常咬人。4.1 前端也要配对设置服务端配了Access-Control-Allow-Credentials: true只是完成了一半前端必须同步声明要带凭证。用fetch是credentials: include用 axios 是withCredentials: true用原生 XHR 是xhr.withCredentials true。如果前端没设Cookie 不会跟着发服务端拿不到会话表现为跨域通过了但用户一直是未登录状态。跨域场景下 Cookie 还有额外的限制这个限制比 CORS 本身更容易让人困惑即使请求成功响应里的Set-Cookie也可能被浏览器丢弃。原因是现在主流浏览器要求SameSiteNone的 Cookie 必须同时带Secure而SameSiteNone是跨站发送 Cookie 的前提。如果服务端下发的会话 Cookie 是默认的SameSiteLax跨站请求就不会带上它。我见过不少团队在这个点上耗了很久最后发现是 Cookie 属性没配对跟 CORS 配置一点关系都没有。这里给出一个可以照抄的排查清单遇到跨域带了 Cookie 但登录态丢了的时候按顺序过一遍检查项期望值常见错误响应头 Allow-Origin具体域名不能是*用了通配符响应头 Allow-Credentialstrue忘了设或拼写错误请求是否带 Cookie请求头里有Cookie前端没开 credentialsCookie 的 SameSiteNone默认的LaxCookie 的 Secure有只在 https 下需要Cookie 的 Domain与请求域名匹配写成了错误的顶级域名4.2 白名单匹配的两个经典漏洞第一个是后缀匹配。origin.endsWith(example.com)这种写法会被https://notexample.com命中。一定要加上分隔符判断或者干脆用全等。第二个是正则没锚定。[a-z]\.example\.com这种模式如果不加^和$https://evil.com/?xa.example.com这类构造就可能绕过。写白名单正则的时候我的习惯是先写测试用例把明显应该拒绝的字符串列出来跑一遍通过了再上线。还有一类边界情况值得提一句Origin: null。这个值会出现在file://页面、沙箱化的 iframe、以及某些重定向场景里。如果你的白名单里有null这一项等于对所有来源开放——任何攻击者都能用一个沙箱 iframe 伪造出null的 Origin。所以除非你非常清楚自己在做什么白名单里不要出现null。4.3 OPTIONS 预检的短路处理预检请求要尽可能早地返回不要让它穿透到业务逻辑层。理想的处理方式是CORS 中间件识别到OPTIONS且带Access-Control-Request-Method头直接返回 204带上完整的 CORS 响应头不进入业务路由。这么做有两个好处。一是性能预检请求量不小每个真实请求前面都可能跟一个 OPTIONS让它走完整条链路数据库连接、鉴权、日志纯属浪费。二是避免误报——如果 OPTIONS 打到了业务代码很可能因为参数校验失败返回 400 或者 500这个响应如果没有 CORS 头前端会报跨域错误把真正的错误信息掩盖掉排查方向直接跑偏。预检结果的缓存由Access-Control-Max-Age控制单位是秒。设大一点比如 600 到 3600能显著减少预检请求数量。但注意 Chrome 对这个值有上限目前是 2 小时设再大也没用。另外如果白名单是动态变化的缓存时间设太长会导致新加的域名要等缓存过期才生效这个权衡要根据实际情况定。5. 五个技术栈的落地写法前面讲的都是原理这一段给可以直接抄的代码。每个框架内部对多域名的处理机制不太一样有的直接支持传数组有的需要自己写中间件还有的需要借助服务器层的能力。5.1 Nginx 网关层用 map 做白名单在网关层统一处理的好处是后端服务完全不用关心 CORS。Nginx 的map指令可以在配置阶段就把白名单映射好map $http_origin $cors_origin { default ; ~^https://(www|app|m)\.example\.com$ $http_origin; } server { listen 443 ssl; server_name api.example.com; location /api/ { add_header Access-Control-Allow-Origin $cors_origin always; add_header Access-Control-Allow-Credentials true always; add_header Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS always; add_header Access-Control-Allow-Headers Content-Type, Authorization, X-Requested-With always; add_header Access-Control-Max-Age 600 always; add_header Vary Origin always; if ($request_method OPTIONS) { return 204; } proxy_pass http://backend; } }这里有几个坑必须说。第一add_header默认只对 2xx 和 3xx 生效错误响应4xx、5xx不会带上这些头一定要加always参数。这个坑的典型表现是接口正常时跨域没问题一旦后端报错前端看到的就是一个没有 CORS 头的错误响应控制台提示跨域失败实际是业务报错。第二map里的正则如果匹配不上返回空字符串此时add_header Access-Control-Allow-Origin 会设置一个空值的头规范上这是不合法的。严格来说应该用if判断空值时不加这个头但 Nginx 的if有邪恶之名能不用就不用。实际工程中空值头的表现通常是浏览器判定不匹配然后拒绝效果和没设一样可以接受。5.2 Spring Boot直接传数组Spring 的 CORS 支持是内建白名单机制的allowedOrigins传一个数组它内部会自动读请求的 Origin 做匹配然后反射不需要你手写。Configuration public class CorsConfig implements WebMvcConfigurer { private static final ListString ALLOWED List.of( https://www.example.com, https://app.example.com, https://m.example.com ); Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(ALLOWED.toArray(new String[0])) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }要注意allowedOrigins和allowCredentials(true)的组合。如果两个都用通配符Spring 会在启动时直接抛异常——这是它在帮你避开那个安全陷阱。另外如果你用了 Spring Security还需要在 Security 的过滤链里显式开启 CORShttp.cors()否则请求会在 Security 层被拦掉Spring MVC 的配置根本执行不到。这是 Spring 项目里配置写了但不生效的最常见原因。5.3 FastAPIStarlette 中间件自带反射FastAPI 的 CORS 是 Starlette 提供的allow_origins传列表内部会做精确匹配然后反射行为符合预期。from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app FastAPI() app.add_middleware( CORSMiddleware, allow_origins[ https://www.example.com, https://app.example.com, ], allow_credentialsTrue, allow_methods[GET, POST, PUT, DELETE, OPTIONS], allow_headers[Content-Type, Authorization], max_age600, )有个细节要注意allow_origins和allow_origin_regex是两个独立参数配了 regex 之后能满足更复杂的匹配需求但正则写错的风险也随之增加。另外 FastAPI 里中间件的注册顺序是后注册的先执行如果你的鉴权中间件写在 CORS 中间件后面注册它会先跑预检请求就会被拦。这个顺序反直觉我在项目里踩过一次表现就是所有 POST 跨域失败、GET 正常。5.4 Express手写中间件更可控Node 生态里cors这个包用得最多它的origin参数支持传数组、传函数、传正则。传数组时它内部做的就是白名单匹配加反射。如果你想完全掌控逻辑手写一个也不复杂const ALLOWED new Set([ https://www.example.com, https://app.example.com, ]); app.use((req, res, next) { const origin req.headers.origin; if (origin ALLOWED.has(origin)) { res.setHeader(Access-Control-Allow-Origin, origin); res.setHeader(Access-Control-Allow-Credentials, true); res.setHeader(Access-Control-Allow-Methods, GET,POST,PUT,DELETE,OPTIONS); res.setHeader(Access-Control-Allow-Headers, Content-Type,Authorization); res.setHeader(Access-Control-Max-Age, 600); } res.setHeader(Vary, Origin); if (req.method OPTIONS) { return res.sendStatus(204); } next(); });用Set做精确匹配天然避免了后缀匹配的漏洞。Vary头无条件设置不管是否命中白名单这样缓存层的隔离更彻底。5.5 PHP 与 JSONP 的遗留场景老项目里还有 PHP 直接用header()输出的情况逻辑和上面一样$allowed [https://www.example.com, https://app.example.com]; $origin $_SERVER[HTTP_ORIGIN] ?? ; if ($origin ! in_array($origin, $allowed, true)) { header(Access-Control-Allow-Origin: . $origin); header(Access-Control-Allow-Credentials: true); } header(Vary: Origin); if ($_SERVER[REQUEST_METHOD] OPTIONS) { header(Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS); header(Access-Control-Allow-Headers: Content-Type, Authorization); header(Access-Control-Max-Age: 600); http_response_code(204); exit; }至于 JSONP它和 CORS 是两套并行的机制。JSONP 靠script标签不受同源策略限制这一点来绕过跨域只支持 GET而且需要服务端配合返回可执行的 JS 回调。现在还有少量存量接口在用但新项目没有任何理由再用它。注意 JSONP 和 CORS 不能混着配如果同一个接口既处理 JSONP 回调又设置 CORS 头很容易出现响应体格式和响应头不匹配的诡异问题。6. 从浏览器报错倒推配置错误CORS 的报错信息其实写得挺清楚只是很多人不去读直接搜索报错全文然后抄一段配置抄完发现不对再搜。我整理一下几种常见报错对应的真实病因。6.1 三句报错文本分别是什么意思第一句has been blocked by CORS policy: No Access-Control-Allow-Origin header is present on the requested resource。字面意思就是响应里完全没有这个头。可能原因服务端没配 CORS、CORS 配置没生效中间件顺序问题、或者响应是 4xx/5xx 而 Nginx 的add_header没加always。第二句The Access-Control-Allow-Origin header contains multiple values ... but only one is allowed。响应头里出现了多个值多半是重复设置或者逗号拼接。第三句The value of the Access-Control-Allow-Origin header in the response must not be the wildcard * when the requests credentials mode is include。返回了*但请求带了凭证。改成反射具体 origin。还有一个容易混淆的情况是响应本身是 502 或 504网关直接把错误页返回了压根没到后端这种情况报错文本同样是没有 Allow-Origin 头但根因是后端服务挂了跟跨域配置无关。所以看到跨域报错的第一反应不要是改 CORS 配置先看 Network 面板里这个请求的 status code 和完整响应头。6.2 用 curl 复现预检请求浏览器里能看到的调试信息有限用 curl 模拟预检请求能拿到最原始的响应curl -i -X OPTIONS https://api.example.com/user/1 \ -H Origin: https://www.example.com \ -H Access-Control-Request-Method: PUT \ -H Access-Control-Request-Headers: content-type,authorization curl -i https://api.example.com/user/1 \ -H Origin: https://www.example.com第一条发预检第二条发实际请求。对比两次响应的头如果预检有Allow-Origin而实际请求没有那说明你的 CORS 逻辑只处理了 OPTIONS 分支漏了正常响应分支Nginx 里的if写法很容易出这个问题。如果两次都没有说明 CORS 配置根本没生效。6.3 打包部署后突然不通的典型原因搜索里那条 vuedjango 打包部署后无法跨域 是很典型的场景。本地开发时 Vue 通过 vite 或 webpack 的 devServer 代理转发请求浏览器看到的是同源请求压根不触发 CORS所以一切正常。打包部署之后前端静态文件在 Nginx 上后端 Django 在另一个端口或域名变成真跨域了才发现后端从来没配过 CORS。对应的另一条热搜 vue 配置跨域代理后如何获取我的真实的请求地址讲的是代理带来的另一个副作用代理层把请求转发给后端之后后端通过request.get_host()拿到的是代理的地址不是用户实际访问的域名。如果后端有基于 Host 的逻辑比如生成回调地址、拼绝对 URL需要代理层透传原始 HostNginx 的proxy_set_header Host $host;后端也要相应地从X-Forwarded-Host之类的头里读取。这两个问题的共同点是开发环境的便利配置掩盖了生产环境的问题。我的建议是本地开发也尽量模拟真实拓扑或者至少在部署到测试环境时完整走一遍真实域名的跨域流程别等到上线才发现。7. 几个容易被忽略的边界7.1 CDN 缓存污染前面提过Vary: Origin这里再强调一下它在 CDN 场景下的具体表现。有些 CDN 默认不把Origin纳入缓存键即使你设置了Vary也可能需要在 CDN 控制台额外配置。判断方法很简单用两个不同的 Origin 依次请求同一个 URL看两次返回的Access-Control-Allow-Origin是否不同。如果第二次拿到的是第一次的值缓存就污染了。另外如果接口响应本身是可以公开缓存的静态数据还有一种思路是干脆不带凭证、返回*把 CORS 问题降到最低。前提是业务允许。7.2 本地代理绕行了跨域但没绕掉线上代理是开发期的好帮手但要知道它做了什么浏览器请求http://localhost:5173/api/xxx同源代理服务器把这个请求转发到http://127.0.0.1:8000/api/xxx。整个过程里浏览器看到的都是同源所以 CORS 从未被触发。这意味着你本地测出来的跨域没问题是个假象真正的跨域校验从来没发生过。我见过有人因此误判本地代理配好后把所有跨域问题都归结为部署环境的问题结果查了半天发现是后端 CORS 从来没配。判断方法是在浏览器 Network 面板里看请求的 URL如果显示的是本地地址而不是后端地址那就是被代理了。7.3 多域名证书和跨域没有关系热搜里出现了多域名 SSL 证书生成这跟 CORS 是两个完全不同的层面。证书解决的是 HTTPS 握手时的身份验证问题跨域解决的是同源策略下的响应读取权限问题。证书配错的表现是浏览器直接报证书错误、连页面都打不开跨域配错的表现是页面正常、接口报错。不过两者有一个交叉点值得提如果同一个后端要在多个域名下提供服务你说的可能是证书需要支持多域名SAN 证书或多张证书也可能说的是 CORS 白名单要支持多域名还可能是反向代理要根据Host分发。搞清楚自己到底在解决哪一层的问题能省掉大量无效搜索。8. 我在多域名跨域上踩出来的几条经验最后分享几条没什么技术含量、但确实能省时间的经验。第一条把Vary: Origin当成和Allow-Origin同等重要的配置项写进代码模板不要靠记忆。我因为这个头排查过两次线上间歇性故障第二次之后我就把它写进了团队的项目脚手架里。第二条白名单不要写在多个地方网关一层就够应用层不要再配一遍两处配置打架比不配更难查。第三条预检请求单独打日志标记出来出问题的时候一眼就能看出是预检没过还是实际请求没过。第四条加域名的流程做成改配置加重启或热加载不要改代码改代码就会有人图省事顺手把匹配逻辑改成includes。还有个小技巧写 CORS 配置的时候用一张便利贴记住三个必须同时满足的条件——Allow-Origin必须是具体域名、Allow-Credentials必须是true、前端必须开 credentials。三个里缺一个带登录态的跨域就一定不通过。这个组合我写在过好几个项目的 README 里新人上手时省了很多来回。
返回列表