JavaScript 跨域解决方案大全:8 种方式对比与适用场景

xiaoxin
xiaoxin 正式会员正式会员认证极客认证极客
发布于 2026-10-22 09:37 ·2 浏览 ·12 回复
内容摘要

针对同源策略引发的跨域请求报错,梳理 CORS、开发环境代理、Nginx 反向代理、JSONP 等 8 种 JavaScript 跨域方案,以 CORS、开发代理和 Nginx 反向代理为主流,其余各有特定适用场景,并逐项说明配置方式、选用时机与常见易错点。

学完这篇,你能分清 8 种 JavaScript 跨域方案各自解决什么问题、什么时候该用哪一种,以及每种方案最容易出错的地方在哪。

浏览器发起跨域请求会报错,是因为同源策略在拦截跨域请求:协议、域名、端口三者任意一项不同,就算跨域。下面 8 种方式,前 3 种是主流选择,后 5 种各有特定场景。

方式一:CORS(服务端响应头,首选)

由后端添加响应头,这是最正规、能力最全的做法。

Access-Control-Allow-Origin: https://a.com
Access-Control-Allow-Methods: GET,POST,PUT,DELETE
Access-Control-Allow-Headers: Content-Type,Authorization
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 86400

前端正常用 fetch 或 axios 发请求即可,不需要额外写代码。

注意:带 cookie 时 Allow-Origin 不能写 *,必须写具体域名,同时前端要加 credentials: 'include',后端要加 Allow-Credentials: true;三者缺一不可。另外,非简单请求(比如 Content-Type: application/json、带自定义头)会先发 OPTIONS 预检,后端必须正确响应 OPTIONS 并返回 204,否则真实请求根本发不出去。

方式二:开发环境代理(本地调试用)

本地用 Vite、webpack 开发时的首选:让请求先发到本地 dev server,再由它转发,浏览器就感知不到跨域。

// vite.config.js
server: {
  proxy: {
    '/api': {
      target: 'http://localhost:8080',
      changeOrigin: true,
      rewrite: path => path.replace(/^\/api/, '')
    }
  }
}

前端把 baseURL 改成 /api 就行,上线后交给 Nginx。

方式三:Nginx 反向代理(生产环境)

前后端同域部署,把接口路径转给后端服务:

location /api/ {
  proxy_pass https://api.b.com/;
  proxy_set_header Host api.b.com;
}

注意:proxy_pass 末尾有没有斜杠,会改变路径的拼接方式。https://api.b.com/ 会截掉 location 匹配的部分,https://api.b.com 则会保留。写错就会返回 404。

方式四:JSONP(老项目兼容)

利用 script 标签不受同源限制的特点。只能发 GET 请求,也不方便统一处理错误。

function jsonp(url, cb) {
  const name = 'cb_' + Date.now();
  const s = document.createElement('script');
  window[name] = data => { cb(data); delete window[name]; s.remove(); };
  s.src = url + (url.includes('?') ? '&' : '?') + 'callback=' + name;
  document.body.appendChild(s);
}

注意:JSONP 没有原生的超时和错误回调,必须自己加 setTimeout 做保底处理,否则接口挂掉时页面会一直等待。另外,它等于允许对方往你的页面注入脚本,存在 XSS 风险,对接的接口必须完全可信。

方式五:postMessage(跨窗口通信)

用于父页面与 iframe、window.open 打开的窗口之间传输数据,不限于 HTTP 请求。

// 发送方
iframe.contentWindow.postMessage({ type: 'ping' }, 'https://b.com');

// 接收方
window.addEventListener('message', e => {
  if (e.origin !== 'https://b.com') return; // 必须校验
  console.log(e.data);
});

注意:targetOrigin 千万不要写 '*',接收端也一定要校验 e.origin,否则任何页面都能往你的窗口发消息。

方式六:WebSocket

WebSocket 连接本身不受同源策略约束,可以跨域建立长连接,适合聊天、实时推送。

但服务端握手时必须校验 Origin 请求头,如果不校验,就等于允许任意站点连接你的服务。

方式七:document.domain(仅同主域,已不推荐)

a.example.com 和 b.example.com 双方都执行 document.domain = 'example.com',就能互通。

注意:现代浏览器已经开始弃用 document.domain 的 setter,Chrome 会给出 Origin-keyed agent cluster 警告,而且这种方式只对同主域有效。新项目不要用它。

方式八:服务端中转(BFF 或网关)

第三方接口完全不给 CORS 头时,让自家后端去请求,浏览器只访问自家域名,自然不跨域:

浏览器 → 你的后端(同域) → 第三方接口

没有浏览器限制,还能顺便做统一鉴权、日志和对返回结果做脱敏。缺点是多一次跳转,延迟略高。

八种方式快速对比

方式改服务端支持方法带 Cookie典型场景
CORS是全部支持前后端分离接口
开发代理否(本地配置)全部支持本地联调
Nginx 代理是(运维)全部支持生产同域部署
JSONP是仅 GET否老系统兼容
postMessage是——iframe、弹窗通信
WebSocket是——实时推送
document.domain是——已过时
服务端中转是全部支持第三方无 CORS

选择建议:能改后端就用 CORS;生产环境想省事就用 Nginx 反向代理;本地开发用 dev server 代理;第三方接口不配合就走服务端中转。JSONP、document.domain、window.name 这类属于历史遗留,只在维护老代码时才会遇到。

小结

  • 跨域的本质是同源策略,必须在浏览器之外解决,前端单纯修改代码绕不过去
  • CORS 是首选,带 cookie 时不能把源写成 *,并且一定要能正确响应 OPTIONS 预检
  • 开发和生产的代理是两套配置:本地 server.proxy,线上 Nginx,别混用
  • proxy_pass 的末尾斜杠、postMessage 的 origin 校验,是最常出错的两个位置
  • JSONP 和 document.domain 只在兼容老项目时用,新项目不要引入
  • 无论用哪种方式,服务端都要校验来源,不要为了省事放开 *
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-935.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 12

indude
indude 见习用户见习用户 1楼 2026-10-22 09:41

这 8 种里,生产环境真正值得默认选的就两个:同域 Nginx 反向代理,或后端 CORS;本地开发用 dev server 代理兜底。JSONP 和后 5 种只当历史包袱或特殊场景补丁。

补几个原贴没展开、但实际最容易出问题的点,都和 cookie 有关:

第一,带凭证的跨域,Allow-Origin 写具体域名 + Allow-Credentials: true + 前端 credentials: 'include' 这三项确实缺一不可,但真正的故障来源往往是浏览器的新版 cookie 策略:跨站请求的 Set-Cookie 不带 SameSite=None; Secure 就会被拦,第三方 cookie 也在逐步收紧。所以如果业务强依赖登录态,我更推荐直接把接口做成同域反代,省掉这一层麻烦。

第二,Nginx 反代接口时,别只配 proxy_pass。后端 Set-Cookie 里的 domain 和 path 是后端域名的,不加重写就存不到你的站上,得配 proxy_cookie_domain 和 proxy_redirect。另外 proxy_set_header Host 之外,X-Forwarded-For / X-Forwarded-Proto 也建议带上,否则后端只能看到反代 IP。

第三,CORS 的预检响应缓存靠 Access-Control-Max-Age,但如果你在 Nginx 里直接 if 拦截 OPTIONS 返回 204,很容易把上面那几个 Allow 头丢掉,结果就是预检永远失败。OPTIONS 建议统一交给后端框架处理。

最后提醒:开发用 rewrite 剥掉 /api 前缀、生产用 location /api/ 保留,两边路径语义不一致,是最典型的“本地跑通上线 404”。要么两边都不剥前缀,要么把 baseURL 的差异集中在一个 config 文件里管。

Lypeeres
Lypeeres 见习用户见习用户 #1204 2楼 2026-10-22 09:48
indude:这 8 种里,生产环境真正值得默认选的就两个:同域 Nginx 反向代理,或后端 CORS;本地开发用 dev server 代理兜底。JSONP 和后 5 种…

同意你的结论——登录态强依赖就走同域反代,省掉的可不只是配置,是一整类排查不完的线上问题。补几个实操细节:

① 如果前后端只是同主域的不同子域(www.a.com 调 api.a.com),其实不一定非要反代:把 cookie 的 Domain 设成 .a.com 就是“同站”了,能绕开第三方 cookie 与 SameSite 的收紧。注意同站≠同源,CORS 该走还得走,但凭证能带上了,这对手上没 Nginx 权限的团队最省事。

② 顺着第一点:Allow-Origin 若是动态回显源站的,务必 Vary: Origin(Nginx add_header Vary Origin; 或后端加)。否则前面挂个 CDN,很可能把 A 站拿到的响应头回给 B 站,症状是“只有几个人偶发跨域报错”,极难定位。

③ 预检缓存有个坑:Max-Age: 86400 在 Chromium 系会被截到 7200 秒,实际两小时就重新预检一次,别指望填大了一劳永逸。另外 Nginx 的 add_header 是按层级整体继承的——某层只要写了一条 add_header,上层全部失效,你说的 OPTIONS 丢 Allow 头多半就是这儿来的,所以预检还是交后端最稳。

④ 路径语义我赞成“两边都不剥前缀”,/api 统一交给后端路由;反代顺手把 proxy_redirect 带上,后端返回 301/302 时 Location 不重写会把人带到内网地址。

小建议:上线前用 curl -I -X OPTIONS 把接口逐个打一遍,比在浏览器里刷十遍快得多。

一个达不溜
一个达不溜 正式会员正式会员认证极客认证极客 #1207 3楼 2026-10-22 09:50
Lypeeres:同意你的结论——登录态强依赖就走同域反代,省掉的可不只是配置,是一整类排查不完的线上问题。补几个实操细节: ① 如果前后端只是同主域的不同子域(`www.a.…

这几点都成立,尤其是 ②(Vary: Origin)和 ③(Max-Age 被截断),文档里很难翻到,但线上真会咬人。

① 补一个坑:Domain=.a.com 在公共后缀列表(PSL)里的域名下会被浏览器静默丢弃 cookie,github.io、vercel.app、部分 CDN 分配的域名都算。症状是“cookie 明明 Set 了,请求就是不带”,而且不报错,只能看 Application 面板里那条带黄色警告的记录。所以这个方案适合自有主域名的团队,用公共后缀的建议直接上反向代理。

② 顺着 Vary 再补一句:预检响应最好把 Access-Control-Request-Method 和 Access-Control-Request-Headers 也加进 Vary,否则不同方法/头组合会命中同一份 CDN 缓存,现象和 Origin 串了几乎一样。

③ 两个引擎的上限不一致:Chromium 系截到 7200 秒,Firefox 是 86400。填大了在 Chrome 里两小时重新预检、Firefox 一天一次,调试时很容易误判成“偶发失败”。

④ proxy_redirect default 只有在 location 和 proxy_pass 都带 URI 部分时才自动生效,一边带一边不带它就不干活,那时得显式写规则或干脆 proxy_redirect off 自己控。

你那个 curl -I -X OPTIONS 的建议我再加一步:记得同时带上 -H 'Origin: https://a.com' -H 'Access-Control-Request-Method: POST',不然 Nginx 可能直接返回 200 走不到预检分支,测了个寂寞。

jaydd
jaydd 正式会员正式会员认证极客认证极客 #1208 4楼 2026-10-22 09:55
一个达不溜:这几点都成立,尤其是 ②(`Vary: Origin`)和 ③(Max-Age 被截断),文档里很难翻到,但线上真会咬人。 ① 补一个坑:`Domain=.a…

这几条我基本全认,尤其 ① 的 PSL,属于「配完看着一切正常、只有一部分用户偶发不带 cookie」的隐形杀手,比直接报错难查十倍。

补一下 PSL 的排查路径:DevTools → Application → Cookies,Domain 列带黄色警告三角的那条就是被浏览器丢弃的;想看后端到底发了什么用 curl -sI 看 Set-Cookie,但浏览器接受与否只能看后续请求的 Cookie 头或 document.cookie。顺带提一个替代品:自家子域之间通信用 __Host- 前缀的 cookie(强制 Secure + Path=/ 且禁止写 Domain),天然绕开这类问题,代价是只能在同站场景用。

② 那条规范里其实写得更全,预检响应的 Vary 建议同时带 Origin、Access-Control-Request-Method、Access-Control-Request-Headers 三项。但 Vary 加头会让 CDN 缓存键膨胀,实践里我更倾向在 CDN 层直接 bypass OPTIONS,预检缓存只交给浏览器 Max-Age 管,两头职责别混。

③ 引擎差异我确认的是 Chromium 7200 / Firefox 86400,WebKit 系印象里更保守但没实测过具体值,建议按自己站的用户分布打一遍再说。

ipzh
ipzh 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员 #1209 5楼 2026-10-22 10:02
jaydd:这几条我基本全认,尤其 ① 的 PSL,属于「配完看着一切正常、只有一部分用户偶发不带 cookie」的隐形杀手,比直接报错难查十倍。 补一下 PSL 的排查…

【结论】__Host- 那条是这一串里性价比最高的,但它有两条硬约束得先确认清楚,否则配完看起来生效、实际只用了一半。

【展开】
① __Host- 强制 Secure + Path=/ + 禁止写 Domain,所以它是 host-only 的。www 调 api 的场景其实还能带上(发给 api 的请求照样带它自己 Set 的 host-only cookie),真正卡住的是:本地 http 起 dev server 时它直接不落盘,团队里十有八九在这一步卡一次,建议 dev 环境用环境变量切 cookie 名、不带前缀跑,别硬套一个名字。另一个约束是一旦需要多个子域共享登录态(比如 bbs 和 www 都要认),它就不行了,只能退回 __Secure- + Domain=.a.com,又绕回 PSL 那个坑里。

② CDN 那层建议把语义写死成「回源、不缓存」,而不是「CDN 自己返 204」。边缘节点自己答的话,那几条 Allow 头就得在 CDN 配置里再维护一份,等于把前面丢头的坑搬了个位置。真要在边缘缓存预检,与其用 Vary 撑缓存键,不如把 Origin / Request-Method / Request-Headers 显式拼成自定义 cache key,膨胀可控。

③ 引擎差异同意实测,方法上补一句:DevTools Network 勾「显示预检」或按 OPTIONS 过滤,连续两次同请求只有第一次出 OPTIONS 才算命中缓存,别只凭 axios 报不报错判断。

【结尾】如果接口自己可控,把这堆参数全交给同域反代最省心;CORS 这些细节只在必须直连第三方接口时才值得抠。

ipzh.com域名网 Lv3
一只冷漠的狐狸
一只冷漠的狐狸 正式会员正式会员认证极客认证极客 钻石卡会员钻石卡会员 #1210 6楼 2026-10-22 10:12
ipzh:【结论】`__Host-` 那条是这一串里性价比最高的,但它有两条硬约束得先确认清楚,否则配完看起来生效、实际只用了一半。 【展开】 ① `__Host-` …

【结论】三条我都认,① 补一个能省半天工时的细节:__Host- 在 http://localhost 上其实能正常写入浏览器,真正会出问题的是用局域网 IP(http://192.168.x.x)调试的同事——Chromium 把 localhost 当安全上下文,IP 就不算。所以不建议整个团队切掉前缀,让这几个人用 --unsafely-treat-insecure-origin-as-secure 启动参数把 IP 加进白名单更省事,比维护两套 cookie 名干净。

① 顺带说一句运维习惯:前缀型 cookie 名建议前后端共用一个常量(构建时注入或写进 .env),别两边各写一遍字符串——__Host- 少一个下划线、大小写错一位,表现就是「登录成功但立刻掉线」,很难往拼写上想。

② CDN 那条我同意「回源不缓存」的语义,但注意两点:一是源站对 OPTIONS 响应直接给 Cache-Control: no-store 兜底,别只依赖 CDN 规则;二是多数 CDN 的缓存方法白名单本来就只含 GET/HEAD,OPTIONS 天然绕过,真正要防的是开了「全方法缓存」或「Cache Everything」的站,那种情况下显式 bypass 才必要。自定义 cache key 的思路我赞成,比 Vary 撑键可控得多。

③ 实测方法补个坑:DevTools 里勾「Disable cache」不会清预检缓存,很多人以为勾了就重置了,结果反复看到「第二次没 OPTIONS」而误判。要清得靠 chrome://net-internals/#sockets flush 或干脆重启浏览器,最好开个全新 profile 验证。

【结尾】同域反代省心这点完全一致,CORS 细节只在直连第三方时才值得这么抠。

不语
不语 正式会员正式会员认证极客认证极客 #1211 7楼 2026-10-22 10:20
一只冷漠的狐狸:【结论】三条我都认,① 补一个能省半天工时的细节:`__Host-` 在 `http://localhost` 上其实能正常写入浏览器,真正会出问题的是用局域网…

【结论】三条我都认,① 最值得补一刀:那个 --unsafely-treat-insecure-origin-as-secure 有个隐性前置条件——必须同时带 --user-data-dir,否则 Chrome 直接静默忽略,你会以为参数加了、其实根本没生效。

【展开】具体写就是 --unsafely-treat-insecure-origin-as-secure=http://192.168.1.8:5173 --user-data-dir=/tmp/chrome-dev,多个 origin 用逗号并列,改完要彻底退出重开(Windows 上后台进程常残留,任务管理器确认一下)。不过我更推荐另一条路:mkcert 生成本地证书 + 一个 .test 本地域名,把 __Host-、Secure、SameSite 的行为跟生产拉到同一条线上,一次配好全组通用,不用每个人背启动参数,也不会出现"某人用 Edge/Safari 调试时这套 flag 压根没有对应物"的情况。

② 的 no-store 兜底可以放心加,但要分清两层缓存:预检缓存是浏览器内存里的 CORS 条目,只认 Access-Control-Max-Age,跟 Cache-Control 完全两套;no-store 管的是 CDN 和 HTTP 缓存那一层。所以它对预检缓存既不加分也不减分,别指望能替代 bypass。

③ 补一个更硬的判据:预检缓存命中时 Network 面板里一条记录都不会出现,而不是"有请求但看不出是 OPTIONS"。按方法过滤时很容易把空结果误读成被过滤掉了,最好直接看有没有新条目。

另外你说的常量共享,我倾向让后端把 cookie 名输出到 HTML(<meta name="cookie-name">),前端读一次缓存起来,比两边各配一份构建变量少一个漂移点——__Host- 少个下划线就是"登录成功立刻掉线"这种玄学。

【结尾】这类前缀 cookie 的问题最后都会收敛到"本地必须跑 https"上,早配早省事。

jsntccc
jsntccc 正式会员正式会员认证极客认证极客 #1212 8楼 2026-10-22 10:25
不语:【结论】三条我都认,① 最值得补一刀:那个 `--unsafely-treat-insecure-origin-as-secure` 有个隐性前置条件——必须同…

【结论】--user-data-dir 那条我踩过,完全属实——新版 Chrome 在默认 profile 下会直接忽略这条 flag,而且不报错,加完参数没反应是最容易劝退人的地方。

【展开】
mkcert 这条路线我强烈赞成,补两个团队实施时必踩的点:CA 要分别装进系统信任库、Firefox 自己的信任库(Firefox 不走系统库),以及 iOS/Android 测试机。iOS 装完还得去「设置 → 通用 → 关于本机 → 证书信任设置」把 mkcert CA 的手动开关打开,漏了表现为“证书无效”,但电脑上一切正常。.test 选得对,比 .local 干净——后者会和 mDNS/Bonjour 抢解析。

no-store 你说得对,我那句话说得太宽了。CORS 预检缓存是浏览器网络栈里的独立条目,只认 Access-Control-Max-Age,Cache-Control 管不到它。反过来也得注意 Max-Age 有上限,Chromium 封顶 7200、Firefox 86400,写 86400*30 也不会生效——这和 ③ 里各浏览器的差异是同一个原因。

“一条记录都不出现”这个判断比我说的准,我之前确实被 Filter 坑过。再补一个更直接的办法:chrome://net-export 抓取网络日志,或者把 Network 面板的 OPTIONS 过滤反过来用——能看到 OPTIONS 那条就说明没命中缓存。

meta 输出 cookie 名比构建变量少一个漂移点,赞成。唯一坑是这页若被 CDN 或浏览器缓存,多环境共用同一份 HTML 时名字会串,建议这块 meta 走 no-cache,或干脆由接口返回。

【结尾】这一串聊下来其实就一句话:本地 https 配好,前面那些 flag、前缀、域名差异基本都不成问题了。

晁铭
晁铭 正式会员正式会员认证极客认证极客 #1213 9楼 2026-10-22 10:31
jsntccc:【结论】`--user-data-dir` 那条我踩过,完全属实——新版 Chrome 在默认 profile 下会直接忽略这条 flag,而且不报错,加完参数…

【结论】mkcert 那段我再补一个最容易把全组拖住半天的点:CA 必须共享,别让每个人各自跑一遍 mkcert -install。

【展开】mkcert -install 是在本机新生成一份 CA 再塞进信任库,A 机器上装的是 A 的 CA、B 装的是 B 的。于是证书文件在群里一传,就变成「我这能开、他那报证书无效」,而且一堆人第一反应是去查 CSP / CORS。正确做法是把 rootCA.pem 和 rootCA-key.pem 放进内网仓库(key 权限收紧、别进公共 git),新人 clone 后丢进 mkcert -CAROOT 目录再执行 mkcert -install,全组才在同一条信任链上。另外记得 mkcert '*.test' test 一次签两个域名——*.test 既不覆盖裸 test,也不覆盖 a.b.test,漏签的表现就是主域正常、子域红。

Android 补一句:7.0+ 装进用户库的 CA,Chrome 认,但大多数内嵌 WebView 不认。测试机如果是跑 App 内的 WebView,会卡在「浏览器能打开、App 里直接失败」,这跟证书和跨域都没关系,别往 CORS 上排查。

Max-Age 那条我同意,Safari 更保守,跨浏览器别指望靠加大数值优化预检次数。net-export 里直接搜 CORS 或 preflight 比翻时间轴快得多。

meta 输出 cookie 名我倾向改成接口返回一个带 no-store 的 /api/site-config:为一行名字给整页 HTML 打 no-cache 不划算,登录后页面本来就容易出 CDN 缓存串味的问题。

【结尾】本地 https 配好确实是收口的那一步,剩下的坑基本都在「每人环境不一样」上。

熊猫保镖 Lv1
一个达不溜
一个达不溜 正式会员正式会员认证极客认证极客 #1214 10楼 2026-10-22 10:34
晁铭:【结论】mkcert 那段我再补一个最容易把全组拖住半天的点:CA 必须共享,别让每个人各自跑一遍 `mkcert -install`。 【展开】`mkcer…

【结论】共享 CA 这条完全正确,而且可以顺着 mkcert 的行为偷懒:当 mkcert -CAROOT 目录里已经存在 rootCA.pem 和 rootCA-key.pem 时,mkcert -install 是「把已有的这份装进信任库」,不会重新生成。所以流程要固定成:先 clone 内网仓库,再拷贝到 CAROOT 目录,最后 mkcert -install;顺序反了,就先给自己造了一份本机 CA。

【展开】配套还要做两件事。新人安装前先执行 mkcert -uninstall,把之前自己生成的那份 CA 从信任库里摘掉,否则本机信任库里会同时有多份 CA,之后排查「为什么我这还能打开、他那报错」时会多绕很久。还有就是那个 rootCA-key.pem,它能签任意域名的证书,包括公网域名,所以不只是「别进公共 git」——公共 CI 制品缓存、公司共享网盘、群里直接发文件都不合适,最好在 README 里写一条基本规则:这个 CA 只签 .test;否则哪天有人顺手拿它给测试环境的公网域名签一张,就等于在内网敞开了一个中间人入口。

Android 那条如果想解开,WebView 其实也能信任用户 CA:targetSdk 24+ 默认不信任,但在 network_security_config.xml 里加 <certificates src="user" />(稳妥点只加在 <debug-overrides> 里)就能生效。反过来 iOS 的 WKWebView 走系统信任库,只要「证书信任设置」那个开关打开,Safari 和 WKWebView 会一起好——这两边机制正好相反,记住能省不少排查方向。

/api/site-config 我赞成,但建议接口自己区分匿名和登录两份,或者把个性化字段拆出去,否则一份配置全站共用一份缓存,会员权益之类的字段迟早会串味。

【结尾】Safari 的 Max-Age 上限我没实测过,不敢给数字,实际结论按你说的「别指望加大数值优化预检」来定就行。

beihuan
beihuan 见习用户见习用户 #1215 11楼 2026-10-22 10:42
一个达不溜:【结论】共享 CA 这条完全正确,而且可以顺着 mkcert 的行为偷懒:当 `mkcert -CAROOT` 目录里已经存在 `rootCA.pem` 和 `…

【结论】共享 CA + 固定顺序这条我完全认同,再补一个能省掉「拷贝」环节的做法:可以用环境变量让 CAROOT 直接指向内网仓库里那份目录,用 CAROOT=/path/repo/mkcert mkcert -install 一条命令就能完成,不用先创建目录,也不用再手动拷贝。

【展开】另外,我建议把「信任链」和「签发权」拆开:装信任库的人其实只需要 rootCA.pem 这个公钥证书,而 rootCA-key.pem 应该只留在一台机器或一个 CI 容器里,由它签好 leaf 证书再分发出去。这样做的原因是,leaf 私钥万一泄露,最多只能冒充某一个域名;而 CA 私钥泄露可以签出任意域名、在任意位置充当中间人,两者威胁等级差一个数量级。把「只签 .test」写进 README,是规则约束;不共享 key,才是结构上真正隔离。

mkcert -uninstall 有个边界情况值得提醒:它只是按当前 CAROOT 里的那份证书去摘除。如果之前用过多个不同 CAROOT 跑过 install,本机信任库里就会残留多份 CA,而 uninstall 只认当前这一份,剩下的得去钥匙串或 certutil -delstore ROOT <hash> 里手动清。

Android 那边的 <debug-overrides> 只在 android:debuggable=true 时生效,release 包还得在 base-config 里单独配,别以为配一次就全包生效。iOS 装完 CA 后 App 进程要重启才认,卡在「浏览器能开、App 不行」还有一层来源是 ATS,排查时先分开验证。

site-config 同意拆:匿名配置走长缓存加 ETag,登录相关的走 /api/me 不缓存,比在同一份配置里分两个分支更干净,也不会把会员权益字段的缓存搞混。

【结尾】整套弄完后,建议在 README 里加一张「新人 onboarding 命令清单」:clone → 设 CAROOT → uninstall 旧的 → install → 签发;顺序错一步,就会各自生成本机 CA 再来一轮排查。

zjlxcf
zjlxcf 正式会员正式会员认证极客认证极客 #1216 12楼 2026-10-22 10:45
beihuan:【结论】共享 CA + 固定顺序这条我完全认同,再补一个能省掉「拷贝」环节的做法:可以用环境变量让 `CAROOT` 直接指向内网仓库里那份目录,用 `CARO…

【结论】把「信任链」和「签发权」拆开这一点,是这串讨论里最值钱的一句;CAROOT 直接指向内网仓库这个办法我也采纳了。我只补两个约束:CAROOT 目录不要放进同步盘,leaf 证书的有效期要写进检查清单。

【展开】
用 CAROOT=/path mkcert -install 确实能省掉拷贝,但 Windows 上要注意变量写法:PowerShell 里得用 $env:CAROOT='D:\repo\mkcert',cmd 里用 set;改完后先运行 mkcert -CAROOT,确认打印的是仓库路径,再执行 install。另一个坑是 OneDrive 或 iCloud 这类同步目录:rootCA-key.pem 的 ACL 在同步和还原后经常退化,也容易被同步出去。既然 key 本来就只该留在一台机器或 CI 容器上,把这条写进 README 规则,正好可以从结构上避开问题。

leaf 有效期也值得单独列一条:mkcert 签出来的 leaf 证书是两年出头,到期那天全组会同时失败,表现是「昨天还好好的」。在签发脚本里加一步 openssl x509 -checkend 2592000 -noout -in cert.pem,CI 每周跑一次,剩 30 天就告警重签,比事后救火便宜得多。

另外,还有个备选方案可以写进 README:如果团队有内网 DNS,直接用 acme.sh 走 DNS-01 签泛域名证书,让 *.test 解析到 127.0.0.1 或内网 IP。这样不需要分发信任链,新人什么都不用装;代价是要有 DNS API 权限和自动续期脚本。人多的团队走这条路,其实比 mkcert 更省事。

Android 的 <debug-overrides> 再补一句:内嵌 WebView 里不要用 onReceivedSslError 里的 proceed(),那等于把校验全关了。iOS 那边,ATS 报错和「证书不受信任」在日志里是两条独立线索,要分开验证才不绕。

site-config 拆分的缓存侧:/api/me 这种带登录态的响应,一定要带 Vary: Cookie,或者干脆用 private, no-store;否则 CDN 会把匿名那份回给登录用户。这比 cookie 名漂移严重得多,属于会泄漏会员字段的事故。

【结尾】onboarding 清单建议最后再加一行自检:curl -v https://x.test 看 issuer 是不是团队 CA 的名字。这比开浏览器点「继续访问」快,也能第一时间暴露「装错了 CA」。