本页尚未翻译,当前显示英文版本。
Auth & Players
登录是可以覆盖的步骤,而不是黑盒。 提供方、会话、身份关联、封禁。流程中的每个点——登录前后、关联前后、合并前后、状态变更时——都是声明过的扩展点,并声明了类型:拦截型 hook 可以拒绝这一步,观察型 hook 不能。
何时使用
- 玩家(player)需要登录(设备、邮箱、Apple、Google、Steam 或自定义方式),首次登录时创建玩家只是一个标志,不是另一套流程。
- 游客账号(account)之后要升级:
Link添加 Steam 且保留进度,合并(merge)则把两个账号归并为一个玩家。 - 策略必须在无法绕过的位置执行:登录前检查地区,创建玩家的那次登录(sign-in)之后发放新手礼包。
- 管理操作必须立即生效:吊销会话、暂停账号、封禁设备,并发出所有在线系统都能收到的
banned事件。 - 声明的上下文(地区、平台、build)必须送达后续每个 hook,而不必让每个 hook 重新读取玩家。
- 没有更轻量的替代方案。 其他所有模块都通过本模块识别调用方;只要其中任何一个模块还需要玩家主体,就无法关闭
auth:模块配置器会拒绝,并列出依赖它的模块。
分工
| 主体 | 在本页中的职责 |
|---|---|
player | 登录,关联或解除关联身份,刷新凭据,登出 |
moderator | 吊销会话;封禁、暂停或恢复玩家 |
backend-service | 按地区拦截登录;为新玩家写入初始数据行;读取和吊销会话 |
概览
SignIn 调用,首次登录时创建玩家只是一个标志;Link 添加 Steam// client — one call per provider; create-on-first-sign-in is a flag
var session = await PlayServ.Auth.SignIn(Provider.Device, create: true);
await PlayServ.Auth.Link(Provider.Steam); // one player, many identities// client — one call per provider; create-on-first-sign-in is a flag
const session = await PlayServ.auth.signIn(Provider.Device, { create: true });
await PlayServ.auth.link(Provider.Steam); // one player, many identities# client — one call per provider; create-on-first-sign-in is a flag
session = await playserv.auth.sign_in(Provider.DEVICE, create=True)
await playserv.auth.link(Provider.STEAM) # one player, many identitiesGo 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。
// client — one call per provider; create-on-first-sign-in is a flag
Client->Auth->SignInWithProvider(FPSProviderId::Device, Credential,
TPSOnResult<FPSSession>::CreateWeakLambda(this, [this](const TPSResult<FPSSession>& Result)
{
if (!Result.HasValue()) { return; }
// one player, many identities — add Steam to the same account
Client->Auth->Providers->Link(FPSProviderId::Steam, SteamCredential);
}));
// co_await support ships later: a built-in SDK coroutine wrapper that respects Unreal's GC and threading.// client — one call per provider; create-on-first-sign-in is a flag
var session = await PlayServ.Auth.SignIn(Provider.Device, create: true);
await PlayServ.Auth.Link(Provider.Steam); // one player, many identities每个点都在声明它的位置定制;处理器可以采用的各种形式汇总在 Extensibility 中:
[Before(Auth.SignIn)] // a gate: it may refuse, and it is fail-closed
public static Verdict GateRegion(SignInAttempt a) =>
a.Region == "sanctioned"
? Hook.Reject(Problem.Forbidden, "region not served")
: Hook.Continue(a);
[After(Auth.SignIn, created: true)] // an observer: it watches, it cannot refuse
public static async Task GrantStarterPack(Player player)
{
await player.Inventory.Grant("chest.gold", count: 1);
}// a gate: it may refuse, and it is fail-closed
export const gateRegion = before(Auth.signIn, (a: SignInAttempt) =>
a.region === 'sanctioned'
? Hook.reject(Problem.forbidden, 'region not served')
: Hook.continue(a));
// an observer: it watches, it cannot refuse
export const grantStarterPack = after(Auth.signIn, { created: true },
async (player: Player) => {
await player.inventory.grant('chest.gold', { count: 1 });
});@before(auth.sign_in) # a gate: it may refuse, and it is fail-closed
def gate_region(a: SignInAttempt) -> Verdict:
if a.region == "sanctioned":
return Hook.reject(Problem.FORBIDDEN, "region not served")
return Hook.continue_(a)
@after(auth.sign_in, created=True) # an observer: it watches, it cannot refuse
async def grant_starter_pack(player: Player):
await player.inventory.grant("chest.gold", count=1)Go 中基于特性的声明方式尚未确定(O-1),确定写法后会补上示例。
覆盖和 hook 都是云函数(cloud function):它们在平台上执行,而不是在引擎中。用 C#、TypeScript 或 Python 编写,Unreal 订阅由此产生的事件。引擎内编写的函数已在计划中:Blueprint 图表,以及 C++(转译为平台验证的脚本目标)。两者都会像其他声明一样经过分析和推送,执行仍在平台上。
覆盖和 hook 都是云函数:它们在平台上执行,而不是在引擎中。用 C#、TypeScript 或 Python 编写,Unity 订阅由此产生的事件。
管理面板展示的就是声明的形式:每个点列出它的处理器、处理器类型和最终的执行顺序。处理器本身失败时会发生什么,由类型决定:
| 类型 | 处理器本身失败时 | 拒绝时 |
|---|---|---|
| 拦截型 hook(gate) | 拒绝这一步——地区检查无法执行,不等于地区检查已通过 | 返回平台目录中的一个代码,外加一条给人看的原因。调用方根据代码分支;原因文本可以随时修改,也可以翻译 |
| 观察型 hook(observer) | 这一步仍然完成,所以新手礼包没发出去只是少了一个宝箱,登录不受影响 | 不能拒绝 |
hook 从不决定谁登录了
拦截型 hook 只针对平台已经确认的身份回答“是”或“否”。它不指定玩家、不分配身份,也不能代替提供方的确认。
模型
玩家是身份(identity)的持有者,而不是你 schema 中的一行,其标识符稳定且永不复用——合并时也是如此:被合并玩家的 id 仍可解析,不会变成悬空引用。关联(link)是一个三元组:提供方(provider) · 外部主体标识 · 玩家。
| 始终成立 | 含义 |
|---|---|
提供方 + 主体标识 | 唯一;这种唯一性带来的是冲突,而不是禁止:遇到“该账号已被占用”,解决办法是选择合并 |
每个玩家在每个提供方下最多一个关联 | 同一提供方的第二个账号属于冲突 |
外部主体标识 | 永远不作为玩家标识符:它属于提供方,拿来当作我们的标识符会把我们的 id 和对方的绑死 |
身份类型与访问状态 | 是两个不同的维度:anonymous 与 registered 是一个维度;active / suspended / banned 是另一个,所以“已封禁的匿名玩家”和“已暂停的注册玩家”都能表达 |
会话与凭据 | 是两回事:会话(session)是记录,凭据(credential)是你出 示的东西。吊销会话会让它的全部凭据失效,并关闭它打开的订阅(subscription) |
多个同时存在的会话 | 各自独立吊销 |
凭据中的 claim | 是声明的上下文——地区、语言区域——而且仅仅是上下文。claim 从不携带权限 |
设备指纹 | 不是身份:它从来不是放行的依据,只能作为拒绝的依据,并以不可逆的形式存储和比较 |
三个状态机。
| 对象 | 状态 |
|---|---|
| 身份类型 | anonymous → registered,转换是单向的 |
| 访问状态 | active ⇄ suspended;解封时为 active → banned → active |
| 玩家 | alive → merged,其中 merged 是终态:被合并的玩家不会再登录 |
使用方声明的内容。