中文  |  English

济宁米多信息科技有限公司

两个独立 3D 世界怎么互通?一次跨世界传送的架构复盘

联邦互通 跨世界传送 多世界互联 自部署3D世界 RS256凭证 一次性nonce防重放 反向代理限流取真实IP 分布式3D世界

摘要:如果两个 3D 世界部署在不同人的服务器上、各有各的域名和账号体系,"把人从一个世界送到另一个世界"就不是一个跳转链接能解决的事。本文复盘我们实现联邦传送时依次撞上的四个问题:身份怎么跨域、凭证怎么防重放、账号同名冲突怎么收场、以及一个 200 行的"过设计"怎么把所有玩家都弄挂了。
联邦最反直觉的地方是:技术上最难的部分不是加密和协议,而是"同名的时候该怎么办"这种产品问题。

一、先分清"联邦"和"一个平台开多个房间"

这两件事经常被混为一谈,但架构上完全不同:

一个平台多个房间联邦(多个独立世界)
部署一套服务、一个库各自部署、各自域名、各自库
账号一张用户表各自的账号体系
谁说了算平台每个世界的主人
停服影响全挂只影响自己那个世界
互通难点几乎没有身份、资产、凭证、命名冲突

选后者的理由通常不是技术性的,而是治理性的:没有一个中心方*能把你的世界关掉。代价是上面右边那一列——全都得自己解决。

二、问题一:身份怎么跨域

把一个玩家从世界 A 送到世界 B,最省事的想法是"两个世界共享一张用户表"。这条路我们没走,因为一旦共享,两边就不再是独立的了——A 的管理员能看到 B 的用户,A 库挂了 B 也进不去,联邦的意义直接归零。

我们走的是一次性交接凭证这条路:

  1. 世界 A 在确认玩家身份之后,签一张短时凭证(我们是 RS256 非对称签名,B 侧只需要公钥就能验);
  2. 凭证里带 nonce,且只能消费一次——用完即废,不能拿同一张票进两次;
  3. 凭证有效期是分钟级的(不是天级),过期作废;
  4. 世界 B 验签通过后,在自己的体系里把这个人认出来(或建出来),然后丢弃这张票。

三个要点值得单独说:

用非对称签名而不是共享密钥。B 只需要 A 的公钥,A 的私钥不出自己的服务器。这样"两个世界之间不共享秘密"这条联邦底线才守得住。

nonce 必须真的校验。顺手说一个尴尬的发现:我们人类登录链路上原本生成了 nonce 却从来没校验过——凭证做得挺正式,但防重放那一环是空的。做联邦时把这块补上了。这类"看起来做了、实际没生效"的安全实现非常常见,建议对自己的登录链路也做一次"nonce 到底有没有被校验"的检查

凭证过期时间宁短勿长。跨世界传送是"用户点一下就走"的动作,分钟级足够;把它设成几小时,后果无非是给自己留一个长期有效的伪造入口。

三、问题二:跨过去之后,"我是谁"要不要落地

一个容易被忽略的设计选择:跨世界传送过来的会话,要不要在目标世界的库里建一个用户、建一个角色

我们的选择是不建——用 transient(临时)会话,只存在于这次连接的生命周期里。验收方式是直接对比传送前后的 users / characters 表行数:前后不变

为什么这个选择重要:

  • 不污染目标世界的数据。否则每来一个访客,目标世界里就多一条"影子账号",久了之后用户表里全是没法管理的垃圾;
  • 符合联邦的语义。这个人的身份归属还是原来那个世界,目标世界只是"接待",不是"收编";
  • 隐私面更干净。临时会话不留痕,目标世界的管理员不应该因为你路过一次就拿到你的账号。

代价是:他想在目标世界长期待下去,得明确地在那边建号。这是有意的摩擦。

四、问题三:同名冲突——一个把玩家丢成游客的 bug

这是我们踩得最深的一处,也是"产品问题比技术问题难"的典型。

场景:玩家在世界 A 的昵称是"老王",世界 B 里也有一个"老王"(B 自己的用户,跟 A 这位没关系)。

旧实现的行为链是这样的:

目标世界尝试用同昵称建号
  → 唯一键冲突,服务端返回 500
    → 前端收到 500,弹登录框让玩家重新登录
      → 玩家在陌生世界里没有账号,登录失败
        → 玩家变成了游客

一个命名冲突,最后把玩家降级成了游客。 用户的感受是"传送过去了,但我人没了"。

新实现的链条:

  1. 优先按邮箱复用目标世界已有的账号(同一个人,正常应该复用,而不是新建);
  2. 邮箱也对不上,就在目标世界里按候选昵称逐个探测,找一个可用的;
  3. 建号时如果还是撞上唯一键冲突,捕获冲突并自动改名重试,而不是把错误抛给前端。

验收判据:同名、但邮箱不同的情况 4/4 PASS。这条判据的写法本身也值得记——要专门测"同名"这个边界,因为它不会被正常路径覆盖,只会在真实用户身上爆。

五、问题四:那个 200 行的"过设计",把所有玩家都弄挂了

这一节是整篇最值得看的部分。

现象:联邦传送过去之后,世界里的资产加载不出来。查下来是浏览器的混合内容拦截——页面是 https,而要去加载的世界资产是 http,浏览器直接硬拦,问都不问。

我们第一版怎么修的:写了一个约 200 行的 URL 归一化模块,准备把所有形态的地址统一改写。

结果:它把路径形态改坏了,导致所有玩家的资产加载失败。

踩坑总结:这类"把所有输入都归一化"的模块,危险在于它作用于全部流量——一个边界情况判错,就不是某个功能坏,是全员坏。而我们当时要解决的,其实只是一个很窄的问题。

最终方案:只做协议表头修正——一个纯函数,3 处加载口各加 1 行。就这些。验收:纯函数 15/15 + 浏览器端到端 10/10 + 主世界冒烟 9/9

这件事固化下来两条经验:

  1. 先问"最小的正确改动是什么",再问"怎么优雅地通用化"。 尤其当改动会作用于所有流量时,"通用化"是风险不是加分项。
  2. 已经跑错方向的大改动要敢全撤。 那 200 行我们全删了,没有留一段"以后可能有用"的死代码——留着它,下一个人还会把它接回去。

六、顺带两个不写文档就会踩的坑

① 反向代理之后的限流口径

限流按 IP 做,但如果直接取连接的对端 IP,在反向代理后面全世界会被算成同一个 IP——然后你的限流会把所有用户一起打爆。正确做法是从 X-Real-IPX-Forwarded-For 的最后一段取真实客户端 IP。这种 bug 在压测时不会出现(压测通常直连),只在上了反代的生产环境出现。

② CORS 全开是设计必需,不是漏洞

联邦场景下,各个世界部署在不同域名上,跨域请求是正常业务流。所以 CORS 放开是架构要求,不是开发时图省事。

这件事的教训不是"要不要开",而是:必须把它写进架构文档,并说明原因。否则下一个接手的人(或者下一次安全审查)看到"生产环境 CORS 为 *",第一反应就是"这是个漏洞,修掉"——然后联邦就瘫了。一个没有被文档解释清楚的设计决定,迟早会被当成 bug 修掉。

七、联邦适合谁,不适合谁

场景判断
多个独立组织各自建世界,但希望用户能互相串门适合联邦,这正是它的设计目标
你自己一个人运营、以后也只会有一套先别做联邦。先把单世界的体验做扎实
想用联邦解决"账号统一"问题别用。这是 SSO 的问题,不是联邦的问题
需要跨世界迁移资产(背包、模型)目前传送的是身份与会话,资产迁移是另一件事,别混谈
希望用户在多世界间高频来回谨慎。每次跨世界都是重新建会话,高频切换的体验需要单独设计

八、常见问题

问:联邦传送的凭证怎么防伪造?

答:非对称签名(RS256),验证方只需要公钥;凭证有效期分钟级;凭证里带 nonce 且一次性消费,用后即废。三点缺一不可——尤其 nonce 必须真的校验。

问:传送过去,对方世界会不会拿到我的用户数据?

答:我们的实现是临时会话,不在目标世界的库里建用户和角色(可对比传送前后表行数验证)。身份归属仍在原来的世界。

问:两个世界同名昵称怎么办?

答:优先按邮箱复用已有账号;邮箱对不上则按候选昵称探测可用名;建号撞唯一键则自动改名重试。关键是不要把这个错误抛给用户——否则用户看到的就是"传送失败"或"我变成游客了"。

问:为什么不用共享一张用户表?

答:共享之后两个世界就不再独立了:一方能看到另一方的用户、一方挂了另一方也进不去、一方的管理员能管另一边的人。联邦的价值就在"互不隶属"。

问:跨世界能带东西过去吗?

答:要说清楚——能跨的是身份与会话,不是资产。背包、模型这类资产迁移是完全另一个问题。选型时别把两件事混在一句话里问。

关于创世Genesis

创世Genesis是一套基于Three.js+WebGL构建的自部署3D虚拟世界系统,帮助个人与企业搭建属于自己的3D空间。浏览器直接访问,PC和手机双端兼容,支持多人在线、联邦传送、商铺系统,并支持Agent接入——AI能以具身角色进入你部署的世界。数据运行在你自己的服务器上,不经过第三方平台——让每个世界都真正属于它的主人。

如果你在多世界互通这件事上做选型,上面是我们在做创世Genesis(浏览器端、可部署在自有服务器上的 Three.js 3D 虚拟世界基底)时整理出来的复盘,指标来自仓库里可重跑的验收脚本。

本文为开发过程中的技术复盘,数据来自项目内验收脚本(同名冲突 4/4、协议修正 15/15 + 端到端 10/10 + 冒烟 9/9),可直接复跑核验。

源码与仓库

三个地址内容一致,国内访问用上面两个更快。仓库里有部署说明与演示入口。

← 返回文章列表