如何處理使用者的密碼,是衡量一個後端開發者專業程度和安全意識的試金石。
一旦處理不當,導致使用者密碼洩漏,將會對使用者和公司帶來災難性的後果。
本篇文章,我們將學習密碼儲存的黃金標準。
安全鐵律第一條:絕對、絕對、絕對不要用明文儲存密碼!
有些開發者可能會想:「我把密碼加密一下再存總可以了吧?」於是他們使用了像 MD5 或 SHA256 這樣的快速雜湊演算法。這是一個極其危險的誤區。
雜湊(Hashing) vs. 加密(Encryption):加密是可逆的,有金鑰就可以解密回原文。雜湊是單向的,理論上無法從結果反推出原文。儲存密碼,我們需要的是單向的雜湊。
快速雜湊演算法的漏洞:像 MD5 和 SHA 這類演算法,被設計出來的目的是追求「快」,以便快速校驗檔案的完整性。但這個「快」字,在密碼學上卻是致命的弱點。攻擊者可以利用這個特性,建立一個「彩虹表(Rainbow Table)」。他們可以預先計算好上億個常用密碼的雜湊值,存成一個巨大的查詢表。一旦他們拿到了你資料庫裡的密碼雜湊,只需在彩虹表裡一查,就能瞬間反推出大量使用者的原始密碼。
原文: ji394su3
md5: 7b96d9d359699d09c0ec43d521689ee5
sha1: 3fe25f46739b6ae8fa930d8a8acaeb692675fe4f
bcrypt
為了安全地儲存密碼,我們需要一個慢速的、加鹽的雜湊演算法。
bcrypt
就是專為此目的而設計的。
bcrypt
有三個核心特性,完美地解決了上述問題:
慢(Slow):這是 bcrypt
最重要的特性。它被故意設計得非常消耗計算資源。這意味著,即使攻擊者拿到了你的資料庫,想對一條密碼進行暴力破解,也需要花費極長的時間,使得攻擊在經濟上和時間上都變得不可行。
加鹽(Salted):這是 bcrypt
對抗彩虹表的法寶。在對密碼進行雜湊前,bcrypt
會為每一位使用者自動生成一個獨一無二的隨機字串,稱為「鹽(Salt)」。它實際進行的操作是 hash(password + salt)
。這樣一來,即使兩個使用者使用了完全相同的密碼(例如 123456
),由於他們的「鹽」不同,最終儲存在資料庫中的雜湊值也將是截然不同的。彩虹表瞬間失效。
可調整的成本因子(Cost Factor):bcrypt
允許我們設定一個「成本因子」,這個數字決定了計算的複雜度。隨著未來電腦算力的提升,我們可以簡單地透過調高這個成本因子,來繼續保證雜湊的「慢速」,以應對更強大的硬體。
PasswordService
現在,讓我們動手來實作一個專門處理密碼的服務。
使用 bcrypt
函式庫:Go 的官方擴充套件加密庫中已經包含了 bcrypt
的實作。
建立服務檔案:
// internal/service/password/password.go
// 提供密碼雜湊功能
func (s *Service) Hash(password string) (string, error) {
hashedBytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", err
}
return string(hashedBytes), nil
}
// 提供密碼比對功能
func (s *Service) Compare(hashed, password string) error {
return bcrypt.CompareHashAndPassword([]byte(hashed), []byte(password))
}
internal/application/service.go
type Service struct {
Password *password.Service
}
func NewService(app *Application) *Service {
return &Service{
Password: password.NewService(),
}
}
internal/application/application.go
func New(cfg *config.Config) (*Application, error) {
... // 省略其他初始化程式碼
app := &Application{
...
}
app.Service = NewService(app)
return app, nil
}
在本篇文章中,我們學習了安全儲存密碼的關鍵原則,並使用 bcrypt
演算法實作了一個健壯的 PasswordService
。
這為我們整個身份驗證系統打下了最堅實、最安全的基礎。
也完成了我們之前 usecase 所需要的 password Interface。
現在,我們已經能夠安全地「驗證」使用者的身份了。
在下一篇文章中,我們將解決下一個問題:驗證成功後,如何發給使用者一個安全的「通行證」,讓他們可以在後續的請求中證明自己的身份。
這個通行證,就是 JWT (JSON Web Token)。
也就是我們之前 usecase 所需要的 token Interface。
以上程式碼的完整內容可以到 Github 觀看