5.注册登录系列总结

5.总结


5.1 知识点

这个注册登录系列,整体是按「架构演变 → JWT / RSA → 注册流程 → 登录流程」这条线铺开的。

第一篇先讲注册登录系统为什么会一步步演变。
一开始可以是单个中心登录服:客户端把账号密码发过去,服务端查数据库,验证成功后通知游戏服放行。这种方式很适合 Demo 或早期项目,流程短,也好理解。但玩家量上来以后,单点压力、职责混杂、故障影响范围、后续扩容都会变成问题。所以后面才会逐步引入多登录服、共享缓存、Token 化凭证、Gate 本地验签这些设计。

第二篇单独拆 JWT 和 RSA。
JWT 可以理解成一种带格式的登录票据,常见结构是 Header.Payload.Signature。这里最容易误解的是:HeaderPayload 默认只是 Base64Url 编码,不是加密,客户端是可以看到内容的。真正有价值的是 Signature,它用来证明这张票是不是可信服务签发的,以及票面内容有没有被客户端改过。RSA 在这个系列里主要用于非对称签名:鉴权服拿私钥签发 Token,Gate 拿公钥验签。Gate 能验票,但不能造票。

第三篇落到注册流程。
注册不是简单地插一条账号数据,而是一次“开户”。客户端先根据用户名选择鉴权服,鉴权服收到请求后还要重新计算账号归属。只有当前节点确实负责这个用户名,才继续做参数校验、限频、协程锁、缓存检查、数据库检查和账号创建。这里最关键的是:客户端选服只是第一层分流,服务端不能完全信任;同一个用户名的注册逻辑必须串行,否则高并发下可能重复创建账号。

第四篇落到登录流程。
登录比注册多一层 Gate。鉴权服登录成功,只代表账号密码验证通过,并签了一张短期票。客户端拿到 Token 后,解析 Payload 里的 Gate 地址,再连接 Gate。客户端解析 Token 只是为了拿地址,不代表完成安全校验。真正的验签、过期时间、发行者、接收方、SceneId 匹配,都要放在 Gate 上做。Gate 验票通过后,再加载或创建 GameAccount,绑定当前 Session,处理重复登录、顶号、断线重连和延迟下线。

把前四篇串起来看,完整链路可以压成一句话:

注册是在开户,登录是在领票,Gate 是入口验票和接人进场。

这个比喻只是帮助建立印象。真正写代码时,要回到几个工程边界:

  • 客户端可以参与分流,但不能作为信任来源。
  • 鉴权服负责账号校验和 Token 签发。
  • JWT 负责证明票据可信,不负责所有账号状态。
  • Gate 负责验票、绑定连接和管理运行时账号状态。
  • 缓存可以减压,但不能代替数据库和业务状态系统。
  • 顶号、断线重连、延迟下线不是附加功能,而是登录系统必须考虑的运行时问题。

5.2 核心要点速览

注册登录系统演变

注册登录系统从中心化到分布式,再到 Token 化,本质上是在回答一个问题:

后面的服务器凭什么相信这个玩家已经通过登录验证?

早期方案最直接:登录服查库,验证通过后通知游戏服放行。
这个阶段能跑,但登录服承担了太多事情:账号校验、数据库访问、登录状态通知、游戏服准入都堆在一起。玩家少时没问题,玩家一多,登录服就会变成入口瓶颈。

中期方案会拆多台登录服,并引入 Redis 这类共享缓存。
多台登录服一起处理请求,缓存里保存 Token、账号状态、在线状态等数据。这样可以减轻数据库压力,也能让不同节点看到同一份登录状态。但缓存中心本身也会变成关键链路,缓存和数据库一致性、Token 过期、顶号、封号、强制下线都要继续设计。

后期更常见的是 Token 化凭证。
鉴权服验证账号后,签发带签名的 Token。客户端拿 Token 去连 Gate,Gate 本地验签,先判断这张票是否可信。需要提前失效、封号、顶号时,再配合 Redis、账号状态中心、tokenVersion 或黑名单机制处理。

这里要注意:
Token 化不是不要状态,而是把“票据有没有被篡改、是不是过期、是不是发给当前服务”这类高频校验放到 Gate 本地做。

JWT 与 RSA

JWT 常见结构是:

Header.Payload.Signature

Header 描述 Token 类型和签名算法。
Payload 放 Claims,比如账号 ID、区服 ID、过期时间、签发方、接收方等。
Signature 用来验证前两段有没有被改过。

JWT 最大的误区是把它当成“加密后的用户信息”。
实际上,默认情况下 HeaderPayload 只是 Base64Url 编码,客户端可以解码看到。所以 Payload 里不能放密码、手机号、身份证、支付信息、真实姓名这类敏感数据。

JWT 真正解决的是:

  • Token 是不是可信服务签发的。
  • Header 和 Payload 有没有被篡改。
  • Token 是否还在有效期内。
  • Token 是否发给当前服务使用。

但 JWT 不天然解决:

  • 玩家是否刚刚被封号。
  • 玩家是否被其他设备顶号。
  • Token 是否需要提前失效。
  • 玩家当前绑定在哪个 Gate。
  • 账号权限是否刚刚变化。

RSA 在这个系列里主要用于 JWT 的非对称签名。
鉴权服保存私钥,用私钥签发 Token;Gate 保存公钥,用公钥验证 Token。这样 Gate 只具备验票能力,不具备造票能力。

这比 HS256 在多 Gate 场景里更好控风险。
HS256 是共享密钥,签发和验证用同一把密钥。如果很多 Gate 都持有这把密钥,一旦某个 Gate 泄露,理论上就可能伪造 Token。RS256 则可以把私钥只放在鉴权服,Gate 只拿公钥。

在 .NET 里,TokenValidationParameters 里几个字段要特别注意:

  • ValidateIssuer:检查签发方。
  • ValidateAudience:检查接收方。
  • ValidateLifetime:检查过期时间。
  • ValidateIssuerSigningKey:检查签名密钥。
  • ValidIssuer:合法签发方。
  • ValidAudience:合法接收方。
  • IssuerSigningKey:用于验签的密钥或公钥。

学习阶段为了调试可能会临时关掉 ValidateLifetime
但正式流程里不能这么做。否则 Token 里写了 exp,Gate 却不检查过期时间,这个有效期就没有实际意义。

注册流程

注册流程可以理解成“开户”。

它真正要处理的不是“插入账号”这么简单,而是要保证同一个用户名只会被正确的鉴权节点创建一次。

注册大致顺序是:

  • 客户端根据用户名选择鉴权服。
  • 鉴权服检查请求频率和基础参数。
  • 鉴权服重新计算账号归属。
  • 如果当前节点不负责这个用户名,直接拒绝。
  • 对同一个用户名加协程锁。
  • 进锁后先查注册缓存。
  • 缓存未命中再查数据库。
  • 数据库也没有,才创建账号。
  • 创建成功后写数据库、写短期缓存,并返回注册结果。

这里有两个核心点。

客户端选服只是第一层分流。

客户端用用户名哈希选鉴权服,是为了把请求打散到不同节点。
但服务端不能完全信任客户端。客户端配置可能过期,也可能被恶意修改请求目标。所以服务端收到请求后,还要用同一套规则重新计算一次账号归属。

当前示例里类似这样:

MurmurHash3(userName) % AuthenticationCount

如果结果和当前鉴权服的 Position 不一致,就拒绝处理。

协程锁要放在查缓存和查数据库之前。

同一个用户名如果并发注册,两个请求可能都查到缓存没有、数据库没有,然后同时创建账号。
所以同用户名注册必须串行。锁的粒度是用户名,不是整个注册服务。不同用户名仍然可以并发注册。

还有一个正式项目必须补的边界:

密码不能明文保存,也不能直接拼进缓存 Key。

示例代码主要是为了看懂流程。上线项目里,密码应该使用带盐、可调成本的密码哈希方案,比如 Argon2id、BCrypt、PBKDF2 等,而不是明文密码或普通快速哈希。

登录流程

登录流程可以理解成“领票 + 验票 + 入场”。

鉴权服阶段主要做这些事:

  • 检查用户名和密码是否为空。
  • 重新计算账号归属。
  • 对同一个用户名加登录锁。
  • 查短期登录缓存。
  • 缓存未命中时查数据库。
  • 验证账号和密码。
  • 更新 LoginTime
  • 根据账号 ID 分配 Gate。
  • 签发 JWT,写入 aIdAddressSceneId
  • 返回错误码和 Token。

这里要分清楚:

鉴权服登录成功,不代表客户端已经进入游戏。

它只是签了一张短期票。
客户端后面还要拿这张票去连接 Gate。

客户端拿到 Token 后,会解析 Payload 里的 Address
这一步只是为了拿 Gate 地址,不是安全校验。因为客户端只是把 Payload 解码出来,它没有验证签名,也没有验证过期时间。

真正验签在 Gate。Gate 收到 Token 后,至少要检查:

  • Token 是否为空。
  • JWT 签名是否正确。
  • issuer 是否可信。
  • audience 是否匹配。
  • Token 是否过期。
  • SceneId 是否等于当前 Gate 的 SceneConfigId
  • 账号 ID 是否能加载到对应 GameAccount

SceneId 校验很关键。
Token 可能是真的,也没过期,但它是发给 Gate A 的。如果客户端拿去连 Gate B,Gate B 也应该拒绝。

Gate 侧还要分清 AccountGameAccount

  • Account 是鉴权账号,偏账号系统。
  • GameAccount 是 Gate 侧游戏入口账号,偏运行时状态。

顶号和断线重连主要靠 SessionRunTimeId 判断。
如果当前 Session 的 RunTimeId 和账号上记录的一样,说明是重复请求。
如果不一样,说明新连接进来了,可能是断线重连,也可能是另一个设备顶号。

顶号时最容易踩的坑是旧 Session 的销毁逻辑。
如果直接断旧 Session,而不先清空旧 Session 上的 GameAccountFlagComponent,旧 Session 销毁时可能触发账号下线逻辑,最后误伤刚登录成功的新 Session。

所以顺序应该是:

  • 找到旧 Session。
  • 清空旧 Session 的账号标识。
  • 给旧客户端发重复登录消息。
  • 延迟断开旧 Session。
  • 当前 Session 绑定账号标识。
  • 更新 SessionRunTimeId
  • 返回 Gate 登录成功。

5.3 面试题精选

基础题:登录失败的「认证问题」和「路由问题」怎么分?为什么要合并「账号不存在」和「密码错误」?

题目

本系列鉴权服登录里,失败大致有两类:一类是账号密码校验不过关,一类是当前节点根本不该处理这个用户名。
这两类在工程上分别该怎么理解?为什么示例里经常把「账号不存在」和「密码错误」合并成同一个返回?

深入解析

这题不想考「背四个数字」,而是考能不能把失败原因分桶:输入不合法、认证失败、路由 / 归属不对。

本系列示例里鉴权服返回习惯写成:

  • 0:成功。
  • 1:参数不合法,例如用户名为空。
  • 2:认证失败侧:查库后账号不存在,或密码不匹配;对外不区分细项。
  • 3:路由侧:按用户名算出来的 position 与当前鉴权服不一致,请求本来就不该打到这台。

23 容易混在一起排障。2 多半是「这对账号口令在当前库里不过关」;3 更像是「客户端选服或服务端列表和网关版本不一致,流量打错机」。线上若短时间某节点 3 飙高,优先核对 AuthenticationCount、节点顺序、配置版本是否对齐,而不是当成集体输错密码。

把「不存在」和「密码错」合并成同一个 2,是为了降低撞库、枚举账号是否存在的风险;客户端提示可以保持笼统(如「用户名或密码错误」)。代价是运营侧如果要区分原因,要走内部日志或风控渠道,而不是把差异暴露给所有客户端。

数字错误码本身再常见,也要在工程里收口:单独枚举、协议文档、日志关键字和客户端文案同源,避免注册一套码、登录另一套码、日志又写字符串,排障时对不上号。

答题示例

我先把失败分两类想:一类是请求格式或参数有问题,一类是业务校验不过。
认证失败那条线里,账号不存在和密码不对在本系列里合成一个码,主要是防别人用登录接口试探某个用户名有没有注册。
另一类是路由不对,就是算的 position 和当前鉴权服对不上,这一般是列表不一致或配置没一起发版,跟密码没关系。
错误码别在各处手写魔法数字,统一枚举和文档,日志和客户端提示才好对齐。

参考文章

  • 3.注册流程
  • 4.登录流程

进阶题:客户端已经按用户名选择了鉴权服,服务端为什么还要重新计算账号归属?

题目

客户端用 MurmurHash3(userName) % AuthenticationList.Count 选择鉴权服后,服务端为什么还要用同一套规则重新计算 position?

深入解析

这题问的是“客户端分流”和“服务端信任边界”。

客户端哈希选服的作用是分流。
不同用户名落到不同鉴权服,避免所有注册和登录请求都打到一个节点。

但客户端不能作为信任来源。
客户端可能拿到旧配置,也可能被恶意修改目标地址。服务端收到请求后,必须重新计算账号归属,确认当前节点确实应该处理这个用户名。

所以这两层不是重复,而是职责不同:

  • 客户端选服:解决入口流量分布。
  • 服务端 position 校验:解决服务端准入和数据归属。

这里还要注意,客户端和服务端必须使用同一套哈希规则、同一份有序节点列表。
如果节点数量或顺序不一致,就会出现客户端认为选对了,但服务端一直返回错误码 3

另外,当前 MurmurHash3 + % Count 是哈希取模分流,不是严格意义上的一致性哈希。
如果鉴权服数量变化,大量用户名会重新映射。后面如果要动态扩缩容,要考虑一致性哈希、服务发现或中心路由。

答题示例

客户端按用户名哈希选鉴权服,主要是为了分流,不是为了让服务端信任它。
服务端收到请求后还要再算一次 position,确认这个用户名确实归当前节点处理。否则客户端配置过期、节点列表不一致,或者请求被篡改时,就可能把账号数据写到错误节点。
所以客户端选服解决的是流量打散,服务端校验解决的是准入和数据归属。

参考文章

  • 1.注册登录系统演变
  • 3.注册流程
  • 4.登录流程

进阶题:注册时为什么协程锁要放在查缓存和查数据库之前?

题目

注册流程里,同用户名协程锁为什么要放在查缓存、查数据库之前?
只靠数据库唯一索引行不行?

深入解析

注册最怕的是同一个用户名被并发创建。

如果锁放得太晚,可能出现这种情况:

  • 请求 A 查缓存:没有。
  • 请求 B 查缓存:没有。
  • 请求 A 查数据库:没有。
  • 请求 B 查数据库:没有。
  • 两个请求都进入创建账号流程。

所以同用户名注册必须串行,而且要在查缓存和查数据库之前就进入锁。

这把锁的粒度不能太粗。
如果所有注册请求共用一把锁,那整个注册入口就被串行化了。正确做法是按用户名锁,同一个用户名串行,不同用户名仍然并行。

数据库唯一索引仍然需要。
服务层锁是减少并发穿透和业务竞态,数据库唯一索引是最后一道兜底。两者不是互相替代关系。

还有一个细节是锁 Key。
如果只在当前进程内加协程锁,用 userName.GetHashCode() 勉强能看流程。
如果是跨进程、跨机器的分布式锁,就不能依赖运行时 GetHashCode() 的稳定性,应该使用规范化后的用户名或稳定哈希。

答题示例

注册锁要放在查缓存和查库之前,因为两个同用户名请求如果同时查到“不存在”,后面就可能一起创建账号。
这里不是把所有注册都锁住,而是只锁同一个用户名,不同账号仍然可以并发。
数据库唯一索引我觉得还是要有,它是最后兜底;服务层锁主要是减少业务层并发穿透和重复创建流程。

参考文章

  • 3.注册流程

深度题:JWT 的 Payload 能被客户端解析,为什么还安全?

题目

JWT 的 Payload 默认可以被客户端解码看到。
那为什么 JWT 还能用于登录凭证?它到底安全在哪里?

深入解析

JWT 的安全点不在于 Payload 看不见,而在于 Payload 被改了以后签名对不上。

JWT 常见结构是:

Header.Payload.Signature

HeaderPayload 默认只是 Base64Url 编码。
客户端能解码 Payload 很正常,所以不能把密码、手机号、身份证、支付信息这类敏感信息放进去。

真正起作用的是 Signature
服务端签发 Token 时,会对 Header.Payload 计算签名。Gate 收到 Token 后,用密钥或公钥验证签名。只要客户端改了 Payload 里的账号 ID、权限、区服、过期时间等字段,签名就会验证失败。

但也要注意:JWT 只能证明票据本身可信,不代表账号当前状态一定有效。
玩家是否被封号、是否被顶号、Token 是否要提前失效,这些还要靠业务状态系统处理,比如 tokenVersion、黑名单、在线状态中心等。

答题示例

JWT 的 Payload 默认不是加密的,客户端能解出来不代表不安全。它的安全点在签名。
Gate 验签时会判断 Header 和 Payload 有没有被改过,只要客户端改了账号 ID、权限、区服这些字段,签名就对不上。
但 Payload 可见,所以不能放敏感数据。而且 JWT 只证明这张票可信,不解决封号、顶号、强制下线这类业务状态。

参考文章

  • 1.注册登录系统演变
  • 2.JWT
  • 4.登录流程

深度题:客户端解析 Token 拿 Gate 地址,为什么不能算安全校验?

题目

客户端拿到 JWT 后,解析 Payload 里的 Address 去连接 Gate。
为什么这个解析过程不能算安全校验?真正的校验应该放在哪里?

深入解析

客户端解析 Token,只是为了拿下一跳地址。
它做的是分段、Base64Url 还原、JSON 反序列化,并没有验证签名,也没有验证过期时间。

所以客户端解析成功,只能说明这段字符串格式上像一个 JWT。
它不能证明:

  • Token 是鉴权服签的。
  • Token 没有被改过。
  • Token 没有过期。
  • Token 是发给当前 Gate 的。

真正的校验必须放在 Gate。

Gate 收到 Token 后,至少要做:

  • 验证签名。
  • 验证 issuer
  • 验证 audience
  • 验证过期时间。
  • 检查 SceneId 是否等于当前 scene.SceneConfigId
  • 根据账号 ID 加载或检查 GameAccount

SceneId 校验解决的是“错接入口”的问题。
Token 可能是真的,但它是发给 Gate A 的。如果客户端拿去连 Gate B,Gate B 也应该拒绝。

答题示例

客户端解析 JWT 只是为了拿 Address 去连 Gate,这一步不能当安全校验。
因为它只是把 Payload 解码出来,没有验签,也没有验过期时间。
真正安全判断要在 Gate 做:验签、验 issuer、audience、exp,还要检查 SceneId 是否和当前 Gate 一致。SceneId 的作用就是防止拿着给 A Gate 签的票去连 B Gate。

参考文章

  • 2.JWT
  • 4.登录流程

深度题:RSA 在 JWT 里主要解决什么问题?为什么多 Gate 更适合 RS256?

题目

JWT 可以用 HS256,也可以用 RS256。
为什么在多 Gate 场景里,RS256 更适合?RSA 在这里是加密 Payload 吗?

深入解析

RSA 在这个登录流程里主要用于签名和验签,不是用来加密 Payload。

RS256 的典型分工是:

  • 鉴权服持有私钥。
  • 鉴权服用私钥签发 Token。
  • Gate 持有公钥。
  • Gate 用公钥验证 Token。

这就像售票处拿真印章盖票,入口保安拿验章模板检查。
保安能验票,但不能自己盖出真票。

HS256 是对称签名,签发和验证用同一把 secret。
如果有很多 Gate 都要验签,那这些 Gate 都要拿到同一把 secret。一旦某个 Gate 泄露,理论上它就具备伪造 Token 的能力。

RS256 的好处是私钥只需要放在签发服务里。Gate 只拿公钥,泄露后的风险更可控。
当然,公钥也不是随便乱发,后续还要考虑公钥版本、密钥轮换、kid、新旧 Token 共存等问题。

答题示例

RSA 在 JWT 里主要不是为了加密 Payload,而是做非对称签名。
鉴权服用私钥签发 Token,Gate 用公钥验签。这样 Gate 只具备验证能力,不具备签发能力。
多 Gate 场景下,RS256 比 HS256 更容易控制密钥风险,因为 HS256 的 secret 一旦发给很多 Gate,任何一个泄露都有伪造 Token 的风险。

参考文章

  • 2.JWT
  • 4.登录流程


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 785293209@qq.com

×

喜欢就点赞,疼爱就打赏