« 网络游戏中商人系统的一点想法 | 返回首页 | 一个 Lua 内存泄露检查工具 »

登陆认证系统

最近在忙代理项目 狂刃 的事情。

因为这个项目,我们提前建立了平台开发团队。但许多东西开始的都很仓促,比如需要对接用户登陆认证系统。

虽然已经有很多成熟的认证协议,比如最有名的 kerberos 。但这次时间紧迫,我就临时设计了一个简单协议。

因为不是 web 应用接入,所以我不想直接使用 https 来提交用户名和密码,而基于 http 协议,在不安全信道上建立了一个自定义协议来应付一下。这种临时设计的协议当然不会很缜密,但也基本够用。

这样,合作方的客户端可以较容易实现相应模块。

在整个业务中,有三个实体:

C 玩家客户端 G 游戏服务器 E 我们的认证平台

登陆流程是这样的:

  1. G 发现有 C 企图登陆时,生成一个一次性的 salt (如果直接用时间的话,可选对称加密),然后发送给 C

2, C 收到 salt ,利用自己的用户密码,对 salt 加密并签名。加密可以使用标准的 DES 算法,签名可以用标准的 MD5 算法。具体方法是:讲用户密码先 md5 hash 一次,得到一个串。取串的前一半做 DES 加密算法的 key 加密 salt ;然后把结果连接上密码 hash 串的后半部分,一起做一次 MD5 。密文和签名连接在一起,最终会得到一个加过签名的密文 secret 。

  1. C 将第 2 步生成的 secret 连同自己的登陆名,以及需要登陆的游戏名发送给 E 。

  2. E 收到 secret 后,从登陆名查询到用户信息,用保存在 E 上的密码做反向操作,验证签名是否正确。若正确则解出 salt 。失败则发送错误信息给 C ,登陆流程结束。

  3. E 利用解开的 salt ,附加上用户 id 。利用游戏名查询到事先和 G 约定的游戏服务器密码,做同样流程的加密签名处理,发送回 C 。

  4. C 转发认证密文回给 G 。G 用约定要的服务器密码,校验签名并解密,核对 salt 是否是在步骤 1 里发送出去的 salt ,确认用户的合法性。

整个流程,G 和 E 不必保持通讯,只需要事先约定要密码。这可视做一个简化版的 kerberos 协议,其中有一些安全隐患这里不一一指出。目前暂且可用。日后再来完善。

G 和 E 不保持通讯有一个好处:G 不需要对 C 保持连接状态,而只需要最终检查一下 C 发过来的认证包就可以知道认证是否通过。


设计完毕后,我用 C 实现了基本的校验认证函数。老马同学整合到他用的 web server 中,完成了认证服务器。并做了压力测试,结果还不错。

可惜合作方是用 Windows 做开发的,不方便联调。他们在开发期不希望依赖我们的用户系统,而我们的平台系统即使给他们,在 windows 下短期也较难配置起来。

我一拍脑袋,决定帮合作方现写一个认证用 web server 在 windows 下跑。对于我来说,最称手的某过于 lua 了。利用现成的 lua socket 模块,整个认证服务器只需要不到 100 行代码 。把用户名密码直接配置在 lua 源代码中,放到一个 table 中即可,数据库都不需要。反正是调试嘛。

一开始图简单,直接 bind/listen 了 web 端口,然后没 accept 一个认证请求,就处理一个。用阻塞方式工作。我觉得也就是调试一下流程,调通了后就可以用老马做好的正式认证服务器了。

可写完之后,手又痒了。感觉不支持并发很看不过眼,赶紧重写了。


幸亏这次重写,让事后扛住了一次压力。

昨天,合作方花前请了一个公会 600 人帮忙做了一次压力测试。这次测试他们认为是自己的事情,就没有通知我们。结果因为要用他们自己的用户系统(直接对公会放号),就没有联系老马,而是用了我那个拍脑袋临时写的测试用认证服务器。

还好做了并发处理,没在这个问题上翻船。不过昨天还是出了点小事故,在晚饭间接了好几个电话才搞定。

由于一开始考虑到是测试用,我把服务器 bind 地址默认配置成了 127.0.0.1 。结果合作方直接改成了服务器的配置 ip 。然后许多玩家认证不过,客户端编写的时候又没有足够的提示信息。搞了老半天才清楚,服务器配有电信和网通双线,所以是有双 IP 的。最后配置改为 0.0.0.0 搞定了这个问题。


12 月 12 日补充:

关于中间人攻击的问题:

我们需要防范用中间人代理玩家的所有通讯,在玩家完成认证后,取代玩家做一些事情。

为了防止玩家和游戏服务器间的通讯包被篡改,我们需要对游戏服务器和玩家客户端间的通讯协议加密。加密的首要步骤,就是在握手时,交换一个秘密,之后用这个密码对数据加密。

为了安全的交换密码, SSL 这种协议使用的是非对称加密,以及 CA 信任链完成的。但我们这个应用环境下,却有一个额外的优势:玩家和服务器间已经有了一个秘密(用户密码),不需要用复杂的方式交换。

但直接用用户密码做通讯加密是不可行的。因为游戏服务器并不知道这个密码,只是认证服务器知道。

那么,可以这样:在认证环节,认证通过后,认证服务器产生一个随机数,并同时用玩家密码和服务器密码加密两份,返回给玩家。

如果这个环节存在中间人,中间人不可能知道随机数,也无法伪造出两份密文蕴含同样的随机数。

玩家解出属于他的通讯密码(随机数),并把另一份转交给服务器。这样,游戏服务器和玩家间就安全的交换的通讯密码了。

Comments

有点不对的地方。C利用G给的salt做hash。但在hash之前,最好还有一个C与E的交互,交互的内容就是最开始C与E之间达成一致的salt。

还是觉得很复杂如果可以自简单一点就好了 http://www.haobitou.com#03

如果c只是接受到G的响应,而没有发送响应到G,那么G那边应该有个对C的超时判断,判断C的连接登陆超时该close掉。这里应该是这个逻辑吧

2, C 收到 salt ,利用自己的用户密码,对 salt 加密并签名。加密可以使用标准的 DES 算法,签名可以用标准的 MD5 算法。具体方法是:讲用户密码先 md5 hash 一次,得到一个串。取串的前一半做 DES 加密算法的 key 加密 salt ;然后把结果连接上密码 hash 串的后半部分,一起做一次 MD5 。密文和签名连接在一起,最终会得到一个加过签名的密文 secret 。

md5_hash_of_password = md5(pwd)
encrypted_salt = des(first_half(md5_hash_of_password));
secret1 = md5(encrypted_salt + second_half(md5_hash_of_password))

secret = encrypted_salt + secret1

反向操作的时候直接用first_half(md5_hash_of_password) 来解前面这一段encrypted_salt
获得salt后 再用salt来验证一下secret1 这个吗?

@Daly

正文我已经补充了。

E 生成一个 random key 直接编码在前面那个 secret 里,也就是用服务器密码对称加密过的密文,让 C 转交给 G 就可以了。

另外, E 额外发一个用玩家密码对称加密过的通用的 random key 给玩家。

这样 C 和 G 就通过 E 交换了秘密。

这样可行是因为 C 和 E 有共有秘密, G 和 E 也有共有秘密,C 和 G 就可以通过 E 中转秘密了。

但是G在不跟E或玩家认证数据库通信的前提下,不知道client的密码信息,怎么破?

@Daly

对称加密可以解决中间人攻击。我在上面已经补充了。

这是因为,服务器和客户端预先有一个共知的秘密,不需要交换。那就是用户自己的密码。

只需要依赖这个加密通讯就可以了。这个和认证无关。就是说你拿了胸牌也无法和办公室里的员工交流。

@Cloud
不用盗号,登一次把钱和装备洗劫走就够了。对称加密无论如何变形,都解决不了中间人攻击。因为G对C的唯一认识,仅仅是那个secret. 被人拿了胸卡冒名顶替进门口,没有经过私有知识的问答,胸卡里面如何复杂的算法都是浮云。

就是再搞出个session key,最后就是完整的kerberos了。

@Daly

中间人攻击并不能盗号,只能伪装成用户登陆一次。而攻击人是获取不了密码,也修改不了密码的。

Diffie - Hellman 协议本身并不能防止中间人攻击。

防止中间人攻击必须用 CA 保证 C 和 G 的信道安全。如果信道不安全,即使登陆流程被保护了,后续的业务包也可以被监听和篡改,改进 secret 的传输流程不解决根本问题。

加个时间戳即可规避重放攻击,不过需要几个实体间时间同步。

盗号方法:步骤3把E到C的串截取,在步骤4中发给G,即可登录。在网吧很容易实现,网吧都是NAT出,验证IP也没用。

secret被盗号者(中间者)截包,就被登录盗号了。为什么不考虑类似diffie-hellman或WoW的SRP算法这种靠大数数论的秘密交换算法呢。算法不用自己实现,组合用openSSL里面的就可以了。

你给出的狂刃主页http://kr.ejoy.com/打不开……

你给出的狂刃主页http://kr.ejoy.com/打不开……

拼错了,不好意思。

C 和 G 的通讯是合作方的事情了。是否加密,怎样加密都不管了。

文中提到的kobas协议是指Kerberos协议么?
认证后会建立会话密钥对C与G之间的连接加密么?
如果不加密,如何保证后续C与G间通信的安全性?

@tony Huang
你的代码写错了。
Secret 由两部分组成,第一部分是 des 过的 salt 第二部分是 md5 对第一部分的签名。你给出的代码是签名的生成。若把密文丢掉,后续步骤 salt 就无法由 E 还原了。

当提到密码这个词的时候,指数据库里保存的密码。你可以做任何变形。只需要 client 做任何运算前将输入的明文密码做相同前置处理。

"E 收到 secret 后,从登陆名查询到用户信息,用保存在 E 上的密码做反向操作,验证签名是否正确。若正确则解出 salt 。失败则发送错误信息给 C ,登陆流程结束。"
这个过程不是直接说了要用“保存在E上的密码嘛”

"具体方法是:讲用户密码先 md5 hash 一次,得到一个串。取串的前一半做 DES 加密算法的 key 加密 salt ;然后把结果连接上密码 hash 串的后半部分,一起做一次 MD5 。最终会得到一个加过签名的密文 secret 。"
我用代码来解释:
md5_hash_of_password = md5(password);
encrypted_salt = des(first_half(md5_hash_of_password));
secret = md5(encrypted_salt + second_half(md5_hash_of_password))

这里可以看到secret只依赖于md5_hash_of_password,而md5_hash_of_password依赖于password,所以知道md5值一样能够实现验证

@Tony Huang

你完全没看懂 -_-

密码不用明文储存。
储存是储存,你可以把运算用的密码经过任意你需要的运算。比如你可以把用户输入的密码做一次 hash 。数据库中实际储存也用同样的算法就可以了 。和本文讨论的协议无关。

真正的密钥不是 md5 值, md5 在这里是签名用的。真正是密钥就是用户的密码的一部分,用它做了 des 加密 salt 。

擦,上一条消息中,我的博客地址写错了……

这样的验证系统非常不安全:

1)系统必须以明码(准明码)的方式来保存用户的密码。万一出现服务器被黑客攻击,或是运维人员泄库,就会对用户的信息安全(通常情况下用户会在不同的网站上使用相同的用户名和密码)造成极大的安全威胁。

2)整个验证过程中,真正的密钥是用户密码的md5值,而由于现在广泛存在的md5泄漏状况,黑客只需使用md5就可以登陆玩家的账号了。这里至少的改进是第一次做md5时,应当时对密码+salt的字符串做md5 hash

G发到C的salt以及,C到G的应答,都被监听记录下来的话。salt不能重复利用吧?

@zjxuejun

加密是用 des 对称加密。md5 是做签名,用来校验加密方是不是认可的人。

secret是MD5后的,为何还能反向,得到salt,不太明白。

@天浪

salt 是 G 生成的,但是被 G 自己加密过。

换一个游戏 G' ,会用 G' 自己的加密。所以 G 是生成不了 G' 的 salt 的。

确实比较简单,双方约定的密钥安全性有较大风险,提供access token跑完整的 Kobas吧;
另外,因为salt是G生成的,可以结合客户端C上报中间生成的所有密文,未来可以让用户不经过平台而是用平台的密码登陆。换言之,用户被导走了

Post a comment

非这个主题相关的留言请到:留言本