首发地址:个人我的博客,转载请注明出处。php
通过各类安全事件后,不少系统在存放密码的时候不会直接存放明文密码了,大都改为了存放了 md5 加密(hash)后的密码,但是这样真的安全吗?git
这儿有个脚原本测试下MD5的速度, 测试结果:github
[root@f4d5945f1d7c tools]# php speed-of-md5.php Array ( [rounds] => 100 [times of a round] => 1000000 [avg] => 0.23415904045105 [max] => 0.28906106948853 [min] => 0.21188998222351 )
有没有发现一个问题:MD5速度太快了,致使很容易进行暴力破解.算法
简单计算一下:安全
> Math.pow(10, 6) / 1000000 * 0.234 0.234 > Math.pow(36, 6) / 1000000 * 0.234 / 60 8.489451110400001 > Math.pow(62, 6) / 1000000 * 0.234 / 60 / 60 3.69201531296
使用6位纯数字密码,破解只要0.234秒!服务器
使用6位数字+小写字母密码,破解只要8.49分钟!函数
使用6位数字+大小写混合字母密码,破解只要3.69个小时!性能
固然,使用长一点的密码会显著提升破解难度:测试
> Math.pow(10, 8) / 1000000 * 0.234 23.400000000000002 > Math.pow(36, 8) / 1000000 * 0.234 / 60 / 60 / 24 7.640505999359999 > Math.pow(62, 8) / 1000000 * 0.234 / 60 / 60 / 24 / 365 1.6201035231755982
使用8位纯数字密码,破解要23.4秒!this
使用8位数字+小写字母密码,破解要7.64小时!
使用8位数字+大小写混合字母密码,破解要1.62年!
可是,别忘了,这个速度只是用PHP这个解释型语言在笔者的弱鸡我的电脑(i5-4460 CPU 3.20GHz)上跑出来的,还只是利用了一个线程一个CPU核心。如果放到最新的 Xeon E7 v4系列CPU的服务器上跑,充分利用其48个线程,并使用C语言来重写下测试代码,很容易就能提高个几百上千倍速度。那么即便用8位数字+大小写混合字母密码,破解也只要14小时!
更况且,不少人的密码都是采用比较有规律的字母或数字,更能下降暴力破解的难度... 若是没有加盐或加固定的盐,那么彩虹表破解就更easy了...
提高安全性就是提高密码的破解难度,至少让暴力破解难度提高到攻击者没法负担的地步。(固然用户密码的长度固然也很重要,建议至少8位,越长越安全)
这里不得不插播一句:PHP果真是世界上最好的语言 -- 标准库里面已经给出了解决方案。
PHP 5.5 的版本中加入了 password_xxx
系列函数, 而对以前的版本,也有兼容库能够用:password_compat.
在这个名叫“密码散列算法”的核心扩展中提供了一系列简洁明了的对密码存储封装的函数。简单介绍下:
password_hash
是对密码进行加密(hash),目前默认用(也只能用)bcrypt算法,至关于一个增强版的md5函数
password_verify
是一个验证密码的函数,内部采用的安全的字符串比较算法,能够预防基于时间的攻击, 至关于 $hashedPassword === md5($inputPassword)
password_needs_rehash
是判断是否须要升级的一个函数,这个函数厉害了,下面再来详细讲
password_hash
须要传入一个算法,如今默认和可使用的都只有bcrypt算法,这个算法是怎么样的一个算法呢?为何PHP标准库里面会选择bcrypt呢?
bcrypt是基于 Blowfish 算法的一种专门用于密码哈希的算法,由 Niels Provos 和 David Mazieres 设计的。这个算法的特别之处在于,别的算法都是追求快,这个算法中有一个相当重要的参数:cost. 正如其名,这个值越大,耗费的时间越长,并且是指数级增加 -- 其加密流程中有一部分是这样的:
EksBlowfishSetup(cost, salt, key) state <- InitState() state <- ExpandKey(state, salt, key) repeat (2^cost) // "^"表示指数关系 state <- ExpandKey(state, 0, key) state <- ExpandKey(state, 0, salt) return state
好比下面是笔者的一次测试结果(我的弱机PC, i5-4460 CPU 3.20GHz) :
cost time 8 0.021307 9 0.037150 10 0.079283 11 0.175612 12 0.317375 13 0.663080 14 1.330451 15 2.245152 16 4.291169 17 8.318790 18 16.472902 19 35.146999
附:测试代码
这个速度与md5相比简直是蜗牛与猎豹的差异 -- 即便按照cost=8, 一个8位的大小写字母+数字的密码也要14万年才能暴力破解掉,更况且通常服务器都会至少设置为10或更大的值(那就须要54万年或更久了)。
显然,cost不是越大越好,越大的话会越占用服务器的CPU,反而容易引发DOS攻击。建议根据服务器的配置和业务的需求设置为10~12便可。最好同时对同一IP同一用户的登陆尝试次数作限制,预防DOS攻击。
总上所述,一个安全地存储密码的方案应该是这样子的:(直接放代码吧)
class User extends BaseModel { const PASSWORD_COST = 11; // 这里配置bcrypt算法的代价,根据须要来随时升级 const PASSWORD_ALGO = PASSWORD_BCRYPT; // 默认使用(如今也只能用)bcrypt /** * 验证密码是否正确 * * @param string $plainPassword 用户密码的明文 * @param bool $autoRehash 是否自动从新计算下密码的hash值(若是有必要的话) * @return bool */ public function verifyPassword($plainPassword, $autoRehash = true) { if (password_verify($plainPassword, $this->password)) { if ($autoRehash && password_needs_rehash($this->password, self::PASSWORD_ALGO, ['cost' => self::PASSWORD_COST])) { $this->updatePassword($plainPassword); } return true; } return false; } /** * 更新密码 * * @param string $newPlainPassword */ public function updatePassword($newPlainPassword) { $this->password = password_hash($newPlainPassword, self::PASSWORD_ALGO, ['cost' => self::PASSWORD_COST]); $this->save(); } }
这样子,在用户注册或修改密码的时候就调用 $user->updatePassword()
来设置密码,而登陆的时候就调用 $user->verifyPassword()
来验证下密码是否正确。
当硬件性能提高到必定程度,而cost=11没法知足安全需求的时候,则修改下 PASSWORD_COST
的值便可无缝升级,让存放的密码更安全。