ZhangZhihui's Blog  

 

 

zzh@ZZHPC:/zdata/Github/greenlight$ migrate create -seq -ext .sql -dir ./migrations create_token_table
/zdata/Github/greenlight/migrations/000005_create_token_table.up.sql
/zdata/Github/greenlight/migrations/000005_create_token_table.down.sql

 

CREATE TABLE IF NOT EXISTS token (
    hash    bytea                       PRIMARY KEY,
    user_id bigint                      NOT NULL REFERENCES users ON DELETE CASCADE,
    expiry  timestamp(0) with time zone NOT NULL,
    scope   text                        NOT NULL
);

 

DROP TABLE IF EXISTS token;

 

       

 

zzh@ZZHPC:/zdata/Github/greenlight$ migrate -path ./migrations -database "$(GREENLIGHT_DB_DSN)" up
5/u create_token_table (15.073531ms)

 

 

复制代码
package data

import (
    "context"
    "crypto/rand"
    "crypto/sha256"
    "encoding/base32"
    "time"

    "greenlight.zzh.net/internal/validator"
)

const ScopeActivation = "activation"

// Token holds the data for a token.
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) {
    // We add the provided ttl (time-to-live) duration parameter to the current time
    // to get the expiry time.
    token := &Token{
        UserID: userID,
        Expiry: time.Now().Add(ttl),
        Scope:  scope,
    }

    // Initialize a zero-valued byte slice with a length of 16 bytes.
    randomBytes := make([]byte, 16)

    // Use the Read() function from the crypto/rand package to fill the byte slice with random 
    // bytes from your operating system's CSPRNG. This will return an error if the CSPRNG fails 
    // to function correctly.
    _, err := rand.Read(randomBytes)
    if err != nil {
        return nil, err
    }

    // Encode the byte slice to a base32-encoded string and assign it to the token's Plaintext 
    // field. This will be the token string that we send to the user in their welcome email. 
    // They will look similar to this: Y3QMGX3PJ3WLRL2YRTQGQ6KRHU
    // Note that by default base32 strings may be padded at the end with the = character. We dont' 
    // need this padding character for the purpose of our tokens, so we omit them.
    token.Plaintext = base32.StdEncoding.WithPadding(base32.NoPadding).EncodeToString(randomBytes)

    // Generate a SHA-256 hash of the plaintext token string. This will be the value that we store 
    // in the 'hash' field of our database table. Note that the sha256.Sum256() function returns 
    // an *array* of length 32, so to make it easier to work with we convert it ot a slice using 
    // the [:] operator before storing it.
    hash := sha256.Sum256([]byte(token.Plaintext))
    token.Hash = hash[:]

    return token, nil
}

// ValidateTokenPlaintext validates the plaintext token is exactly 26 bytes long.
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 struct wraps a database connection pool wrapper.
type TokenModel struct {
    DB *PoolWrapper
}

// New is a shortcut which creates a new Token struct and then inserts the data in the token table.
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 inserts a new record in the token table.
func (m TokenModel) Insert(token *Token) error {
    query := `INSERT INTO token (hash, user_id, expiry, scope) 
              VALUES ($1, $2, $3, $4)`

    args := []any{token.Hash, token.UserID, token.Expiry, token.Scope}

    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    _, err := m.DB.Pool.Exec(ctx, query, args...)

    return err
}

// DeleteAllForUser deletes all tokens for a specific user and scope.
func (m TokenModel) DeleteAllForUser(userID int64, scope string) error {
    query := `DELETE FROM token 
              WHERE user_id = $1 AND scope = $2`

    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    _, err := m.DB.Pool.Exec(ctx, query, userID, scope)

    return err
}
复制代码

 

 

 

 

 

复制代码
package main

import (
    "errors"
    "net/http"
    "time"

    "greenlight.zzh.net/internal/data"
    "greenlight.zzh.net/internal/validator"
)

func (app *application) registerUserHandler(w http.ResponseWriter, r *http.Request) {
    var input struct {
        Name     string `json:"name"`
        Email    string `json:"email"`
        Password string `json:"password"`
    }

    err := app.readJSON(w, r, &input)
    if err != nil {
        app.badRequestResponse(w, r, err)
        return
    }

    user := &data.User{
        Name:      input.Name,
        Email:     input.Email,
        Activated: false,
    }

    err = user.Password.Set(input.Password)
    if err != nil {
        app.serverErrorResponse(w, r, err)
        return
    }

    v := validator.New()

    if data.ValidateUser(v, user); !v.Valid() {
        app.failedValidationResponse(w, r, v.Errors)
        return
    }

    // Insert the user data into the database.
    err = app.models.User.Insert(user)
    if err != nil {
        switch {
        case errors.Is(err, data.ErrDuplicateEmail):
            v.AddError("email", "a user with this email address already exists")
            app.failedValidationResponse(w, r, v.Errors)
        default:
            app.serverErrorResponse(w, r, err)
        }
        return
    }

    // After the user record is created in the database, generate a new activation token
    // for the user.
    token, err := app.models.Token.New(user.ID, 3*24*time.Hour, data.ScopeActivation)
    if err != nil {
        app.serverErrorResponse(w, r, err)
        return
    }

    // Send the welcome email in background.
    app.background(func() {
        data := map[string]any{
            "activationToken": token.Plaintext,
            "userID":          user.ID,
        }

        err = app.emailSender.Send(user.Email, "user_welcome.html", data)
        if err != nil {
            app.logger.Error(err.Error())
        }
    })

    err = app.writeJSON(w, http.StatusCreated, envelope{"user": user}, nil)
    if err != nil {
        app.serverErrorResponse(w, r, err)
    }
}
复制代码

 

 

 

复制代码
func (app *application) activateUserHandler(w http.ResponseWriter, r *http.Request) {
    var input struct {
        TokenPlaintext string `json:"token"`
    }

    err := app.readJSON(w, r, &input)
    if err != nil {
        app.badRequestResponse(w, r, err)
        return
    }

    v := validator.New()

    if data.ValidateTokenPlaintext(v, input.TokenPlaintext); !v.Valid() {
        app.failedValidationResponse(w, r, v.Errors)
        return
    }

    user, err := app.models.User.GetForToken(data.ScopeActivation, input.TokenPlaintext)
    if err != nil {
        switch {
        case errors.Is(err, data.ErrRecordNotFound):
            v.AddError("token", "invalid or expired activation token")
            app.failedValidationResponse(w, r, v.Errors)
        default:
            app.serverErrorResponse(w, r, err)
        }
        return
    }

    // Update the user's activation status.
    user.Activated = true

    // Save the updated user record in database.
    err = app.models.User.Update(user)
    if err != nil {
        switch {
        case errors.Is(err, data.ErrRecordNotFound):
            app.editConflictResponse(w, r)
        default:
            app.serverErrorResponse(w, r, err)
        }
        return
    }

    // If everything went successfully, we delete all activation tokens for the user.
    err = app.models.Token.DeleteAllForUser(user.ID, data.ScopeActivation)
    if err != nil {
        app.serverErrorResponse(w, r, err)
        return
    }

    // Send the updated user details to the client in a JSON response.
    err = app.writeJSON(w, http.StatusOK, envelope{"user": user}, nil)
    if err != nil {
        app.serverErrorResponse(w, r, err)
    }
}
复制代码

 

复制代码
// GetByToken retrives the user associated with a particular activation token from the users table.
func (m UserModel) GetForToken(tokenScope, tokenPlaintext string) (*User, error) {
    query := `SELECT u.id, u.created_at, u.name, u.email, u.password_hash, u.activated, u.version 
                FROM users u 
               INNER JOIN token t ON u.id = t.user_id 
               WHERE t.hash = $1 
                 AND t.scope = $2 
                 AND t.expiry > $3`

    tokenHash := sha256.Sum256([]byte(tokenPlaintext))

    args := []any{tokenHash[:], tokenScope, time.Now()}

    var user User

    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    err := m.DB.Pool.QueryRow(ctx, query, args...).Scan(
        &user.ID,
        &user.CreatedAt,
        &user.Name,
        &user.Email,
        &user.Password.hash,
        &user.Activated,
        &user.Version,
    )
    if err != nil {
        switch {
        case errors.Is(err, pgx.ErrNoRows):
            return nil, ErrRecordNotFound
        default:
            return nil, err
        }
    }

    return &user, nil
}
复制代码

 

In token.go:

复制代码
// DeleteAllForUser deletes all tokens for a specific user and scope.
func (m TokenModel) DeleteAllForUser(userID int64, scope string) error {
    query := `DELETE FROM token 
              WHERE user_id = $1 AND scope = $2`

    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    _, err := m.DB.Pool.Exec(ctx, query, userID, scope)

    return err
}
复制代码

 

复制代码
zzh@ZZHPC:~$ curl -X PUT -d '{"token": "invalid"}' localhost:4000/v1/users/activated
{
    "error": {
        "token": "must be 26 bytes long"
    }
}
zzh@ZZHPC:~$ curl -X PUT -d '{"token": "ABCDEFGHIJKLMNOPQRSTUVWXYZ"}' localhost:4000/v1/users/activated
{
    "error": {
        "token": "invalid or expired activation token"
    }
}
zzh@ZZHPC:~$ curl -X PUT -d '{"token": "BXPVBU3XI2R6PQ2FY5VGVYBI6U"}' localhost:4000/v1/users/activated
{
    "user": {
        "id": 9,
        "created_at": "2024-11-21T21:50:00+08:00",
        "name": "ZhangZhihui",
        "email": "ZhangZhihuiAAA@126.com",
        "activated": true,
        "version": 2
    }
}
zzh@ZZHPC:~$ curl -X PUT -d '{"token": "BXPVBU3XI2R6PQ2FY5VGVYBI6U"}' localhost:4000/v1/users/activated
{
    "error": {
        "token": "invalid or expired activation token"
    }
}
复制代码

 

 

posted on   ZhangZhihuiAAA  阅读(5)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
历史上的今天:
2023-11-21 PASETO - Platform-Agnostic SEcurity TOkens
2023-11-21 JWT - Problem of JWT
 
点击右上角即可分享
微信分享提示