Go項目實戰(zhàn)-注冊、登錄、登出與用戶Token體系的功能整合
前面我們用三篇教程詳述了一個企業(yè)級用戶認(rèn)證體系的設(shè)計與實現(xiàn),其中主要功能:用戶Token的生成、驗證和刷新都已經(jīng)實現(xiàn)了,現(xiàn)在是時候把Token認(rèn)證和我們的用戶結(jié)合到一起啦。
用戶和Token有以下結(jié)合點:
- 用戶登錄成功后,生成Token給用戶客戶端下發(fā)Token串。
- 用戶退出登錄,把用戶登出平臺(Platform) 對應(yīng)的Token和UserSession主動清除掉。
- 用戶修改密碼,用戶在服務(wù)端的所有Token和UserSession全部清除掉,強制用戶在每個平臺重新登錄。
這些用戶行為的邏輯實現(xiàn)都對應(yīng)著對Token和UserSession的不同操作,所以這也是為什么我在專欄的章節(jié)安排和代碼開發(fā)進(jìn)度上先建設(shè)用戶認(rèn)證體系再來做用戶注冊登錄等功能的原因。
本節(jié)我們來實現(xiàn)用戶注冊、登錄、登出的功能。
圖片
注冊功能
即然要把用戶登錄相關(guān)的行為與Token體系整合到一起,我們得先有用戶才行,我們先來把用戶注冊的功能搞定。其實注冊功能的邏輯沒有什么值得大說特說的,唯一一個值得探究的是怎么保證用戶的密碼安全。
用戶密碼安全怎么保證
保證密碼安全有兩個方向
- 用戶的密碼輸入不能太隨意、太容易讓別人蒙對,必須對其長度、復(fù)雜度進(jìn)行限制。
- 用戶的密碼在服務(wù)端必須是加密存儲的。
第一點比較好理解,我們對用戶密碼的長度、構(gòu)成元素(大小寫、數(shù)字、特殊符號)都要有一定的要求。 第二點關(guān)于用戶密碼的加密存儲,我早期工作的幾家公司,有用md5的sha1的,還有他倆一起用的,一般還會再加個鹽(salt) 再進(jìn)行md5、sha1。
而最近工作的兩家公司,用戶密碼是用的 bcryt 。 那這里我們探討一下到底應(yīng)該用哪種?這里先說答案哈,用bcrypt。
md5,sha1,bcrypt 它們都叫做哈希算法,就是把明文變成哈希字符串的算法,不過他們還有小分類。md5、sha1 這些是快速哈希算法,而bcrypt是慢速哈希算法。 什么意思呢?
意思是如果你不停地堆CPU,快速哈希算法的哈希速度也會成倍增長,可以簡單地理解成以前雙核CPU執(zhí)行哈希一個字符串的任務(wù)要花費一秒,變成四核CPU后就要花0.5s了。
而對慢速哈希來說,如果你不停地堆CPU,它執(zhí)行哈希的速度也會變快,但快的非常有限,雙核變四核,速度也就從 1s 變0.9s這種級別的提升。
上面舉例的數(shù)據(jù)是我為了大家好理解自己編的,快速哈希和慢速哈希大概就是上面這個意思。所以數(shù)據(jù)庫中使用bcrypt這種慢速哈希的密碼,即使是數(shù)據(jù)庫數(shù)據(jù)被盜,想要通過撞庫的方式破解用戶的密碼,比使用md5、sha1哈希后的密碼難度和成本要高很多。
Bcypt 哈希后的字符串構(gòu)成如下:
圖片
- Prefix說明了使用的bcrypt的版本
- Cost是進(jìn)行哈希的次數(shù)-數(shù)字越大生成bcrypt的速度越慢,成本越大。同樣也意味著如果密碼庫被盜,攻擊者想通過暴力破解的方法猜測出用戶密碼的成本變得越昂貴。
- Salt是添加到要進(jìn)行哈希的字符串中的隨機字符(21.25個字符),所以使用bcrypt時不需要我們在表里單獨存儲Salt。
- Hashed Text是明文字符串最終被bcrypt應(yīng)用這些設(shè)置哈希后的哈希文本。
搞清楚用戶密碼使用bcrypt加密的原因后,我們先把會用到的工具函數(shù)寫好,在 common/util 目錄下新建 password.go。 搞清楚用戶密碼使用bcrypt加密的原因后,我們先把會用到的工具函數(shù)寫好,在 common/util 目錄下新建 password.go。
Go語言里通過 "golang.org/x/crypto/bcrypt"支持了bcrypt算法的操作,我們把用到的方法封裝到下面的工具函數(shù)中。
package util
import (
"golang.org/x/crypto/bcrypt"
"unicode"
)
func BcryptPassword(plainPassword string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(plainPassword), 11)
return string(bytes), err
}
func BcryptCompare(passwordHash, plainPassword string) bool {
err := bcrypt.CompareHashAndPassword([]byte(passwordHash), []byte(plainPassword))
return err == nil
}
bcrypt.GenerateFromPassword中第二個參數(shù) Cost 越大,速度越慢,在這里我設(shè)成了11,大家可以自己調(diào)整。
bcrypt.CompareHashAndPassword 這個方法能幫我們比對哈希字符串的原串和給定的明文字符串是否相等。如果不相等會報一個類似這樣的錯誤:
crypto/bcrypt: hashedPassword is not the hash of the given password
我們登錄功能時直接用這個方法就能驗證用戶密碼對不對。
繼續(xù)在password.go里添加驗證用戶密碼復(fù)雜度的工具函數(shù)
func PasswordComplexityVerify(s string) bool {
var (
hasMinLen = false
hasUpper = false
hasLower = false
hasNumber = false
hasSpecial = false
)
if len(s) >= 8 {
hasMinLen = true
}
for _, char := range s {
switch {
case unicode.IsUpper(char):
hasUpper = true
case unicode.IsLower(char):
hasLower = true
case unicode.IsNumber(char):
hasNumber = true
case unicode.IsPunct(char) || unicode.IsSymbol(char):
hasSpecial = true
}
}
return hasMinLen && hasUpper && hasLower && hasNumber && hasSpecial
}
這個函數(shù)會檢查密碼的長度、大小寫、數(shù)字、特殊符號這些元素是不是都符合要求,實現(xiàn)注冊邏輯的時候我們直接調(diào)用即可。
下面我們開始實現(xiàn)注冊功能啦,在實現(xiàn)前我們再默念一遍邏輯分層的口訣
請求驗證和數(shù)據(jù)綁定邏輯 --- Controller
外圍業(yè)務(wù)邏輯 --- 應(yīng)用服務(wù)
核心業(yè)務(wù)邏輯 --- 領(lǐng)域服務(wù)
數(shù)據(jù)訪問邏輯 --- 數(shù)據(jù)訪問層
第三方對接 -- Library(這個本節(jié)用不到)
本節(jié)剩余內(nèi)容和詳細(xì)的代碼實現(xiàn),可在加入項目后訪問 https://github.com/go-study-lab/go-mall/compare/c11...c12 就能看本章節(jié)的詳細(xì)代碼。
圖片