美文网首页
【Go Web开发】创建用户激活token

【Go Web开发】创建用户激活token

作者: Go语言由浅入深 | 来源:发表于2022-03-16 21:49 被阅读0次

上一篇文章创建了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只在特定场景下使用例如,确定性随机是可接受情况,需要快速生成随机数时可用。

相关文章

网友评论

      本文标题:【Go Web开发】创建用户激活token

      本文链接:https://www.haomeiwen.com/subject/fahmdrtx.html