在幾年前,相信不少和我同樣的開發者都是使用 MD5 函數對用戶的密碼等敏感內容進行哈希化後存儲到數據庫中。即使是如今,仍是不少開發者是這樣的作法。php
但不少事實告訴咱們,現在用 MD5 函數生成的值在基於 彩虹表🌈 和強大的 GPU 數億次每秒的暴力破解下能較爲輕鬆的破解。golang
因此對於須要保存用戶密碼等敏感信息的需求場景下,咱們須要尋找另外一種可靠安全的加密方式。算法
有關爲何 MD5 已經不可靠的緣由能夠參考個人另外一篇文章《十萬個爲何:別用 MD5 加密密碼》。數據庫
當下推薦的方案是使用 bcrypt 函數生成密碼,並非由於它絕對安全不可破解,而是破解的成本足夠高。安全
世上沒有絕對的安全,咱們能作的就是提升破解的成本。
首先 bcrypt 是一個密碼加密函數,由 Niels Provos 和 David Mazieres 設計,在 1999 年正式向世人提出。服務器
這個函數由兩個關鍵因素確保其可靠安全:函數
第一點:就像不少其餘加密函數或者方案同樣,會參雜一個 salt 進去,也就是咱們常說的「加鹽」,有了鹽🧂,攻擊者經過彩虹表就沒法破解了,他必須把鹽值也猜出來纔有可能破解。性能
可是光是加鹽,不少加密函數或者咱們使用 MD5 搭配鹽也能弄,不是使用它的強有力理由。加密
第二點:bcrypt 經過接受一個參數 cost 提升計算時長,換句話說,cost 數值越大,bcrypt 運行計算所需的時間就越長。設計
想象一個場景:
咱們經過 MD5 + salt 的方案生成密碼,攻擊者拿到這個密碼後,基於彩虹表攻擊無效後,乾脆直接採用暴力搜索攻擊,由於執行一次 MD5 函數所需的時間在強大的計算能力面前是很短暫的,因此也不須要花多少時間就能破解出來,咱們假定執行一次 MD5 函數所需時間是 1 毫秒。
如今咱們換成使用 bcrypt 函數生成密碼,咱們生成的時候先指定這個 cost 參數值爲 1,而且此時執行一次 bcrypt 函數所需時間也是 1 毫秒,但若是咱們增大這個 cost 參數值,好比爲 10,此時執行一次 bcrypt 函數所需時間多是 50 毫秒,那麼等因而原先平均只須要 1 小時就能破解一個密碼如今須要 50 小時才能破解一個。
攻擊者每每是一次破解一批用戶的密碼,因此能夠想象這個時間成本和算力成本有多大了。
通常的,咱們默認取 10 做爲 cost 參數的值,好比 Go 中的 bcrypt.DefaultCost
就是 10。
咱們也能夠根據當前服務器的性能選擇一個合適的 cost 值,好比我想執行一次 bcrypt 的時間不超過 200 毫秒,這樣既不會容易被破解,也不會太耗時,那麼咱們能夠根據下面這段 Go 代碼來選擇出一個合適的 cost 值:
package main import ( "fmt" "time" "golang.org/x/crypto/bcrypt" ) func main() { for cost := 10; cost <= 20; cost++ { start := time.Now() bcrypt.GenerateFromPassword([]byte("pa55w0rd"), cost) fmt.Printf("cost: %d, duration: %v\n", cost, time.Since(start)) } }
在個人本機上執行結果以下:
cost: 10, duration: 75.797197ms cost: 11, duration: 146.597944ms cost: 12, duration: 298.971358ms cost: 13, duration: 610.758023ms cost: 14, duration: 1.181615153s cost: 15, duration: 2.433344989s cost: 16, duration: 4.917117451s cost: 17, duration: 9.453614867s cost: 18, duration: 19.186913882s cost: 19, duration: 37.79228015s cost: 20, duration: 1m16.157706237s
那麼我就能夠據此選擇 cost 值爲 11,其實從上面的運行結果也能看出來,cost 值和運行時間之間的關係:cost 每增長一,運行耗時就會翻一倍。對具體算法有興趣的人能夠在文末的 Wikipedia 參考連接中找到更多相關信息。
也附上如下 PHP 版本的吧:
<?php for ($cost = 10; $cost <= 15; $cost++) { $start = microtime(true); password_hash("test", PASSWORD_BCRYPT, ["cost" => $cost]); $end = microtime(true); echo "cost: " . $cost . ", duration: " . ($end - $start) * 1000 . "\n"; } ?>
這裏提供 PHP 和 Go 的兩段示例代碼供參考:
<?php $pwd = "pa55w0rd"; echo "Origin Password: " . $pwd . "\n"; $hash = password_hash($pwd, PASSWORD_BCRYPT, ["cost" => 10]); echo "Encrypted Password: " . $hash . "\n"; $match = password_verify($pwd, $hash); echo "Match Result: " . $match; ?>
package main import ( "fmt" "golang.org/x/crypto/bcrypt" ) func main() { pwd := "pa55w0rd" fmt.Println("Origin Password: " + pwd) hash, _ := bcrypt.GenerateFromPassword([]byte(pwd), bcrypt.DefaultCost) fmt.Println("Encrypted Password: " + string(hash)) err := bcrypt.CompareHashAndPassword(hash, []byte(pwd)) fmt.Println("Match Result: ", err == nil) }