在上一篇我們解決了「如何驗證密碼」的問題,現在要解決下一個問題:「驗證成功後,如何讓使用者保持登入狀態?」答案就是使用 Token。
本文將深入探討現代無狀態 API 認證的基石——JWT (JSON Web Token),並實作一個完整的 Token 生成與驗證服務。
JWT(JSON Web Token)是一種基於 JSON 的開放標準(RFC 7519),用於在各方之間以緊湊且自包含的方式安全地傳輸資訊。它通常用於身份驗證和資訊交換。
更多關於 JWT 的介紹可以參考 JWT 官方網站
可以在 這裡 找到各個 Go 的 JWT 函式庫,並包含其支援哪些演算法
JWT 本質上是一個由三個部分組成的、用 .
分隔的長字串,例如 xxxxx.yyyyy.zzzzz
。它緊湊且 URL 安全,非常適合在 HTTP Header 中傳輸。
alg
(簽名演算法,如 HS256
)和 typ
(類型,固定為 JWT
)。⚠️ 極其重要:JWT 是**被簽名(Signed)的,而不是被加密(Encrypted)**的。因此,絕對不要在 JWT 的 Payload 中存放任何敏感資訊!
一個更安全的簽名演算法,例如 ES512
(橢圓曲線數位簽章算法),會比 HS256
更安全,因為它使用一對公私鑰來簽名和驗證。
本文會以 ES512
為例來實作。
在開始實作之前,我們需要一對 ECDSA 公私鑰來簽名和驗證 JWT。
可以使用 OpenSSL 來生成
底下這段把生成的私鑰直接存到 .env
檔案中:
openssl ecparam -genkey -name secp521r1 -noout | tee -a | awk '{printf "%s""\\n",$$0}' | rev | cut -c3- | rev | awk '{printf "\nTOKEN_SECRET=\"%s\"\n",$$0}' >> .env
這會在 .env
檔案中新增一行 TOKEN_SECRET
,其值為生成的私鑰。
TokenService
現在,讓我們動手來實作一個專門處理 Token 的服務。
github.com/pascaldekloe/jwt
函式庫:挑選這一個 JWT Library,因為它簡潔且易於使用。go get github.com/pascaldekloe/jwt
internal/service/token/
目錄下建立 token.go
。type Service struct {
privateKey *ecdsa.PrivateKey
}
func NewService(key []byte) (*Service, error) {
block, _ := pem.Decode(key)
if block == nil {
return nil, fmt.Errorf("failed to decode PEM block")
}
privatekey, err := x509.ParseECPrivateKey(block.Bytes)
if err != nil {
return nil, fmt.Errorf("failed to parse EC private key: %w", err)
}
return &Service{privateKey: privatekey}, nil
}
token_type.go
中定義不同的 Token 類型。/internal/service/token/token_type.go
type tokenType string
const (
tokenTypeAccessToken tokenType = "access"
)
// signToken 使用私鑰對 Claims 進行簽名,並返回簽名後的 JWT 字串
func (s *Service) signToken(claims jwt.Claims) (string, error) {
signed, err := claims.ECDSASign("ES512", s.privateKey)
if err != nil {
return "", err
}
return string(signed), nil
}
// checkToken 使用公鑰驗證 JWT 字串的簽名,並返回解析後的 Claims
func (s *Service) checkToken(token string) (*jwt.Claims, error) {
return jwt.ECDSACheck([]byte(token), &s.privateKey.PublicKey)
}
// 使用固定的結構來生成不同類型的 Token
func (s *Service) generateToken(tokenType tokenType, userID int, expire time.Duration) (string, error) {
var claims jwt.Claims
random := uuid.New()
now := time.Now().Round(time.Second)
expires := now.Add(expire).Round(time.Second)
claims.Issued = jwt.NewNumericTime(now)
claims.Expires = jwt.NewNumericTime(expires)
claims.Set = map[string]any{
"uid": random.String(),
"id": userID,
"type": tokenType,
}
return s.signToken(claims)
}
// 驗證 Token 的有效性,並返回 Token 中的使用者 ID
func (s *Service) validateToken(tokenType tokenType, token string) (int, error) {
claims, err := s.checkToken(token)
if err != nil {
return 0, fmt.Errorf("invalid token: %w", err)
}
if claims.Expires.Time().Before(time.Now()) {
return 0, fmt.Errorf("token expired")
}
if claims.Set["type"] != string(tokenType) {
return 0, fmt.Errorf("invalid token type")
}
userID, ok := claims.Set["id"].(float64)
if !ok {
return 0, fmt.Errorf("invalid user ID in token")
}
return int(userID), nil
}
GenerateAccessToken
方法。/internal/service/token/token_func.go
func (s *Service) GenerateAccessToken(userID int) (string, error) {
return s.generateToken(tokenTypeAccessToken, userID, time.Hour*24)
}
ValidateAccessToken
方法。/internal/service/token/token_func.go
func (s *Service) ValidateAccessToken(token string) (int, error) {
return s.validateToken(tokenTypeAccessToken, token)
}
internal/application/service.go
func NewService(app *Application) (*Service, error) {
passwordService := password.NewService()
tokenService, err := token.NewService([]byte(app.Config.Token.Secret))
if err != nil {
return nil, err
}
return &Service{
Password: passwordService,
Token: tokenService,
}, nil
}
現在,我們擁有了一個可以安全地簽發和驗證「通行證」的服務。
在下一篇文章中,我們將實作 Database 連線。
以上程式碼的完整內容可以到 Github 觀看