
做 WebGIS 系统集成的朋友应该都遇到过这个需求业务系统里已经有一整套权限体系用户在门户里登录好了结果点进某个“空间分析”菜单跳到了一个需要重新输入账号密码的 SuperMap GPA 页面。用户在门户已经登录过为什么还要再登一次体验瞬间崩塌。所以这个项目的核心就一句话在不跳出去、不重新登录的前提下把 SuperMap GPA 的分析页面嵌进自己的管理系统保持整体操作闭环。用到的关键手段就是 iframe 嵌入 免登录访问的会话打通。这篇文章会把这次集成从选型到落地的完整链路讲清楚。内容包括为什么 iframe 会被浏览器拦、GPA 侧要做什么配置、免登录的三种常见方案怎么选、前端嵌入的完整示例、隐藏滚动条和高度自适应这类体验细节以及我在实际调试中踩过的一堆坑。适合正在做 WebGIS 门户集成、或者打算把第三方业务系统嵌进来的前端和后端开发参考。1. 项目背景与整体思路拆解1.1 需求本质这不是一个“套 iframe”的活总有人把这类需求看成是“写个 iframe 标签就完事”其实真正的难点不在嵌套而在会话。我先还原一下我这次做项目的场景。单位里在用 SuperMap GPA 做数据批处理和空间分析日常运维人员、业务人员都要用。我这边负责的综合业务平台需要把 GPA 里最常用的“分析工作台”页面嵌进来用户从业务平台入口点进去直接看到 GPA 页面并能正常分析中间不能出现登录框。理解这个场景后需求可以拆成三件事页面能被 iframe 嵌入不被浏览器安全策略拦掉用户在业务平台登录后访问 iframe 里的 GPA 时不需要再次输入账号密码页面嵌入后的交互体验要接近原生不能一会儿弹个滚动条、一会儿跳个登录页。拆完之后你会发现第一件事是“能不能嵌”第二件事是“怎么免登”第三件事是“嵌得好不好”。前两个是硬骨头第三个是细节打磨。这篇文章主要讲前两个第三个我会单独给一节经验。1.2 免登录方案对比先把路子理清“免登录”不是一个神秘功能本质上就是让两个独立系统之间建立信任关系。常见的实现路子大概有四种我拿这次的 GPA 场景挨个分析一遍。第一种同域反向代理。在业务平台前面加一层 Nginx把某个路径比如 /gpa/反向代理到 GPA 服务器这样在浏览器里看业务平台和 GPA 页面是“同一个域”。同域下 Cookie 默认会携带只要在这两个系统的用户体系之间把会话对应上免登录就顺理成章。这条路子的优点是稳定、改动小不需要动 GPA 本身的认证代码。缺点是要求你对 Nginx 比较熟而且需要确认两边 Cookie 的 name、path、domain 是否冲突。第二种临时 Token 免登。业务平台后端用服务账号调用 GPA 的认证接口换取一个临时 Token然后带着这个 Token 去跳转或者直接拼到 iframe 地址里。GPA 侧校验 Token 通过后给浏览器种下自己的会话 Cookie然后再跳转到 GPA 主页面。这条路子灵活适合 GPA 提供了标准认证接口的情况也是在不能改 Nginx 的环境下的首选。缺点是 Token 的有效期和安全传输要处理好否则容易留后门。第三种SSO 单点登录。把业务平台和 GPA 都接入同一个统一认证中心用户在任何一端登录另一端通过 CAS/OAuth2 流程自动完成登录。这条路子体验最好但改动通常最大。需要运维配合部署认证中心GPA 侧也要能配置对接往往不是前端一个人能推动的。第四种Cookie 注入或隐藏 iframe 登录。先在一个隐藏 iframe 里用账号密码完成登录然后再切到正式 iframe。这个方法实现简单但安全性和体验都很差Chrome 对第三方 Cookie 的限制越来越严之后基本走不通我不建议在生产环境用。选型建议很明确优先考虑同域反向代理其次是临时 Token 免登SSO 作为中长期优化方向。1.3 我为什么选“网关代理 会话关联”说下我这次的实际选型。项目环境里Nginx 是现成的GPA 部署在内网业务平台是另一个域。我最终选择了“Nginx 同域代理 会话 Cookie 关联”的组合方案。这么选有个很重要的现实原因GPA 是产品化平台不是我们写的代码直接改它的登录逻辑风险太大后面版本一升级可能就冲掉了。在网关层做适配所有改动都落在我们自己的配置和代码里出问题也好回退。另外一个原因是我实测了下把 GPA 页面代理到同域之后iframe 跨域数据读取的问题也就顺带没了后续要做高度自适应、会话跳转都用同一个域里面的手段解决简单很多。2. 核心前置跨域限制与登录态会话机制2.1 iframe 为什么会被浏览器“拦”很多人第一次看到 iframe 嵌第三方页面白屏会很懵。其实绝大多数情况下不是页面坏了而是目标服务器明确告诉浏览器“这个页面不允许被嵌”。这个“告诉”的动作靠的是 HTTP 响应头里的两个关键字段。第一个是 X-Frame-Options。这个响应头有 DENY、SAMEORIGIN 两个常见取值。DENY 表示任何页面都不能把它嵌进 iframeSAMEORIGIN 表示只有同源的页面可以嵌。这就解释了为什么本地随便写个 html 嵌进去往往白屏。第二个是 Content-Security-Policy 里的 frame-ancestors 指令。它比 X-Frame-Options 更灵活可以写 frame-ancestors self http://example.com 来指定允许哪些来源的页面嵌入。如果两个头同时存在浏览器会优先采用更严格的那个。如果要去掉限制需要到 GPA 所跑的 Web 服务比如 Tomcat 或 Nginx里调整响应头。不同版本的 SuperMap 产品默认策略不一样有的版本默认就不拦有的版本默认 SAMEORIGIN。我先用 curl 看响应头再决定要不要改这是最稳的做法。2.2 登录态会话Cookie 的“同与异”免登录的核心是让业务平台的登录态能够被 GPA 识别。这里就会碰到浏览器 Cookie 的域和 SameSite 问题。我先说个生活类比。Cookie 就像访客的临时通行证浏览器每次访问某个域名时会主动把对应这个域名的通行证带上。问题来了业务平台是 a.example.comGPA 是 gpa.example.org两个站点看到的 Cookie 根本不是一个集合业务平台里的“已登录”状态GPA 自然感知不到。解决思路一般有两个方向。第一让浏览器访问 GPA 时自动带上业务平台给的特殊凭证这个凭证在 GPA 侧可以被验证这是 Token 方式的基本逻辑。第二让业务平台和 GPA 在浏览器视角变成同一个域Cookie 自动共享这是同域代理的基本逻辑。还有 SameSite 属性要重点讲一下。Chrome 80 之后如果 Cookie 的 SameSite 没设置默认会被当成 Lax只有顶级导航或者同站请求才携带跨站 iframe 请求是不带 Cookie 的。这就是为什么有些内网系统明明配置好了代理但 iframe 里仍然拉不到登录态。解决办法通常是给 Cookie 设置 SameSiteNone; Secure同时保证页面走 HTTPS。下面这个表格总结一下我在配置时关注的点环节关键配置说明页面可被嵌入X-Frame-Options / CSP允许业务平台域名嵌入Cookie 携带Domain / Path代理通路下把 GPA Cookie 落在业务平台主域跨站 CookieSameSiteNone; Secureiframe 跨站场景下允许发送 Cookie会话关联Session 映射业务平台 token 与 GPA session 对应的关联关系2.3 免登录的本质信任如何转换想通了上面的机制“免登录”的实现思路就不神秘了把业务平台的登录态转换成 GPA 可识别的登录态。这种转换通常有三种形态。第一种是“隐形重定向”用户访问 iframe 里的 GPA 时后端发现没有会话就拿着业务平台的凭据去调 GPA 的认证接口认证通过后获得会话并写 Cookie。第二种是“代理代写”所有来访都打给 NginxNginx 用固定账号去 GPA 侧换取会话再把会话写回浏览器。第三种是“票据传递”业务平台把登录票据通过 URL 或 Header 传过去GPA 接口验票后直接建会话。这三种我在项目里都试过整体上最省心的还是把信任转换逻辑放在网关层因为这样前端 iframe 的代码可以保持很干净不掺太多登录逻辑。3. 免登录访问的三种落地实现3.1 方案一Nginx 同域反向代理推荐先说为什么推荐。把 GPA 代理到同域之后iframe src 可以直接写同域地址避开了跨域 Cookie 和 X-Frame-Options 的大部分问题。我这次用的 Nginx 配置大概是这样的server { listen 443 ssl; server_name portal.example.com; # 业务平台 location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } # GPA 页面代理到同域 location /gpa/ { proxy_pass http://gpa-server:8090/; proxy_set_header Host gpa-server:8090; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # 关键把 GPA 返回的 Cookie 域改写到业务平台主域 proxy_cookie_domain gpa.example.org portal.example.com; proxy_cookie_path / /gpa/; } }这里面的几个点我要强调一下。proxy_pass http://gpa-server:8090/; 最后那个斜杠很关键它表示把 /gpa/ 之后的路径原样拼到 GPA 根路径下。如果没有斜杠路径会被整个带过去容易 404。proxy_cookie_domain 和 proxy_cookie_path 是免登录成功与否的胜负手。GPA 登录后种下的 Cookie 默认域是 gpa.example.org路径是 /浏览器看到这个 Cookie 属于别的域就不会在访问 portal.example.com 时自动带。改写成 portal.example.com 和 /gpa/ 之后Cookie 在业务平台域下生效同域 iframe 请求才会自动携带。改完后测一下登录流程先直接访问业务平台并登录再打开含 /gpa/ 的 iframe 页面。如果 Network 面板里请求 /gpa/ 的请求头里带上了业务平台的会话 Cookie说明代理链路是通的。3.2 方案二临时 Token 免登适合跨域强隔离环境有时网络环境不允许你加代理或者 GPA 部署在另一个安全域这时临时 Token 更合适。流程拆解如下用户已在业务平台登录前端在进入 GPA 页面时调用后端接口/api/gpa-token后端用提前配置好的 GPA 服务账号和密钥调用 GPA 认证接口获取 Token后端把 Token 返回给前端前端在 iframe src 上拼接 Token比如https://gpa-server:8090/auth?tokenxxxredirect%2FworkstationGPA 校验 Token成功后种下自己的会话 Cookie并跳转到工作台页面之后 iframe 里的所有请求都会带着 GPA 自己的会话 Cookie不再需要传 Token。后端代码大概长这样Java Spring Boot 伪代码GetMapping(/api/gpa-token) public MapString, String getGpaToken() { // 1. 调用 GPA 认证接口获取临时 token String gpaToken gpaAuthClient.getToken(service-account, password); // 2. 可追加有效期校验、IP 白名单等控制 return Map.of(token, gpaToken, expiresIn, 600); }前端代码iframe idgpaFrame src frameborder0 width100% height100%/iframe script const frame document.getElementById(gpaFrame); fetch(/api/gpa-token) .then(res res.json()) .then(data { // 拼接 token 到 src注意 redirect 参数要编码 frame.src https://gpa-server:8090/auth?token${data.token}redirect%2Fworkstation; }); /script这里有两个容易踩的坑。第一个是 Token 有效期。GPA 的临时 Token 一般有时效如果用户停留在 iframe 页面超过有效期后续操作可能重新要求登录。我的处理方式是让 iframe 页面在会话快过期时主动提示父页面重新获取 Token 并刷新页面这个可以通过 postMessage 实现下面会讲到。第二个是 Token 泄露风险。Token 拼在 URL 里浏览器历史记录、Nginx access log、运维平台抓包都能看到。所以 Token 有效期一定要短最好还是一次性使用验票之后立刻失效。3.3 方案三SSO 单点登录体验最优但改动大如果你们公司有统一认证平台SSO 是最终极的解法。流程大致是业务平台登录成功后用户在访问 GPA 时会被重定向到认证中心认证中心发现用户已有全局会话就直接发放一个针对 GPA 的票据GPA 拿着票据去认证中心换取用户信息并建立本地会话。整个过程对用户无感但对系统架构有要求。GPA 的 SSO 对接通常要看它支持的协议。SuperMap 相关产品一般对接比较多的是 CAS 或 OAuth2有些版本还能配置自定义认证插件。这块不同版本差异很大我强烈建议先查官方文档里关于认证集成的章节确认支持哪种协议再让运维在认证中心注册一个 GPA 客户端。如果协议支持不到位SSO 强行硬接会非常痛苦我见过有项目纠结了两周最后又退回 Token 方案的情况。所以我的建议是大版本明确支持就上 SSO不确定就先用方案二顶上。3.4 iframe 嵌入与免登录组合后的完整示例把上面的方案串起来我给出一个前端集成页面的完整示例这个模板我在项目里直接用过略做脱敏。!DOCTYPE html html langzh-CN head meta charsetUTF-8 / titleGPA 分析工作台/title style html, body { margin: 0; height: 100%; overflow: hidden; } #gpaFrame { width: 100%; height: 100%; border: 0; } /style /head body iframe idgpaFrame titleGPA 工作台 allowfullscreen loadinglazy src/gpa/workstation /iframe script (function () { const frame document.getElementById(gpaFrame); const targetOrigin window.location.origin; // 监听 GPA 内嵌页面发来的消息 window.addEventListener(message, function (event) { // 安全校验只处理信任来源 if (event.origin ! targetOrigin) return; const data event.data || {}; if (data.type gpa:height) { // 动态调整 iframe 高度 frame.style.height data.value px; } if (data.type gpa:session-timeout) { // 会话过期刷新业务平台登录态或提示用户 alert(GPA 会话已过期请重新登录后再试); } }, false); // 页面加载完成后触发一个自定义事件方便 GPA 侧回传消息 frame.addEventListener(load, function () { frame.contentWindow.postMessage({ type: parent:ready }, targetOrigin); }); })(); /script /body /html这套模板里我把高度自适应、会话过期提示、安全校验都做进去了。iframe 里的 GPA 页面如果配合做了一层薄封装就可以在页面加载完后调用 postMessage 向父页面发消息实现“无缝嵌入”的体验。3.5 不管用哪种方案都要做的安全控制Token 有效期默认不要超过 10 到 30 分钟重要操作可以要求二次验证后端接口要校验调用来源建议加 Referer/Origin 校验防止别人直接调你的免登接口iframe 的 allow 属性按需配置别动不动写 allow*关键操作、数据下载等接口不能完全依赖免登要单独校验业务平台侧权限定期检查后端访问日志看看有没有异常的 Token 换取记录。4. iframe 嵌入后的体验细节高度、滚动条与通信4.1 高度自适应怎么做iframe 高度自适应是嵌入体验里的“老大难”。如果两点都是同域最简单直接读取 iframe contentDocument.body.scrollHeight 设置高度。但大部分场景是跨域跨域下你读不到 iframe 内部 DOM只能靠 postMessage 通信。实现思路是GPA 页面在加载完成、数据变化时把 document.body.scrollHeight 通过 parent.postMessage 发给父页面父页面收到后修改 iframe 的 height 样式。如果 GPA 页面不方便改造加脚本还有一种替代方案让 iframe 高度等于一个很大的固定值然后用 CSS 把外部容器滚动条藏起来变相实现内部滚动。这个方案体验一般适合内嵌页面内容比较少的情况。4.2 隐藏滚动条的正确姿势iframe 的滚动条有三个层级可以处理。第一层是 iframe 标签本身的 scrolling 属性设置 scrollingno 可以禁用滚动条但这个属性在 HTML5 里已经不被推荐实际很多浏览器会忽略。第二层是 iframe 内部的页面样式需要在 GPA 页面里给 html、body 设置 overflow: hidden 或 overflow: auto这个跨域场景下一般改不了。第三层是父页面给 iframe 外层容器设置 CSS把滚动条视觉隐藏但保留滚动功能。如果只是“隐藏滚动条但页面还是要能滚”可以用这段 CSS 样式作用于 iframe 外层容器或业务平台页面::-webkit-scrollbar { width: 0; height: 0; } #gpaFrame { scrollbar-width: none; /* Firefox */ }注意隐藏滚动条不等于禁用滚动不要为了好看让用户没法翻页。尤其是数据表格、长列表页面强行 overflow: hidden 会把内容裁掉一半用户还完全没察觉这是体验事故。4.3 加载状态与骨架屏GPA 页面往往包含地图服务、空间分析组件加载速度比普通页面慢不少。iframe 在加载完成前是空白区域用户看到白屏很容易以为页面坏了。我的习惯是给 iframe 加一个“加载中”的状态等 load 事件触发后再加上一个类别 CSS 类控制遮罩的显示。简单做法是给 iframe 外层套一个相对定位容器里面放一个 loading 层iframe 加载完成后把 loading 层隐藏。如果页面里还有异步请求单靠 load 事件不一定准确可以适当加个延时或者由 GPA 封装页主动发送“内容就绪”消息。4.4 通过 postMessage 实现跨域安全通信这部分我在示例里提过单独再说几个要点。使用 postMessage 的时候一定要校验 event.origin不能把任何来源的消息都当成 GPA 页面发来的。实际开发中常见的错误就是把消息来源校验写错或者干脆不校验这等于把数据接口在外面裸奔。GPA 侧如果没法改代码那父页面和 iframe 之间的双向通信就实现不了只能做单向的监听。遇到这种情况功能上要想清楚没有消息通道会话过期提示、高度自适应这些统统实现不了只能靠父页面轮询自己的会话状态来做兜底。4.5 会话过期后别让用户卡死在 iframe 里这里分享一个真实教训。之前一个同事做嵌入时只处理了登录前的免登没处理后端会话过期的场景。用户第二天打开页面业务平台是登录的但 iframe 里的 GPA 会话已经过期页面一直停留在登录界面用户不知道发生了什么一直点一直没有响应。解决思路是父页面在 iframe 加载后维持一个心跳或定时检查发现业务平台会话正常但 iframe 内请求返回 401 或跳转到了登录地址时就给用户一个重新加载或重新登录的按钮同时调用后端重新发起免登流程。5. 常见问题与排查技巧实录5.1 白屏问题排查白屏是最常见的。我建议按下面的顺序排查直接用浏览器访问 iframe 里的 GPA 地址看页面是否正常打开开发者工具的 Network 面板找到 GPA 页面的请求查看响应头里有没有 X-Frame-Options 或 Content-Security-Policy 的 frame-ancestors 限制如果有到 GPA 的 Web 服务配置中放开限制允许业务平台域名嵌入如果没有再看请求是否被代理确认 Nginx location 是否匹配、proxy_pass 是否转对最后检查 console 的报错信息区分是跨域问题还是资源加载失败。一个我常用的检查命令是用 curl 模拟访问curl -I https://gpa-server:8090/workstation看响应头里有没有 X-Frame-Options有的话再确认是不是 SAMEORIGIN 或 DENY。5.2 免登录逻辑失效 / 登录态丢失明明配置好了 Nginx 代理结果 iframe 里 GPA 页面还是显示未登录。多数情况是两个原因。第一个是 Cookie 没有写进代理后的域。检查 Nginx 的 proxy_cookie_path 和 proxy_cookie_domain 是否配置正确。Cookie 的 Path、Domain 只要有一个不对浏览器就不会在后续请求中携带。第二个是 SameSite 问题。如果两个站点不同域iframe 属于跨站请求Cookie 的 SameSite 属性必须设置为 None 并配合 Secure而且在 HTTPS 环境下才能使用。很多内网系统是 HTTP设置 Secure 后 Cookie 反而不生效需要特别注意。我建议遇到这种问题直接在浏览器 Application 面板里看 Cookie看 key、path、domain、sameSite 四个字段基本一眼就能看出问题。5.3 自动化测试里的二层 iframe 切换做浏览器自动化测试的人经常会遇到定位不到 iframe 内部元素的问题。比如用 Automa 操作一个嵌入了 GPA 的业务平台页面页面结构是“最外层是业务页面 iframe里面还有一层或两层的 GPA 内部 iframe”。Automa 的框架选择逻辑里需要先进入外层 iframe再进入内层 iframe。很多人只切了一次就去找元素结果元素不存在。正确的顺序是逐层 Frame 定位。不同版本的 Automa 在界面入口上略有差别但操作逻辑一致先选外层 Frame再选内层 Frame不能跳层。类似的在用 Playwright 时可以用 frame_locator 链式定位# Playwright 示例先定位外层 iframe再定位内层 iframe inner page.frame_locator(#outer-frame).frame_locator(#inner-frame) element inner.locator(#gpa-toolbar) element.click()关键点不要在顶层 page 上直接找 iframe 内部元素要找对 Frame 的层级。5.4 爬虫 / 动态表单里的动态 iframe 处理还有一个相关场景是爬虫或者数据抓取时遇到动态生成的 iframe。用 Scrapy 配合 Playwright 渲染页面时要注意 iframe 内容不是一开始就存在于 DOM 里的必须等待 iframe 加载完成而且 iframe 的 src 可能是动态变化的。处理思路是先等待 iframe 节点出现再切到 Frame 上下文里等待内容加载完成。下面是一段参考page playwright_page # 等待动态 iframe 出现 page.wait_for_selector(#dynamic-frame) frame page.frame_locator(#dynamic-frame) # 在 iframe 内部等待内容渲染 content frame.locator(body).inner_text(timeout10000)同理这类技巧其实和 iframe 嵌入本身没什么关系但我在做这个项目的自动化验收时用到了一并写出来供参考。5.5 常见问题速查表写一张速查表方便你现场排查。现象可能原因解决办法iframe 白屏X-Frame-Options/CSP 拦截GPA 侧配置允许来源页面能开但未登录Cookie 未代理或 SameSite 限制配置 proxy_cookie_path / SameSiteNoneiframe 高度不对未做自适应postMessage 同步高度或固定高度GPA 会话频繁过期Token 有效期太短延长 Token 有效期或做静默续期页面在 iframe 里变慢大页面重复加载增加缓存、优化代理响应、避免重复创建 iframe父页自动刷新后 iframe 丢失顶部页面被重定向给 iframe 外层加持久化处理利用 sessionStorage 记忆路径5.6 配合后端的一种“静默续期”思路最后补充一个我实际用下来很顺手的技巧。如果 GPA 的 Token 有效期是固定的十几分钟用户停留在页面上的时间可能远超这个值。我的做法是父页面设置定时器在 Token 过期前两分钟调用后端/api/gpa-token重新获取 Token再通过 postMessage 传给 iframe 内的 GPA 封装页。GPA 封装页收到新 Token 后在内部刷新请求。这样用户基本感觉不到会话过期。前提还是那句GPA 页面得有能接收 postMessage 并做处理的封装不然只能退而求其次在过期时间点强制刷新整个 iframe虽然体验差一些但至少不会让用户一直停留在登录页。做这个项目前后花了差不多一周最难的不是技术细节而是“想清楚为什么”。iframe 嵌入看起来简单但免登录、Cookie 策略、安全性这几个问题不提前想明白后面就是反复踩坑。这次做完之后我把这套“代理 Cookie 关联 消息通信”的模板固化了下来后来再遇到类似的第三方系统嵌入基本一天内可以搞定。最后再分享一个小技巧如果项目里经常要做这种嵌入集成建议从第一天开始在网关层把跨域、Cookie、代理相关配置单独写成一套可复用的片段分环境维护好。这样每次对接新的系统只需要调整白名单和路径规则不需要从头踩一遍浏览器安全策略的坑。