【Go Web 開發】創建用戶激活 token
上一篇文章創建了 token 數據庫表,而我們激活過程的完整性取決於一件關鍵的事情:發送到用戶郵箱的 token(稱爲令牌)具有 “不可猜測性”。如果令牌很容易被猜到或可以被暴力破解,那麼攻擊者就有可能激活用戶帳戶,即使他們無法訪問用戶的郵箱。
因此,需要生成的 token 具有足夠的隨機性,不可能被猜出來。在這裏我們使用 Go 的 crypto/rand 包 128 位(16 字節)墒。如果你跟隨本系列文章操作,請創建新文件 internal/data/tokens.go。在接下來的幾節中,這將作爲所有與創建和管理 tokens 相關的代碼文件。
$ touch internal/data/tokens.go
在文件中定義 Token 結構體(表示單個 token 包含的數據)和生成 token 的函數 generateToken()。這裏直接進入代碼可以更好地說明並描述所要做的事情。
File: internal/data/tokens.go
package data
import (
"crypto/rand"
"crypto/sha256"
"encoding/base32"
"time"
)
// 定義token使用範圍常量。這裏我們只有激活token,後面會增加新的用途常量。
const (
ScopeActivation = "activation"
)
// 定義Token結構體接收token數據。包括token字符串和哈希值,以及用戶ID,過期時間和範圍。
type Token struct {
Plaintext string
Hash []byte
UserID int64
Expiry time.Time
Scope string
}
func generateToken(userID int64, ttl time.Duration, scope string) (*Token, error) {
//創建Token實例,包含用戶ID,過期時間和使用範圍scope。注意使用ttl來計算過期時間。
token := &Token{
UserID: userID,
Expiry: time.Now().Add(ttl),
Scope: scope,
}
//初始化一個16字節數組
randomBytes := make([]byte, 16)
//使用crypto/rand包的Read函數來填充字節數組,隨機數來自操作系統。
_, err := rand.Read(randomBytes)
if err != nil {
return nil, err
}
//將生成的字節數組轉爲base-32字符串並賦值給token的plaintext字段。這個字符串將
//通過郵件發送給用戶。類似以下內容:
// Y3QMGX3PJ3WLRL2YRTQGQ6KRHU
//注意base-32默認會使用"="填充末尾。這裏我們不需要填充,因此使用withPadding(base32.NoPadding)方法
token.Plaintext = base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(randomBytes)
//將token的文本內容生成SHA-256哈希。這個值將寫入數據庫的hash列。
hash := sha256.Sum256([]byte(token.Plaintext))
token.Hash = hash[:]
return token, nil
}
需要指出的是,我們在這裏創建的純文本 token 字符串 (如 Y3QMGX3PJ3WLRL2YRTQGQ6KRHU) 不是 16 個字符長,而是具有 16 個字節的隨機熵。
明文 token 字符串本身的長度取決於如何對這 16 個隨機字節進行編碼。在我們的例子中,我們將隨機字節編碼爲一個 base-32 的字符串,這將產生一個包含 26 個字符的字符串。相反,如果我們使用十六進制 (以 16 爲基數) 對隨機字節進行編碼,字符串的長度將變爲 32 個字符。
創建數據庫模型 TokenModel 和字段校驗
下面開始設置 TokenModel 類型用於和數據庫 token 表的交互。該過程和前面的 MoiveModel 和 UserModel 一樣,將實現以下方法:
-
Insert() 向數據庫 token 表中插入新的 token。
-
New() 通過調用 generateToken() 函數來創建一個新的 token,並調用 Insert() 存儲數據。
-
DeleteAllForUser() 刪除用戶特定範圍的所有 tokens。
我們還創建一個 ValidateTokenPlaintext() 函數,用於校驗傳入 token 是否爲 26 字節。
再次打開 internal/data/tokens.go 文件,添加以下代碼:
File:internal/data/tokens.go
package main
...
//校驗plaintext是否是26字節
func ValidateTokenPlaintext(v *validator.Validator, tokenPlaintext string) {
v.Check(tokenPlaintext != "", "token", "must be provided")
v.Check(len(tokenPlaintext) == 26, "token", "must be 26 bytes long")
}
//定義TokenModel類型
type TokenModel struct {
DB *sql.DB
}
// New方法是創建Token結構體的構造方法,然後用於插入數據庫tokens表
func (m TokenModel)New(userID int64, ttl time.Duration, scope string) (*Token, error) {
token, err := generateToken(userID, ttl, scope)
if err != nil {
return nil, err
}
err = m.Insert(token)
return token, err
}
// Insert()將token數據插入數據庫tokens表
func (m TokenModel)Insert(token *Token) error {
query := `
INSERT INTO tokens (hash, user_id, expiry, scope)
VALUES ($1, $2, $3, $4)`
args := []interface{}{token.Hash, token.UserID, token.Expiry, token.Scope}
ctx , cancel := context.WithTimeout(context.Background(), 3 * time.Second)
defer cancel()
_, err := m.DB.ExecContext(ctx, query, args...)
return err
}
// DeleteAllForUser()刪除特定用戶和範圍的所有tokens
func (m TokenModel)DeleteAllForUser(scope string, userID int64) error {
query := `
DELETE FROM tokens
WHERE scope = $1 AND user_id = $2`
ctx, cancel := context.WithTimeout(context.Background(), 3 * time.Second)
defer cancel()
_, err := m.DB.ExecContext(ctx, query, scope, userID)
return err
}
最後,我們需要更新 internal/data/models.go 文件,將 TokenModel 添加到 Model 結構體中:
File:internal/data/models.go
package data
...
type Models struct {
Movies MovieModel
Tokens TokenModel
Users UserModel
}
func NewModels(db *sql.DB) Models {
return Models{
Movies: MovieModel{DB: db},
Tokens: TokenModel{DB: db}, //初始化TokenModel實例
Users: UserModel{DB: db},
}
}
此時啓動應用程序,代碼應該可以正常運行。
$ go run ./cmd/api
{"level":"INFO","time":"2022-01-03T03:01:20Z","message":"database connection pool established"}
{"level":"INFO","time":"2022-01-03T03:01:20Z","message":"starting server","properties":{"addr":":4000","env":"development"}}
附加內容
math/rand 包
Go 有一個 math/rand 包能夠提供確定性僞隨機數生成器 (PRNG)。注意不要使用 math/rand 包來創建安全隨機數,例如生成 token 和密碼的時候。實際上,使用 crypto/rand 作爲標準實踐可以說是最好的。math/rand 只在特定場景下使用例如,確定性隨機是可接受情況,需要快速生成隨機數時可用。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/Npu-g855oYpcnSC0Q5I1lw