skynet 提供了一个通用的登陆服务器模版 snax.loginserver 。
架构
先做如下定义:
- 登陆服务器 L 。这即是本篇介绍的 LoginServer
- 登陆点若干 G1, G2, G3 ...
- 认证平台 A
- 用户 C
- C 向 A 发起一次认证请求 (A 通常是第三方认证平台),获得一个 token 。这个 token 里通常包含有用户名称以及用于校验用户合法性的其它信息。
- C 将他希望登陆的登陆点 G1 (或其它登陆点,可由系统设计的负载均衡器来选择)以及 step 1 获得的 token 一起发送给 L 。
- C 和 L 交换后续通讯用的密钥 secret ,并立刻验证。
- L 校验登陆点是否存在,以及 token 的合法性(此处有可能需要 L 和 A 做一次确认)。
- (可选步骤)L 检查 C 是否已经登陆,如果已经登陆,向它所在的登陆点(可以是一个,也可以是多个)发送信号,等待登陆点确认。通常这个步骤可以将已登陆的用户登出。
- L 向 G1 发送用户 C 将登陆的请求,并同时发送 secret 。
- G1 收到 step 6 的请求后,进行 C 登陆的准备工作(通常是加载数据等),记录 secret ,并由 G1 分配一个 subid 返回给 L。通常 subid 对于一个 userid 是唯一不重复的。
- L 将子 id 发送给 C 。子 id 多用于多重登陆(允许同一个账号同时登陆多次),一个 userid 和一个 subid 一起才是一次登陆的 username 。而每个 username 都对应有唯一的 secret 。
- C 得到 L 的确认后,断开和 L 的连接。然后连接 G1 ,并利用 username 和 secret 进行握手。
- 允许同时登陆。由于每次登陆的 subid/登陆点 不同,所以可以区分同一个账号下的不同实体。
- 不允许同时登陆,当新的登陆请求到达并验证后,命令上一次登陆的实体登出。登出完成后,接受新的登陆。
- 如果一个用户在系统中,禁止该用户再次进入。
使用
lualib/snax/loginserver.lua 是一个辅助库,帮助你实现登陆模块。
local login = require "snax.loginserver" local server = { host = "127.0.0.1", port = 8001, multilogin = false, -- disallow multilogin name = "login_master", -- config, etc } login(server)
取到 snax.loginserver 模块后,构造配置表,然后调用它就可以启动一个登陆服务器。
- host 是监听地址,通常是 "0.0.0.0" 。
- port 是监听端口。
- name 是一个内部使用的名字,不要和 skynet 其它服务重名。在上面的例子,登陆服务器会注册为
.login_master
这个名字。 - multilogin 是一个 boolean ,默认是 false 。关闭后,当一个用户正在走登陆流程时,禁止同一用户名进行登陆。如果你希望用户可以同时登陆,可以打开这个开关,但需要自己处理好潜在的并行的状态管理问题。
同时,你还需要注册一系列业务相关的方法。
function server.auth_handler(token)
你需要实现这个方法,对一个客户端发送过来的 token (step 2)做验证。如果验证不能通过,可以通过 error 抛出异常。如果验证通过,需要返回用户希望进入的登陆点(登陆点可以是包含在 token 内由用户自行决定,也可以在这里实现一个负载均衡器来选择);以及用户名。
在这个方法内做远程调用(skynet.call)是安全的。
function server.login_handler(server, uid, secret)你需要实现这个方法,处理当用户已经验证通过后,该如何通知具体的登陆点(server )。框架会交给你用户名(uid)和已经安全交换到的通讯密钥。你需要把它们交给登陆点,并得到确认(等待登陆点准备好后)才可以返回。
如果关闭了 multilogin ,那么对于同一个 uid ,框架不会同时调用多次 login_handler
。在执行这个函数的过程中,如果用户发起了新的请求,他将直接收到拒绝的返回码。
如果打开 multilogin ,那么 login_handler
有可能并行执行。由于这个函数在实现时,通常需要调用 skynet.call 让出控制权。所以请小心维护状态。例如,你希望在这个函数中将上一个实例踢下线。那么你需要在踢人操作后再次确认用户是否真的不在线(很有可能另一个登陆的竞争者恰好在此时又登陆成功了)。
一般你还希望这个登陆服务器可以接受一些 skynet 内部控制指令,比如让登陆点可以通知玩家下线了,动态注册新的登陆点等等操作。所以你可以定义这个函数来接收 skynet 内部传递过来的 lua 协议的消息:
function server.command_handler(command, ...)
command 是第一个参数,通常约定为指令类型。这个函数的返回值会作为回应返回给请求方。
你可以把登陆服务器做为一个单独的 skynet 进程使用,并用 cluster 模块和其它 skynet 进程做集群间通讯;也可以启动在一个 skynet 节点中。在附带的例子 examples/login/logind.lua 中,使用的后一种形式。
你可以参考 examples/login/client.lua 来实现配套的客户端。
wire protocol
登陆服务器和客户端的交互协议基于文本。每个请求和回应包,都以换行符 \n 分割。用户名、服务器名、token 等,为了保证可以正确在文本协议中传输,全部经过了 base64 编码。所以这些业务相关的串可以包含任何字符。
下列通讯流程的协议描述中,S2C 表示这是一个服务器向客户端发送的包;C2S 表示是一个客户端向服务器发送的包。
- S2C : base64(8bytes random challenge) 这是一个 8 字节长的随机串,用于后序的握手验证。
- C2S : base64(8bytes handshake client key) 这是一个 8 字节的由客户端发送过来,用于交换 secret 的 key 。
- Server: Gen a 8bytes handshake server key 生成一个用户交换 secret 的 key 。
- S2C : base64(DH-Exchange(server key)) 利用 DH 密钥交换算法,发送交换过的 server key 。
- Server/Client secret := DH-Secret(client key/server key) 服务器和客户端都可以计算出同一个 8 字节的 secret 。
- C2S : base64(HMAC(challenge, secret)) 回应服务器第一步握手的挑战码,确认握手正常。
- C2S : DES(secret, base64(token)) 使用 DES 算法,以 secret 做 key 加密传输 token 串。
- Server : call auth_handler(token) -> server, uid (A user defined method)
- Server : call login_handler(server, uid, secret) -> subid (A user defined method)
- S2C : 200 base64(subid) 发送确认信息 200 subid ,或发送错误码。
错误码
- 400 Bad Request . 握手失败
- 401 Unauthorized . 自定义的
auth_handler
不认可 token - 403 Forbidden . 自定义的
login_handler
执行失败 - 406 Not Acceptable . 该用户已经在登陆中。(只发生在 multilogin 关闭时)