5.总结
5.1 知识点
这个注册登录系列,整体是按「架构演变 → JWT / RSA → 注册流程 → 登录流程」这条线铺开的。
第一篇先讲注册登录系统为什么会一步步演变。
一开始可以是单个中心登录服:客户端把账号密码发过去,服务端查数据库,验证成功后通知游戏服放行。这种方式很适合 Demo 或早期项目,流程短,也好理解。但玩家量上来以后,单点压力、职责混杂、故障影响范围、后续扩容都会变成问题。所以后面才会逐步引入多登录服、共享缓存、Token 化凭证、Gate 本地验签这些设计。
第二篇单独拆 JWT 和 RSA。
JWT 可以理解成一种带格式的登录票据,常见结构是 Header.Payload.Signature。这里最容易误解的是:Header 和 Payload 默认只是 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 最大的误区是把它当成“加密后的用户信息”。
实际上,默认情况下 Header 和 Payload 只是 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,写入
aId、Address、SceneId。 - 返回错误码和 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 侧还要分清 Account 和 GameAccount:
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 与当前鉴权服不一致,请求本来就不该打到这台。
2 和 3 容易混在一起排障。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
Header 和 Payload 默认只是 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