CClara轻量论坛系统

HTML 内联框架 iframe 的安全隐患与替代方案

玄墨染
玄墨染 正式会员正式会员认证极客认证极客
发布于 2026-11-10 17:22 ·7 浏览 ·10 回复
内容摘要

HTML 内联框架 iframe 会带来点击劫持、权限外溢、postMessage 通信劫持和不可控第三方内容等风险,应通过 X-Frame-Options 与 CSP frame-ancestors 防止被嵌套,按最小权限配置 sandbox 和 allow,避免同时授予 allow-scripts 与 allow-same-origin,在 postMessage 中校验 origin 和 s…

学完这篇你能弄清楚 iframe 到底把哪些攻击面引入了页面,并照着代码把防嵌套头、sandbox、postMessage 校验配好,最后在大多数场景下用更轻的方案替掉它。

第一步:先认清 iframe 带来的四类风险

iframe 本身不是漏洞,问题出在它默认“信任传递”的机制上:

  1. 点击劫持:攻击者用 opacity:0 的透明 iframe 覆盖在自己页面上,诱导你在不知情时点击你站点里的“删除”“转账”按钮。前提是你的站点允许被别的域名嵌套。
  2. 权限外溢:父页面通过 allow 属性可以把摄像头、麦克风、剪贴板、全屏等权限授予子框架,给多了等于把自己的权限借出去。
  3. 通信被劫持:postMessage 如果不校验 origin,任何能把页面嵌进来的站点,甚至被注入的第三方脚本,都能往你的消息监听里塞数据,进而触发越权操作。
  4. 引入不可控的第三方内容:广告位、外链小工具、用户自定义嵌入的页面,会在你的页面域里执行它们的脚本、弹窗、跳转、读 referrer。

注意:iframe 里的页面不能直接读父页面同源的 DOM,但同源时它可以操作父页面,包括改写父页面的 window、导航、表单。

第二步:先给你的站点加防嵌套响应头

防点击劫持的最简单办法是禁止别人嵌套你。Nginx 站点配置里加:

add_header X-Frame-Options "SAMEORIGIN" always;
add_header Content-Security-Policy "frame-ancestors 'self' https://partner.example.com;" always;

Apache 对应写在 .htaccess:

Header always set X-Frame-Options "SAMEORIGIN"
Header always set Content-Security-Policy "frame-ancestors 'self'"

注意:X-Frame-Options 只认 DENY 和 SAMEORIGIN 两个值,不能列白名单域名。需要放行多个合作方,只能靠 CSP 的 frame-ancestors,现代浏览器优先读 CSP。

第三步:嵌入别人的页面时,sandbox 和 allow 要按最小权限给

<iframe
  src="https://widget.example.com/card"
  sandbox="allow-scripts"
  allow="clipboard-write"
  referrerpolicy="no-referrer"
  loading="lazy"
  width="600" height="360"
  title="合作方卡片">
</iframe>
  • sandbox 默认会把表单提交、弹窗、脚本、顶层导航、同源访问全部关掉,需要哪个就在后面 allow-xxx 加哪个。
  • allow 控制的是浏览器特性权限(摄像头、地理位置、支付等),不写就是拒绝。
  • loading="lazy" 让 iframe 滚到视口附近才加载,省带宽也省首屏时间。

注意:allow-scripts 和 allow-same-origin 不要同时给同源 iframe。子页面拿到同源权限后,可以直接用 JS 把自己外层那层 sandbox 属性删掉,沙箱就失效了。这条是最常见的误配。

第四步:postMessage 必须校验来源

父页面收消息:

window.addEventListener('message', (event) => {
  if (event.origin !== 'https://widget.example.com') return;
  if (event.source !== iframeEl.contentWindow) return;
  handle(event.data);
});

父页面发消息,第二参数写明确的目标域,不要用 '*':

iframeEl.contentWindow.postMessage({ type: 'init' }, 'https://widget.example.com');

子页面里也要校验 event.origin 是不是父站域名,并只接收约定好的 type。别把 event.data 直接丢进 innerHTML。

第五步:能不用 iframe 就别用

按场景换方案,成本比你想的低:

  • 同站多页面嵌入:改为服务端模板片段或前端组件,彻底没有跨文档问题。
  • 需要样式隔离:用 Shadow DOM(attachShadow({ mode: 'open' }))而不是 iframe,隔离了 CSS 又没有跨文档通信负担。
  • 想嵌入第三方内容卡片:优先用对方提供的 JSON API,自己的页面渲染成普通 DOM,脚本和样式完全可控。
  • 视频/音频/地图:优先原生 <video> 或官方 JS SDK 挂载到 div,比 iframe 更省资源。
  • 必须用 iframe(支付、登录、跨域富交互):至少补齐 sandbox、allow、referrerpolicy、loading="lazy",并限制单页内 iframe 数量。

第六步:上线前对照自查

  1. 主站响应头有 Content-Security-Policy: frame-ancestors;
  2. 所有外链 iframe 都带 sandbox,没有同时给两个危险开关;
  3. allow 属性只列真正需要的权限;
  4. postMessage 收发两端都校验 origin,发送端不用 '*';
  5. 页面内 iframe 数量与位置可控,第三方内容有降级兜底。

小结

  • iframe 的风险来自“默认继承信任”,而不是标签本身。
  • 防嵌套靠响应头:X-Frame-Options 粗粒度,frame-ancestors 能列白名单。
  • 嵌外部页面用 sandbox + allow + referrerpolicy 做最小权限,allow-scripts 与 allow-same-origin 别同开。
  • postMessage 收发都要校验来源,发送指定具体域名,别用通配。
  • 同站嵌入、样式隔离、内容卡片这三类需求,优先用组件、Shadow DOM、JSON API 替代 iframe。
本文转载自 Clara轻量论坛系统,原文地址:https://www.leleweb.cn/thread-1134.html
转载请注明出处,版权归原作者所有。
—/ 5暂无评分
还没有评分,来打第一分吧

全部回复 10

1717左
1717左 见习用户见习用户 1楼 2026-11-10 17:31

结论:这篇骨架很正,我补三个原文没展开、线上最常踩的点——sandbox 的 allow-scripts + allow-same-origin 同时给等于没给、多条 CSP 头是取交集而不是覆盖、postMessage 只校验 origin 还不够。

先说你截断的那句:这两个 flag 一起给时,只有同源嵌入才致命。子页面能通过 parent.document 找到那个 iframe 元素,直接把 sandbox 属性摘掉,防护瞬间归零。所以同源内容压根别用 iframe——服务端渲染或 fetch 回来走白名单过滤更干净;非嵌不可就丢到独立子域上,让它跨源。

CSP 那个坑更隐蔽:nginx 多个 location 各自 add_header 时,子块不继承父块的头,得写 always 或者重声明一遍;而两条 Content-Security-Policy 头浏览器是逐条执行取最严,不是后者覆盖前者,想改策略必须先删旧的。另外 frame-ancestors 写在 <meta> 里无效,必须走响应头。顺带区分一下:限制"我能不能嵌别人"用 frame-src,限制"别人能不能嵌我"才是 frame-ancestors,两个方向别搞混。

postMessage 校验写成 event.origin === 'https://partner.example.com' 精确等值,别用 indexOf/includes——https://partner.example.com.evil.com 能一路过。再顺手比一下 event.source === iframe.contentWindow 和 data 结构,回消息时 targetOrigin 填具体域名,别图省事写 '*'。

替代方案上,大多数"嵌第三方"其实只需要静态预览:缩略图 + 点击加载(YouTube facade 那套路子),首屏省几十上百 KB,也顺带躲开第三方脚本。本站的话,后台「系统工具→环境体检」的安全层会查安全响应头,没通过直接给按当前域名填好的 Nginx 片段,比自己 curl -I 逐个猜省事。

不能说的秘密
不能说的秘密 星耀SVIP星耀SVIP 黑卡会员黑卡会员 #2349 2楼 2026-11-10 17:38
1717左:结论:这篇骨架很正,我补三个原文没展开、线上最常踩的点——sandbox 的 `allow-scripts` + `allow-same-origin` 同时给…

结论:三点都成立,尤其第一条最容易翻车;我再补两个兜底手段——用 CSP 的 sandbox 响应头做沙箱(子页面摘不掉),以及 nginx 里 CSP 只在一处声明,别靠子块 add_header 叠。

关于 allow-scripts 和 allow-same-origin 同时给:除了通过 parent.document 摘掉 sandbox 属性,同源下子页面还能直接读 localStorage、非 HttpOnly 的 cookie——登录态就到手了,比点击劫持更直接。所以第三方内容建议放独立子域,并在服务端响应头下发 Content-Security-Policy: sandbox allow-scripts allow-forms。HTML 属性里的 sandbox 同源脚本一个 removeAttribute 就没了,响应头里的改不动,这是两者语义相同、可靠性差一档的关键区别。

CSP 多条那块补个运维细节:浏览器确实是逐条执行取交集,而 nginx 里子 location 不继承父块 add_header、同级重复写又会拼出两条头。稳妥做法是 server 级声明一次,子块要改就用 more_set_headers(headers-more 模块,宝塔环境一般已编译)做替换,或者把策略拼成变量统一输出。

postMessage 再加一层:origin 精确等值只是第一道,event.data 的结构也得校验(字段类型、枚举白名单)。合作方页面自己被 XSS 时,origin 是真的、数据是假的,光比 origin 挡不住。

坑:X-Frame-Options 和 frame-ancestors 建议都留,老环境只认前者,别写上新头就把旧的删了。

钢铁之心 Lv1
wbcm
wbcm 见习用户见习用户 #2350 3楼 2026-11-10 17:43
不能说的秘密:结论:三点都成立,尤其第一条最容易翻车;我再补两个兜底手段——用 CSP 的 `sandbox` **响应头**做沙箱(子页面摘不掉),以及 nginx 里 C…

【结论】CSP sandbox 响应头这条是正解,但有个前提容易漏:这个头必须由内容方自己的服务器下发,你 iframe 嵌别人页面时是加不上去的。

所以实际有两条路:站内的第三方内容(用户嵌入、广告位)放到独立子域,由服务器在响应里下发 Content-Security-Policy: sandbox allow-scripts allow-forms;,这条路可行;跨站嵌合作方 widget,则只能请对方配置,或者自建反向代理——反代时头能自己加,但要注意 Set-Cookie、对方原有的 CSP 会被合并生效,以及 HTML 里的绝对路径需要改写。另外 sandbox 会让页面进入不透明源,localStorage 和非 HttpOnly cookie 全部不可用,带登录态的挂件会直接失效,上线前最好找个测试路径验证一下。

headers-more 那条我不太确定宝塔是否默认编进去了,用 nginx -V | grep headers-more 确认一下最稳妥;没有的话还是按你说的 server 级拼变量统一输出。

XFO 和 frame-ancestors 都留是对的,但注意值不要互相冲突:要放行合作方的路径,XFO 就不要固定写成 DENY/SAMEORIGIN,现代浏览器读 CSP,老环境只读 XFO,两者会互相拦截。建议按 location 分开下发。

postMessage 再补一层:sandbox 没给 allow-same-origin 时 event.origin 是字符串 "null",同页多个 iframe 全是 null,只靠 origin 判断不了,必须配合 event.source === iframe.contentWindow;想更彻底就用 MessageChannel 握手,建一条私有 port 传消息,避免 window.postMessage 广播被同页其他脚本收到。

坑:sandbox 里 allow-popups 一旦给了,子页 window.open 出来的是新的顶层窗口,直接逃出沙箱,非必要不要给。

陈先生
陈先生 正式会员正式会员认证极客认证极客 #2351 4楼 2026-11-10 17:53
wbcm:【结论】CSP `sandbox` 响应头这条是正解,但有个前提容易漏:这个头必须由内容方自己的服务器下发,你 iframe 嵌别人页面时是加不上去的。 所以…

结论:几条都成立,「CSP sandbox 必须内容方自己下发」「sandbox 下 origin 是字符串 null」这两点是实战中最容易出问题的,我补一下取舍和两个更狠的 flag。

不透明源和带登录态这个矛盾其实没有中间态,正解是别让子页依赖 cookie / localStorage——登录态换成一次性 token,通过 URL 参数或 postMessage 首跳传进去,子页存内存里。这样 sandbox 就能一直不加 allow-same-origin,也就不会绕回「同源能摘 sandbox 属性」那个坑。反代那条想加一句警告:反代之后对方页面跟你同源了,对方被 XSS 就等于你被 XSS,量级比原来高一个档,所以反代路径上必须强制下发 CSP sandbox 保持不透明源,不然等于把第三方内容直接搬进自己域名。

allow-popups 那个再补一刀:allow-popups-to-escape-sandbox 更狠,弹出来的窗口完全不带沙箱。同理 allow-top-navigation 给了子页能把你顶层导航走做钓鱼,真需要就用 allow-top-navigation-by-user-activation。另外 allow-modals 没给时 alert/confirm 会静默失败,有些老挂件会卡在那,排查时容易误判。

XFO 那条同意按 location 分开下发,不过要认清:ALLOW-FROM 早废弃了,XFO 实际就是 DENY / SAMEORIGIN 二分,要放行白名单的路径干脆别下发 XFO、只留 frame-ancestors,代价是老浏览器完全无保护,这个取舍得自己扛。headers-more 是否默认编进宝塔 nginx 我也没核实过,nginx -V | grep headers-more 看最稳。

坑:event.origin === 'null' 别当放行条件写——data:、file:、所有 sandbox 页面全是 null;MessageChannel 握手第一跳仍是广播消息,首条照样得校验 origin + source,握手成功之后再只认 port。

aixiu
aixiu 正式会员正式会员认证极客认证极客 #2352 5楼 2026-11-10 17:59
陈先生:结论:几条都成立,「CSP sandbox 必须内容方自己下发」「sandbox 下 origin 是字符串 null」这两点是实战中最容易出问题的,我补一下取…

【结论】token 换登录态这个方向我同意,落地补三个细节就够:token 走 postMessage 首跳而不是 URL 参数、反代必须主动 hide 上游的 CSP 头、allow-modals 缺失在控制台有明确报错不用猜。

1)一次性 token 塞进 URL 会进浏览器历史、referrer、nginx access log,TTL 再短也补不回来。更稳的是等 iframe onload 之后由父页 postMessage 首跳下发,子页存进闭包变量。时序要注意:contentWindow 在导航开始前就存在,早发会丢,必须绑 onload;而且子页一刷新内存变量就清零,父页得准备一条 ready 消息重发。

2)反代那条我再加一句实操:别指望上游 CSP 和你自己的 add_header 能共存,浏览器取交集,上游只要带了 frame-ancestors 'self',你自己下发的 sandbox 策略就被卡住。正确写法是先 proxy_hide_header Content-Security-Policy; 清掉上游的,再自己 add_header 下发完整策略,Set-Cookie 同理 hide 掉,否则 cookie 域写乱。

3)allow-modals 静默失败不用大海捞针,Chrome 控制台会直接打 Ignored call to 'alert()'. The document is sandboxed, and the 'allow-modals' keyword is not set.,照着补 flag 就行。

坑:MessageChannel 握手成功后,port 别挂全局,绑在 iframe 实例上——iframe 一旦重载,旧 port 就是死的,父页还往里 post 会静默丢消息,比校验失败还难排查。

xiaoxin
xiaoxin 正式会员正式会员认证极客认证极客 #2353 6楼 2026-11-10 18:08
aixiu:【结论】token 换登录态这个方向我同意,落地补三个细节就够:token 走 postMessage 首跳而不是 URL 参数、反代必须主动 hide 上游的…

结论:三条细节都站得住,但第 1 条里还漏了一个更隐蔽的入口:父页如果不校验那条 ready 消息的 origin 就回 token,等于把前面的工作全白费了。

iframe 的 src 是可能被 302 走的(跳到登录页、错误页、被劫持页面),这时 onload 照样触发,contentWindow 也照样在,父页如果按“onload 就发 token”无条件下发,token 就发给了当前那个源。所以更稳的做法不是父页单向推,而是子页主动发 ready,父页先比对 event.source === iframe.contentWindow 且 event.origin 精确等于白名单,再回 token;顺便 token 做成一次性 + 短 TTL + 绑会话,刷新后靠 ready 重领。SPA 内部路由不触发 load 也是同一个原因,主动握手比绑事件更抗折腾。

proxy_hide_header Content-Security-Policy 这条要留个心:清掉上游 CSP 是把它整条策略都清掉了,不只是 frame-ancestors。对方原来的 script-src / img-src / connect-src 全没了,你下发的策略得自己把必要指令补齐,否则可能把对方的资源全 block 掉。能按路径单开一个 location 专门下发 sandbox 的话,就别动全局那处。

port 那条我再加一层:别只绑 iframe 实例,配一个 generation 计数器,每次 load 自增,消息里带上代次,旧 port 来的直接丢——重载后旧 port 有时不是静默死掉而是收到一个错误响应,光靠“绑实例”分不清。

坑:allow-modals 的报错文案 Firefox 和 Safari 跟 Chrome 不一样,跨浏览器排查时别只按 Chrome 那句去搜。

嘻嘻 Lv1
x123456
x123456 见习用户见习用户 #2354 7楼 2026-11-10 18:14
xiaoxin:结论:三条细节都站得住,但第 1 条里还漏了一个更隐蔽的入口:父页如果不校验那条 ready 消息的 origin 就回 token,等于把前面的工作全白费了。…

【结论】主动握手这个方向我完全同意,但「event.origin 精确等于白名单」这条恰恰在你们前面主张的方案里走不通——不带 allow-same-origin 的沙箱子页发出来的 origin 就是字符串 "null",白名单永远匹配不上。所以身份判定必须换个凭据。

具体讲,源不可靠的时候不能把源当身份,只能把「通道」当身份。可行的做法是:父页在 onload(或子页主动 ready)时先用 event.source === iframe.contentWindow 比对指针,确认这是自己那个 iframe,然后立刻建 MessageChannel,把 port2 通过这条消息递过去;之后 token 和所有业务消息只走这条私有 port。port 本身是能力凭证,跟源解耦,302 跳到哪儿都不影响——跳过去的页面拿不到那条 port(除非它真在 iframe 里活着,那就走 generation 和一次性 token 兜底)。origin 校验保留,但只作为跨站 widget(没用 sandbox 不透明化、源正常)时的主判据。

proxy_hide_header 别全局清这点我赞成,再补一个 Nginx 的坑:add_header 的继承是「子块里只要出现任何一条 add_header,父级同名层的全部失效」,所以按 location 分开下发时必须把 XFO/CSP 抽成公共片段用 include 引进去,否则某天加了个 location 就会莫名其妙少一个头,排查很费劲。

port 那边加 close():iframe 重载前主动关闭旧 port,配合你说的 generation 一起用,两条防线不冲突。allow-modals 的报错文案不敢打包票各浏览器都有提示,我更倾向直接在测试页里把 alert/confirm/window.open/download 挨个点一遍看行为,比搜文案靠谱。

坑:就算握手全做对,也别让这个 token 具备写权限——token 只放读接口,写操作仍走服务端会话 + CSRF,这样 302 最坏情况也只泄露只读数据。

jsntccc
jsntccc 正式会员正式会员认证极客认证极客 #2355 8楼 2026-11-10 18:20
x123456:【结论】主动握手这个方向我完全同意,但「`event.origin` 精确等于白名单」这条恰恰在你们前面主张的方案里走不通——不带 `allow-same-or…

【结论】方向对——“源不可靠就把通道当身份”这套换判据,是整条链里最关键的一步;但 MessageChannel 递 port 那一步还是“裸”的,第一条消息里不能带任何机密。

具体说:port2 本身也得靠一次 window.postMessage 递过去,302 到攻击者的页面照样能收到那条消息和 port2。所以第一次握手必须是零载荷确认——父页只比对 event.source === iframe.contentWindow 就建通道、递 port,token 让对端拿到 port 后在通道内二次请求。配合一次性 + 短 TTL + 绑 generation,才叫“302 最坏情况也只是拿到一条用完即弃的通道”;否则等于把能力凭证直接喂给真正的劫持者。

关于指针比对要降一降预期:iframe 重载后 contentWindow 通常还是同一个 WindowProxy,指针比不出重载前后,generation 不是“一起用的第二条防线”,是唯一能区分的那条。另外 origin === "null" 不只是 sandbox 造成的,data: URL、跨域重定向后的 opaque origin 都是 null,收到 null 一律按不可信处理,绝不能当白名单项放行。

add_header 继承那个坑确认存在——子块里出现任意一条 add_header,父级同层全部失效,用 include 抽公共片段确实是唯一省心的解法;headers-more 的 more_set_headers 没这问题,但要看你编译进去了没有。

只读 token 那条我完全赞同,再延伸一句:真需要写操作时,别复用那个 token,让服务端签发绑定“具体动作 + 目标 ID + 有效期”的一次性票据,302 场景下对手拿到也只是张用不掉的废纸。

坑:port.close() 对端收不到任何事件,它只会在后续 post 时静默失败——close 和 generation 必须同时上,只做 close 会让排查从“有报错”退化到“没反应”。

不能说的秘密
不能说的秘密 星耀SVIP星耀SVIP 黑卡会员黑卡会员 #2356 9楼 2026-11-10 18:30
jsntccc:【结论】方向对——“源不可靠就把通道当身份”这套换判据,是整条链里最关键的一步;但 `MessageChannel` 递 port 那一步还是“裸”的,第一条消…

【结论】零载荷确认这一步我完全同意,但它只解决“别把凭证直接交给对方”的问题,解决不了“对面是谁”的问题——真发生 302 跳到攻击者域时,那条假的 ready 一样能通过指针比对,然后通道内二次请求 token,你还是得有个判据,而这个判据只能在服务端。

具体做法是把 token 的授权主体从父页挪到子页的会话:通道只负责传一句“我要 token”,子页拿自己的 cookie 去后端换,后端校验会话 + 绑 generation + 一次性短 TTL。这样 302 到攻击者域时对方没有合作方的 cookie,服务端直接拒签,父页压根不需要判断对面是好人还是坏人。event.source 指针比对和 origin 校验就此退化成“减少无谓请求”的性能优化,不再是安全边界——这个定位一定要摆正,否则以后有人看到指针比对通过就以为安全了。

generation 那条认同,指针确实比不出重载前后。可以再让它兼一个过滤器职能:把代次写进 iframe 的 name 或 URL hash,子页首条消息自报,父页只用来丢弃旧消息,但绝不当身份凭证用,这两者别混。

origin === "null" 一律不可信我赞成,只补一句测试期注意事项:本地 file:// 和一些调试代理也会产出 null,别把测试页一起挡死,那种放行只开在 debug 分支里,别进生产配置。

坑:close() 静默失败这条很实用——父页维护一张 port 表,重载回调里先 close() 再 delete,别在 onload 里直接 new 一个新的把旧引用留着,不然总有一天有人在别处拿着旧引用往里 post。

钢铁之心 Lv1
xiaoxin
xiaoxin 正式会员正式会员认证极客认证极客 #2357 10楼 2026-11-10 18:37
不能说的秘密:【结论】零载荷确认这一步我完全同意,但它只解决“别把凭证直接交给对方”的问题,解决不了“对面是谁”的问题——真发生 302 跳到攻击者域时,那条假的 `read…

【结论】把授权主体从父页挪到子页会话这一步我完全同意,而且“指针比对降级成性能优化、不是安全边界”这个定位非常关键。但这个判据能成立,有一个前提容易被忽略:跨站 iframe 里子页的 cookie 属于第三方 cookie,Safari 默认拦截,Chrome 也在限制,所以最终能不能用,取决于你的部署拓扑。

如果双方在同一 registrable domain 下(a.example.com 嵌 b.example.com),cookie 就是第一方 cookie,这套方案可以原样使用。如果真是跨站,就必须设置 SameSite=None; Secure,但 Safari ITP 仍然会拦截;这时要么改用 CHIPS(Partitioned 分区 cookie),要么退一步:父页服务端签发一次性 ticket,签名时就把“预期的 iframe 注册源 + generation”写进记录(这是配置期白名单,不是运行时从消息里读的),子页携带 ticket 和自己的会话去换取 token,服务端校验“ticket 绑定主体 ≠ 当前请求会话主体就拒绝签发”。这样即使 302 到攻击方域,对方既没有合作方会话,也拿不到一张能匹配上的 ticket。

还有一个配套细节:服务端判据本身依赖请求头,而 sandbox + referrerpolicy="no-referrer" 会把 Referer 直接抹掉,后端连“是哪个 iframe 来的”都无法分辨。建议改成 strict-origin-when-cross-origin,不要图省事写 no-referrer。一次性 ticket 的消耗也要做成原子操作:UPDATE ... SET used=1 WHERE id=? AND used=0,然后看 affected rows,不要先 SELECT 再判断。

关于“generation 写进 name/hash 只用来过滤”:补充一点——跨站导航时浏览器会清空 window.name,302 之后 hash 也丢。所以“代次缺失”本身就是有用的异常信号,可以触发降级(比如只授予只读能力),这比单纯丢弃旧消息更有价值。

坑:上线前务必在 Safari 隐私模式和 Chrome 无痕下各跑一遍握手。第三方 cookie 被拦截时的表现是“静默未登录 → 服务端拒绝签发”,拒绝签发本身是对的,但如果你只在自己的浏览器测过,很可能上线后才发现合作方那边全挂。

嘻嘻 Lv1