两道门:X-Frame-Options 和 SameSite 到底谁管什么
如果你做过跨系统嵌入(比如把 B 系统用 iframe 塞进 A 系统的页面),大概率被这两个东西坑过。很多人分不清它们的区别,遇到问题就一股脑地去调 Cookie,结果发现页面根本没显示出来;或者好不容易把 iframe 显示出来了,又发现里面一直提示"未登录"。
这篇文章把这两个概念彻底拆开讲清楚:它们管的根本不是同一件事。
一句话区分
X-Frame-Options 管的是"这个网页能不能被装进别人的 iframe 里"。
SameSite 管的是"跨站请求时,浏览器要不要自动带上 Cookie"。
一个卡在"显示"这一关,一个卡在"身份识别"这一关。就算你把第一关打通了,第二关照样能把你拦下来------这也是为什么很多人调半天,页面明明显示出来了,却一直是未登录状态。
第一道门:X-Frame-Options ------ 网站能不能被嵌入
这是一个 HTTP 响应头,由被嵌入的那个网站的服务器 返回,告诉浏览器:"我这个页面,允许不允许被放进别人的 <iframe> 里显示"。
它只有三个可选值:
| 值 | 含义 |
|---|---|
DENY |
谁都不能嵌入,自己嵌自己都不行 |
SAMEORIGIN |
只有同源(协议+域名+端口完全一致)的页面能嵌入 |
ALLOW-FROM https://xxx.com |
只允许指定来源嵌入(已废弃,现代浏览器不再支持) |
这里有个很多人会踩的坑:SAMEORIGIN 判断的是严格同源 ,子域名不算同源。app.example.com 想嵌入 portal.example.com,即使看起来是"同一家系统",在浏览器眼里也是两个不同的来源,一样会被拦截。
现在业界更推荐用 CSP(内容安全策略)里的 frame-ancestors 指令来替代它,因为它支持写多个域名,还支持通配符匹配子域名:
Content-Security-Policy: frame-ancestors 'self' https://*.example.com;
如果两者同时出现,浏览器只认 frame-ancestors,X-Frame-Options 会被忽略------这是浏览器规范明确规定的优先级。
一旦这道门被关上,浏览器会直接拒绝渲染整个 iframe,你会在控制台看到类似这样的报错:
Refused to display 'https://xxx.com/' in a frame because it set
'X-Frame-Options' to 'sameorigin'.
这时候 iframe 区域是一片空白,连页面骨架都出不来------这一步失败了,后面讨论 Cookie 的问题根本无从谈起。
第二道门:SameSite ------ Cookie 带不带得上
假设第一道门已经过了,iframe 里的页面成功显示出来了。接下来它要正常工作,通常需要知道"这个用户是谁",也就是要带上登录用的 Cookie 一起发请求。这时候就轮到 SameSite 登场了。
SameSite 是 Cookie 的一个属性,由服务器在 Set-Cookie 时指定,控制的是:当请求是"跨站"发起的时候,浏览器要不要自动把这个 Cookie 带上。
三个可选值:
| 值 | 行为 |
|---|---|
Strict |
只有同站发起的请求才带 Cookie,跨站一律不带 |
Lax(现代浏览器默认值) |
部分跨站场景(比如点链接跳转)会带,但 iframe 内部发起的子请求、跨站 POST 等不带 |
None(必须搭配 Secure) |
允许跨站请求携带 Cookie,包括 iframe 内的请求 |
关键就在这里:iframe 里加载的内容,对浏览器来说就是一次"第三方请求" 。如果这个网站的登录 Cookie 是默认的 Lax(现在几乎所有浏览器都默认这个值),那 iframe 里发起的接口请求统统不会带上 Cookie。表现出来就是:页面框架显示出来了,但一发请求就是未登录、401、跳登录页。
两道门的"站"概念还不一样
这里还有个容易混淆的细节:两者对"同不同源/同不同站"的判定标准并不统一。
| 机制 | 判定粒度 | 举例:a.xxx.com 和 b.xxx.com |
|---|---|---|
| X-Frame-Options: SAMEORIGIN | 严格同源(协议+完整域名+端口) | ❌ 不算同源,会被拦 |
| Cookie 的 SameSite | 注册域相同即可(俗称"同站") | ✅ 算同站,Lax/Strict 都能正常带 Cookie |
也就是说,同样两个子域名之间,Cookie 层面可能完全没问题,但 iframe 显示这一关如果对方设的是严格的 SAMEORIGIN,照样会被拦下来。两道门要分开检查,不能因为一个通了就假设另一个也没事。
排查思路:出问题先看是卡在哪一关
遇到"嵌入失败"或者"嵌入后功能不正常",可以按这个顺序排查:
-
打开控制台,看有没有
Refused to display ... in a frame这类报错。有 → 卡在第一关,去看目标网站的响应头里
X-Frame-Options或Content-Security-Policy: frame-ancestors设的什么值,需要目标网站服务端加白名单放行你的域名。 -
iframe 能正常显示,但页面提示未登录、接口报 401/403。
没有第一关的报错 → 大概率是第二关的问题,去看目标网站登录用的 Cookie 是不是
SameSite=Lax(默认值)或Strict。如果是,需要目标网站把这个 Cookie 改成SameSite=None; Secure(必须全站 HTTPS 才能生效)。 -
两关都没问题,依然显示未登录。
检查一下登录态是不是走的
localStorage/sessionStorage或者前端持有的 Token,而不是 Cookie------这类存储方式和 SameSite 完全无关,是另一套排查逻辑了。
一个常被忽略的细节:重定向响应上的头不算数
还有一种容易让人误判的情况:如果 iframe 请求的地址先经过了一次 302 跳转(比如先到一个鉴权中转页,再跳到真正的内容页),这个中间的跳转响应 即使设了 X-Frame-Options: DENY,也不会生效------因为它的 body 是空的,浏览器根本没有内容可渲染,自然谈不上"渲染并拦截"。真正起作用的,永远是浏览器最终渲染显示的那个响应的头,和请求链路里经过了几次跳转、中间页设了什么头没有关系。
最稳妥的落地方案
如果这是你自己能控制、要长期稳定运行的系统,不建议依赖浏览器的各种"信任豁免"机制(比如用户先手动登录过某个域名后带来的临时 Cookie 豁免),这些机制不受你控制,且不同浏览器实现差异很大,未来也可能随时被收紧。真正稳定的方案是让被嵌入的系统同时改好两处配置:
# 允许被指定域名嵌入
Content-Security-Policy: frame-ancestors 'self' https://你的域名.com;
# 允许跨站携带登录 Cookie(必须是 HTTPS)
Set-Cookie: session=xxx; SameSite=None; Secure
如果条件允许,更彻底的方案是把两个系统统一到同一个注册域名下 (比如 portal.xxx.com 嵌 app.xxx.com),这样 Cookie 层面天然是"同站",不需要改成 None,唯一还要处理的就只剩 frame-ancestors 这一关了。