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