ایجاد توکنهای فعالسازی امن
یکپارچگی فرآیند فعالسازی ما به یک نکته کلیدی بستگی دارد: ‘حدسناپذیری’ توکنی که به آدرس ایمیل کاربر ارسال میکنیم. اگر توکن به راحتی قابل حدس زدن باشد یا بتوان آن را به صورت brute-force کرد، در آن صورت مهاجم میتواند حساب کاربر را حتی بدون دسترسی به صندوق ورودی ایمیل کاربر فعال کند.
به همین دلیل، میخواهیم توکن توسط یک مولد اعداد تصادفی امن رمزنگاریشده (CSPRNG) تولید شود و دارای انتروپی (یا تصادفیت) کافی باشد به طوری که حدس زدن آن غیرممکن باشد. از Go 1.24 به بعد، یک روش ساده برای ایجاد توکنی که این معیارها را دارد، استفاده از تابع rand.Text() از پکیج crypto/rand است.
این توکنهایی با ۱۲۸ بیت (۱۶ بایت) انتروپی تولید میکند که با الفبای استاندارد base32 رمزگذاری شدهاند. در عمل، این به این معنی است که rand.Text() رشتههایی به طول ۲۶ کاراکتر برمیگرداند که به این شکل هستند:
CN5MWVETIILGP32FBV3EOGBNRV LYDISI72PTLGTIVEDSV5IATEAR EADOYJU5WJC3CCR3KZPSJW5BJA
اگر میخواهید همراه ما پیش بروید، یک فایل جدید internal/data/tokens.go ایجاد کنید. این فایل به عنوان خانه تمام منطق مربوط به ایجاد و مدیریت توکنها در چند فصل آینده عمل خواهد کرد.
$ touch internal/data/tokens.go
سپس در این فایل یک struct به نام Token (برای نمایش دادههای یک توکن خاص) و یک تابع generateToken() که میتوانیم برای ایجاد توکن جدید استفاده کنیم، تعریف میکنیم.
این یکی دیگر از مواقعی است که احتمالاً بهتر است مستقیماً وارد کد شویم و در حین کار توضیح دهیم چه اتفاقی میافتد.
package data import ( "crypto/rand" "crypto/sha256" "encoding/base32" "time" ) // Define constants for the token scope. For now we just define the scope "activation" // but we'll add additional scopes later in the book. const ( ScopeActivation = "activation" ) // Define a Token struct to hold the data for an individual token. This includes the // plaintext and hashed versions of the token, associated user ID, expiry time and // scope. type Token struct { Plaintext string Hash []byte UserID int64 Expiry time.Time Scope string } func generateToken(userID int64, ttl time.Duration, scope string) *Token { // Create a Token instance. In this, we set the Plaintext field to be a random // token generated by rand.Text(), and also set values for the user ID, expiry, and // scope of the token. Notice that we add the provided ttl (time-to-live) duration // argument to the current time to get the expiry time. token := &Token{ Plaintext: rand.Text(), UserID: userID, Expiry: time.Now().Add(ttl), Scope: scope, } // Generate a SHA-256 hash of the plaintext token string. This will be the value // that we store in the `hash` column 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 to a slice using the [:] operator before storing it. hash := sha256.Sum256([]byte(token.Plaintext)) token.Hash = hash[:] return token }
ایجاد TokenModel و بررسیهای اعتبارسنجی
بیایید به مسیر خود ادامه دهیم و یک نوع TokenModel راهاندازی کنیم که تعاملات پایگاه داده با جدول PostgreSQL tokens ما را در بر میگیرد. ما الگوی بسیار مشابهی با MovieModel و UsersModel دنبال خواهیم کرد و سه متد زیر را روی آن پیادهسازی میکنیم:
Insert()برای درج یک رکورد توکن جدید در پایگاه داده.New()یک متد میانبر است که یک توکن جدید با استفاده از تابعgenerateToken()ایجاد میکند و سپسInsert()را برای ذخیره دادهها فراخوانی میکند.DeleteAllForUser()برای حذف تمام توکنهای با یک scope خاص برای یک کاربر خاص.
همچنین یک تابع جدید ValidateTokenPlaintext() ایجاد خواهیم کرد که بررسی میکند آیا یک توکن متن ساده که در آینده توسط کلاینت ارائه میشود دقیقاً ۲۶ بایت طول دارد یا خیر.
دوباره فایل internal/data/tokens.go را باز کنید و کد زیر را اضافه کنید:
package data import ( "context" // New import "crypto/rand" "crypto/sha256" "database/sql" // New import "encoding/base32" "time" "greenlight.alexedwards.net/internal/validator" // New import ) ... // Check that the plaintext token has been provided and 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") } // Define the TokenModel type. type TokenModel struct { DB *sql.DB } // The New() method is a shortcut which creates a new Token struct and then inserts the // data in the tokens table. func (m TokenModel) New(userID int64, ttl time.Duration, scope string) (*Token, error) { token := generateToken(userID, ttl, scope) err := m.Insert(token) return token, err } // Insert() adds the data for a specific token to the tokens table. func (m TokenModel) Insert(token *Token) error { query := ` INSERT INTO tokens (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.ExecContext(ctx, query, args...) return err } // DeleteAllForUser() deletes all tokens for a specific user and scope. 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 جدید در struct والد Models ما گنجانده شود. به این صورت:
package data ... type Models struct { Movies MovieModel Tokens TokenModel // Add a new Tokens field. Users UserModel } func NewModels(db *sql.DB) Models { return Models{ Movies: MovieModel{DB: db}, Tokens: TokenModel{DB: db}, // Initialize a new TokenModel instance. Users: UserModel{DB: db}, } }
در این مرحله باید بتوانید برنامه را مجدداً راهاندازی کنید و همه چیز بدون مشکل کار کند.
$ go run ./cmd/api/ time=2023-09-10T10:59:13.722+02:00 level=INFO msg="database connection pool established" time=2023-09-10T10:59:13.722+02:00 level=INFO msg="starting server" addr=:4000 env=development