【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 一樣,將實現以下方法:

我們還創建一個 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