登录过期不应该变成全系统故障
移动端和跨平台游戏里,平台账号票据过期很常见。玩家从后台回来,渠道 SDK 需要刷新票据;游戏后端 access token 过期,需要换新;资源 CDN 的临时下载 URL 过期,需要重签。正常情况下,这些都应该是可恢复事件。但很多客户端会把它们做成全系统故障:资源下载报 401,房间请求失败,活动配置拉不到,UI 显示“网络错误”,玩家只能重启。
Godot 项目如果把账号、网络请求和资源下载分别封装,却没有统一授权模型,就很容易出现这种问题。下载器只知道 URL 失败,不知道是账号票据过期;账号服务刷新成功后,不知道下载队列还需要重签;UI 层看到失败码,只能弹通用错误。结果是一个可恢复的授权问题,把资源系统、网络系统和玩家体验一起拖死。
这篇文章讨论平台账号票据刷新如何和资源下载授权协作。它不是讲登录流程本身,而是讲登录态变化时,正在进行的资源请求怎么暂停、重签、恢复,如何避免重复下载和错误提示失真。
三种凭证不要混在一起
先把凭证分清。第一种是平台票据,例如 Apple、Google、Steam、渠道 SDK 返回的身份凭证。它用于证明玩家在平台侧是谁。第二种是游戏会话 token,由游戏后端签发,用于调用业务 API。第三种是资源下载授权,可能是 CDN signed URL、短期 token 或 manifest 授权,用于下载某个资源包。
这三种凭证生命周期不同,失效方式不同,刷新入口也不同。平台票据可能需要调用原生 SDK,游戏 token 可能通过 refresh token 刷新,资源 URL 可能需要游戏后端重新签名。把它们都叫 token,会让日志和错误处理非常混乱。
建议在客户端模型里明确字段:platform_ticket_state、game_session_state、download_auth_state。每个状态都有 generation、expires_at、refreshing、last_error。资源下载不直接关心平台票据,但它必须知道游戏会话和下载授权是否可用。
授权状态机
授权刷新要避免并发风暴。多个请求同时收到 401 时,不能每个请求都触发一次刷新。应该有统一的 AuthCoordinator,把刷新合并成一次,并让等待者订阅结果。
sequenceDiagram
participant D as DownloadJob
participant A as AuthCoordinator
participant P as PlatformSDK
participant S as GameServer
participant C as CDN
D->>A: need download auth
A->>A: check session generation
A->>P: refresh platform ticket if needed
P-->>A: platform ticket
A->>S: renew game session
S-->>A: session token
A->>S: sign pack url
S-->>A: signed url
A-->>D: auth ready
D->>C: resume chunk request
这条链路里,DownloadJob 不应该直接调用 PlatformSDK。它只声明自己需要下载某个 pack revision 的授权。AuthCoordinator 判断需要刷新哪一层。这样平台差异被限制在平台层,下载器保持简单。
下载任务如何等待授权
当下载请求收到 401、403 或 signed URL 过期时,任务不应直接失败。它应该进入 waiting_auth 状态,并记录当前已验证分片。等待期间不要清除进度,也不要继续重试同一个 URL。AuthCoordinator 刷新完成后,任务拿到新授权,再从 verified chunk 后继续。
如果授权刷新失败,失败原因要分层。平台票据失败可能需要玩家重新登录或打开平台 SDK;游戏会话失败可能是账号异常、异地登录、服务器维护;下载签名失败可能是资源版本过期或权限不足。这些原因对应完全不同的 UI。统一显示“网络错误”会让玩家无法行动,也让客服无法判断。
任务等待授权时,还要有超时。平台 SDK 有时会卡住,尤其前后台切换或系统弹窗后。超时不代表下载失败,而是授权恢复失败。UI 可以显示“正在恢复登录状态”,超过阈值后提供重试或返回登录页。
generation 防止旧凭证污染
凭证刷新最容易出现旧回调覆盖新状态。假设玩家从后台回来触发刷新,第一次平台票据请求很慢;用户又手动重试,第二次刷新成功;随后第一次请求失败回调晚到,如果直接写状态,就会把成功状态改成失败。
每次刷新都要带 generation。AuthCoordinator 当前只接受最新 generation 的结果。下载任务也要记录自己使用的是哪个 auth_generation。请求返回时,如果 generation 已过期,结果只能用于日志,不能改任务状态。
资源 signed URL 也要绑定 pack revision。manifest 更新后,旧 URL 即使还没过期,也不应该继续用于新资源版本。否则可能下载到旧包,最后校验失败,浪费时间和流量。
UI 文案要区分授权和网络
玩家看到下载暂停时,文案要准确。网络不可用、蜂窝未授权、登录态恢复中、资源授权过期、资源版本变化,这些都可能让下载停住。只有网络真的不可用时才说网络问题。登录态恢复中可以写“正在恢复账号状态,下载稍后继续”。资源版本变化可以写“资源已更新,正在重新确认下载内容”。
如果需要玩家重新登录,不要在下载页面突然弹出复杂登录错误。更好的做法是暂停下载,显示登录恢复入口,保留已下载分片。重新登录成功后,重新签名并继续。只有当账号切换导致资源权限变化时,才需要清理或重新计算任务。
支付和资源授权还要隔离。玩家在商店购买 DLC 后获得资源权限,下载授权可能需要刷新。但购买请求和下载请求不能共用同一个错误提示。购买成功但下载授权暂时失败,应显示“购买成功,资源稍后下载”,而不是让玩家误以为购买失败。
Godot 里的模块边界
可以把系统拆成 PlatformIdentityService、GameSessionService、DownloadAuthService、AuthCoordinator、DownloadQueue。Godot autoload 管理全局状态,具体页面只订阅状态,不直接刷新凭证。
示意代码:
func ensure_download_auth(pack_id: String, revision: String) -> AuthResult:
var session := await game_session.ensure_valid()
if not session.ok:
return AuthResult.failed("session_unavailable")
var auth := await download_auth.sign_pack(pack_id, revision, session.generation)
if not auth.ok:
return AuthResult.failed(auth.reason)
return auth
真实实现里还要处理请求合并。如果多个下载任务同时需要授权,AuthCoordinator 应该复用同一个刷新 promise,而不是重复调用平台 SDK 和后端。重复刷新不仅浪费,还可能触发服务端限流。
和前后台恢复的关系
平台票据刷新通常发生在前后台恢复之后。ResumeGate 探测到后台时间较长、网络 generation 变化或 token 即将过期时,可以要求 AuthCoordinator 先恢复会话,再解冻高风险 UI。下载队列在 Gate Ready 前保持暂停。
如果玩家从后台回来时资源下载还在 waiting_auth,恢复顺序应该是:网络探测、平台票据检查、游戏会话续租、资源 manifest 对齐、下载授权重签、下载队列恢复。任何一步失败,都要保留任务状态,并给出正确 reason。
这条顺序看似保守,但能避免下载器拿旧身份抢跑。移动端切后台期间,系统可能暂停网络,SDK 状态也可能变化。恢复时慢一秒,比进入一串 401 重试要好得多。
安全和隐私
不要把平台票据、access token、signed URL 写入普通日志。日志里只记录凭证类型、generation、过期时间差、刷新结果和错误码。开发包也要避免在 UI 面板里显示完整 token。signed URL 往往包含权限信息,泄露后可能被滥用。
本地持久化也要克制。refresh token 或平台凭证如果必须保存,应使用平台安全存储或后端策略,不要明文写 JSON。下载任务状态可以保存 pack_id、revision、verified chunks,但不要保存长期有效的下载授权。启动后重新签名更安全。
QA 场景
QA 要覆盖这些场景:下载中游戏 token 过期,下载中平台票据刷新失败,下载中 signed URL 过期,后台 30 分钟回来继续下载,购买 DLC 后下载授权延迟生效,账号切换后旧下载任务处理,manifest 更新后旧授权失效。
每个场景检查四点:已下载分片是否保留,刷新是否合并,UI 原因是否准确,旧 generation 回调是否被丢弃。可以用测试服务端强制返回 401、403、URL expired、manifest obsolete。没有这些可控错误,授权系统很难测完整。
开发包调试面板要显示当前平台票据状态、游戏会话状态、下载授权状态和等待任务数。QA 看到下载卡住时,能判断卡在平台、游戏后端还是 CDN,而不是只说“转圈”。
线上指标
建议记录:授权刷新成功率、平均刷新耗时、401 后恢复成功率、waiting_auth 超时率、旧 generation 丢弃次数、因授权导致的重复下载字节、购买后下载授权失败率。尤其重复下载字节很重要,它能量化授权问题对玩家流量的影响。
如果刷新成功率低,要分平台看。不同渠道 SDK 行为差异很大,某个平台在前后台切换后更容易刷新失败,不应该让所有平台共用同一个处理假设。
与资源权限的边界
资源下载授权不只是技术凭证,也可能代表内容权限。免费基础包、已购买 DLC、限时活动资源、地区限定资源、测试服资源,它们的授权规则不同。客户端不能因为曾经拿到过 signed URL,就认为未来一直有权下载。每次 manifest 对齐时,都要确认当前账号和当前环境是否仍有权限。
账号切换尤其危险。玩家 A 下载到一半退出,玩家 B 登录同一设备。下载队列如果只看本地分片,就可能把 A 的资源状态展示给 B。即使资源内容本身不是隐私,也可能造成权限错乱。任务状态需要绑定 account_scope,账号变化时要重新评估。可共享的公共包可以保留,账号绑定包必须暂停或清理。
家庭共享、地区切换和退款也会影响权限。服务器最终说了算,客户端只做缓存和表现。下载授权失败时,UI 要区分“临时无法确认权限”和“当前账号无权限”。前者可以重试,后者要引导购买、切账号或退出入口。
失败恢复策略
授权失败不要只有一种重试按钮。可以按失败类型设计恢复。平台票据刷新失败,先尝试静默刷新;静默失败再打开平台登录或提示重启 SDK。游戏会话过期,先用 refresh token 续租;续租失败进入登录恢复。下载签名失败,先刷新 manifest;manifest 仍有效但签名失败,再提示服务器繁忙或权限异常。
恢复次数也要有限制。连续刷新失败时,客户端要进入 auth_blocked,停止自动重试,避免打爆平台 SDK 或后端。auth_blocked 不等于所有功能不可用,本地已安装资源、离线玩法、设置页面仍可以访问。把整个客户端锁死,往往会让玩家失去处理问题的入口。
下载任务在授权失败后应保持可解释状态。比如 waiting_auth、auth_blocked、permission_denied、manifest_obsolete。这些状态比单个 failed 更有维护价值。客服和 QA 看到状态就能知道下一步该查账号、服务器、资源版本还是网络。
开发期联调清单
平台账号、后端登录和 CDN 签名通常由不同团队维护。联调时要准备一份清单:平台票据过期如何模拟,游戏 token 过期如何模拟,signed URL 过期如何模拟,账号切换时本地下载任务如何处理,购买后权限延迟如何表现,退款或权限撤销如何表现。
每个错误码都要在客户端有映射。没有映射的错误码不能默默当网络失败。开发包可以开启严格模式:遇到未知授权错误时弹出调试提示,并记录完整上下文。正式包则展示保守文案,但仍上传结构化日志。
还要约定时间。客户端本地时间不可信,token 过期判断最好以服务器时间或签发响应中的相对秒数为准。如果设备时间错误,客户端不应该提前大量刷新或误判所有授权过期。移动端玩家设备时间不准并不少见,尤其跨时区旅行或手动改时间时。
小结
平台账号票据刷新不是登录模块自己的事。只要资源下载、房间、聊天和商城都依赖身份,授权状态就必须被统一管理。Godot 客户端要把平台票据、游戏会话和下载授权分层,下载任务遇到授权问题时暂停等待,而不是直接失败或盲目重试。
最小可行版本是 AuthCoordinator 加 DownloadQueue waiting_auth。先让 401 能恢复、进度不丢、文案准确,再逐步补平台差异和安全存储。登录态过期可以是一次短暂停顿,不应该变成全系统故障。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。