密码泄漏,不可小视
今天,跟大家聊聊注册和登录的原理,主要讨论安全问题。安全是一个公司生死存亡的关键,华为和腾讯都有大量的技术人员,来保障业务的安全。
在工作中,一旦遇到了安全问题,必须立即处理,谁都担不起安全责任。安全攻防是一个动态博弈,没有攻不破的防守,也没有防不住的进攻。
而且,在面试中,安全问题也是必然会涉及到的,比如最基本的AES、RSA、TLS、HTTPS、注册登录、密码存储等知识,我们必须熟悉。
注册APP时,你填写用户名和密码,点击提交后,APP后台肯定需要“存储你的用户名和密码”,为后面的登录验证做准备。
登录APP时,你输入用户名和密码,APP后台就能验证你输入的用户名和密码,与当时注册APP时的用户名和密码是否匹配。
那么,有没有思考过这个问题:APP后台开发的程序员GG们,能偷窥到你的密码吗?他们会盗用你的账号和密码进行登录吗?黑客攻击APP后台后,能盗用你的账号和密码吗?
显然,这是不允许的。没有办法在道德和法律的规范下严格禁止这样的行为,那就要从技术机制上禁止,要确保即使在数据库和程序代码被盗走的情况下,坏人也无法破解密码。
一. 密码直接明文存储
如果在APP后台数据库中直接存储用户名和密码,那么黑客可就要在睡梦中笑醒了,比如:
那么,有没有网站中招呢?当然有!当年CSDN博客首当其冲,当时的声明如下:
二. 密码加密存储
自然地,我们想到对密码进行加密存储。不过,这种方式也挺业余的,既然能加密,就有办法解密,同样存在密码被盗的风险,如果APP后台能以某种方式还原密码,那就是流氓系统哈。
我们有这样一个常识:当用户忘记密码后,正确做法不是APP后台告诉用户密码是多少(因为APP后台压根就不应该知道用户的密码),而应该让用户重新设置密码。回想一下,是不是如此?
三. 密码哈希存储
哈希函数,单向散列,且几乎不冲突,故可以考虑对密码进行哈希变换后存储。
从理论上讲,即使哈希值yyyyyy被盗走,别人也难以还原成原来的abcd123了,因为哈希函数不可逆。这样貌似就实现了密码的安全存储,不过,有点太天真了。
如果数据库和程序代码被盗,从理论上讲,难以用yyyyyy反推出密码abcd123, 但黑客可以用彩虹表攻击,分分钟就能破解出密码abcd123, 彩虹表攻击的图示如下:
我来简单解释一下:
黑客对一些常用密码进行哈希,形成一张很大的资料表,一旦黑客盗走数据库中的密码哈希值yyyyyy, 就可以在资料表中进行查找,很快能知道密码是abcd123.
这个资料表,就是所谓的彩虹表。理论上不可反解的哈希函数,被狡猾的黑客给“破解”了,并且得到了密码。所以,直接用哈希的方式存储密码,是肯定不行的。
四. 密码多次哈希存储
有的朋友可能觉得彩虹表能实现攻击,主要是因为只用一次哈希,那么多加几次哈希,是不是就解决问题了呢?
显然不是。多次哈希后,一旦黑客拿到你多次哈希的程序代码和数据库,便可构造多重彩虹表,分分钟破解密码:
五. 固定盐值哈希存储
思考一下,现在的问题是,要抗彩虹表攻击,可以考虑这么做:
P = hash(user + hash(password))
此处的user就是用户名,用作盐值,增加了黑客的破解难度,因为黑客需要针对每个user做一张特定的二重彩虹表,才有可能破解。
即便如此,在攻击一些重要user时,黑客可能感觉值得尝试一下,也是能分分钟进行破解的。所以,用固定盐还是存在较大风险。
六. 随机盐值哈希存储
既然固定盐值不好,那就用随机盐吧,于是可以考虑这么做:
P = hash(salt + hash(password))
这个随机盐salt,需要存储在APP后台数据库中。可问题是,APP和APP后台需要使用相同的随机盐,那么APP是怎样获取APP后台的随机盐呢?
这又涉及到一系列的加密算法和秘钥管理问题,在实际系统中,有些公司就是这么做的。随机盐的引入,可以让彩虹表失效,黑客只能“望盐兴叹”。
七. BCrypt存储
有没有更简洁且安全的做法呢?
有!用BCrypt吧。我觉得,BCrypt这个名字不太好,应该叫bSHA,BCrypt是一个带随机盐的哈希函数,在哈希值中,又携带了盐的信息,而且也是一种单向 Hash 加密算法,因此它不可被反向破解生成明文。
由于计算中使用了随机盐值,并且在密文中包含了 salt 值,默认情况下每次生成的密文都是不同的。随机密文带来的好处是:避免了如果两个人或多个人的密码相同,密码加密后保存到数据库会得到相同的结果,以防破解一个就可以知道其他人的密码。
BCrypt加密后的密文结构如下图所示:
其中密文结构为:$是分割符, 2y是BCrypt加密版本号,10 是 cost 的值(代价因子,值越高,耗时就越长,值为10代表进行,10 轮运算的盐值,10 轮的意思是 2 的 10 次方哈希),紧随其后的前 22 位是盐值(salt),最后的字符串就是密码的密文了。
因为 BCrypt 密文中包含盐值(salt),所以在服务端就能从客户端发送过来的哈希值中解析出 APP 端使用的随机盐值,该随机盐不需要被 APP 后台数据库存储,减少了盐值泄露的风险。
使用 BCrypt 后,注册登录的逻辑图如下:
而且,BCrypt 是一个相对较慢的哈希函数,我们可以通过调整 BCrypt 函数中的代价因子(cost)来控制函数的执行时间,比如针对 8 位密码,在代价因子为 14 的情况,执行 BCrypt 需要 1 秒左右,而 MD5 只需要 0.8 毫秒。
因为 BCrypt 很慢,所以如果黑客想要通过穷举的方式破解密码,那么那些计算机在使用大量的 BCrypt 时性能就会很差,破解的时间成本就很高了。
参考《凤凰架构》里的一个计算方式:如果我们控制 BCrypt 的执行时间大概是 0.1 秒完成一次哈希计算的话,按照 1 秒生成 10 个哈希值的速度,算完所有的 10 位大小写字母()和数字组成的弱密码大概需要 P(62,10)/(3600×24×365)/0.1=1,237,204,169 年时间。
P(62,10)是62取10的排列,这么多种密码,62=26*2的大小写字母+10个数字 3600×24×365,即 3600秒 x 24小时 x 365天
请注意,是 APP 端的 BCrypt 慢,但 APP 后台的校验并不慢,所以不会影响后台服务性能。
最后,我们需要注意的是,虽然黑客已经很难通过彩虹表来破解密码了,但是仍然有可能暴力破解密码,也就是对于同一个用户名使用常见的密码逐一尝试登录。因此,除了做好密码哈希保存的工作外,我们还要建设一套完善的安全防御机制,在感知到暴力破解危害的时候,开启短信验证、图形验证码、账号暂时锁定等防御机制来抵御暴力破解。
八. 扫码登录原理
在本文的最后一部分,我们来聊一下扫码登录的原理。回想一下,扫码登录是不是到处可见呢?那么,扫码登录的逻辑和流程是怎样的呢?别着急,我们一起来看看这个有趣有用的问题。
用户登录APP后,会获取登录态票据(session key, 简写为skey),在后续操作中,不必再输入密码,APP自动携带登录态票据,此时APP后台进行登录态票据校验即可,提升了便利性。
用户登录APP后,获取到了登录态票据skey, 然后用户利用APP对网页上的二维码进行扫描,获取隐藏在二维码中的token,并把token提交到后台,下图一目了然,无需具体解释每一步。
值得注意的是,步骤6执行完后,步骤3中的轮询才会知道网页token和APP端的userID/skey在后台建立了关联,此时扫码登录成功。我曾经也设计了一个扫码登录方案,思路上大同小异。
好的,本文先聊到这里了,相信大家对注册登录中的安全问题有了更深入的理解。愿工作顺利,面试也顺利。今天先这样,咱们明天见。