首發地址:個人我的博客,轉載請註明出處。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
的值便可無縫升級,讓存放的密碼更安全。