JWT 身份验证:构建安全的 API 认证系统

深入学习 JWT(JSON Web Token)的原理和实现,构建无状态的 API 身份验证系统

JWT 身份验证:构建安全的 API 认证系统

在构建现代 Web API 时,身份验证是不可或缺的一环。传统的 Session 认证方式在分布式系统中面临诸多挑战,而 JWT(JSON Web Token)提供了一种优雅、无状态的解决方案。

本文将深入探讨 JWT 的原理、实现和最佳实践,帮你构建安全可靠的认证系统。

什么是 JWT?

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输声明信息。它是一个经过数字签名的 JSON 对象,可以被验证和信任。

JWT 的结构

一个 JWT 由三部分组成,用点号(.)分隔:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxMjMsInVzZXJuYW1lIjoiYWxpY2UifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
|____________header____________|.|_______________________payload_______________________|.|___________________signature___________________|

1. Header(头部)

{
  "alg": "HS256",
  "typ": "JWT"
}
  • alg:签名算法(HS256、RS256 等)
  • typ:Token 类型(固定为 JWT)

2. Payload(负载)

{
  "user_id": 123,
  "username": "alice",
  "exp": 1516239022
}

包含声明(claims):

  • 注册声明:iss(签发者)、exp(过期时间)、sub(主题)等
  • 公共声明:自定义的声明
  • 私有声明:双方约定的声明

3. Signature(签名)

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret
)

用于验证消息的完整性和真实性。

Session vs JWT

Session 认证

客户端 → 登录 → 服务器(创建 session,返回 session_id)
客户端 ← Set-Cookie: session_id=abc123 ← 服务器
客户端 → 请求(携带 Cookie)→ 服务器(查询 session)
客户端 ← 响应 ← 服务器

优点:

  • 服务器完全控制 session
  • 可以随时使 session 失效

缺点:

  • 需要服务器存储 session
  • 分布式系统需要共享 session
  • 扩展性受限

JWT 认证

客户端 → 登录 → 服务器(验证凭证,签发 JWT)
客户端 ← JWT ← 服务器
客户端 → 请求(携带 JWT)→ 服务器(验证 JWT,无需查询数据库)
客户端 ← 响应 ← 服务器

优点:

  • 无状态,服务器不需要存储
  • 易于水平扩展
  • 跨域友好
  • 适合微服务架构

缺点:

  • Token 无法主动失效(需要额外机制)
  • Token 可能较大
  • 需要保护好密钥

使用 golang-jwt 库

go get github.com/golang-jwt/jwt/v5

基础实现

生成 JWT

package main

import (
    "fmt"
    "time"
    
    "github.com/golang-jwt/jwt/v5"
)

// Claims 自定义声明
type Claims struct {
    UserID   int    `json:"user_id"`
    Username string `json:"username"`
    jwt.RegisteredClaims
}

var jwtSecret = []byte("your-secret-key") // 生产环境使用环境变量

// GenerateToken 生成 JWT
func GenerateToken(userID int, username string) (string, error) {
    claims := Claims{
        UserID:   userID,
        Username: username,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "myapp",
        },
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtSecret)
}

func main() {
    token, err := GenerateToken(123, "alice")
    if err != nil {
        panic(err)
    }
    fmt.Println("Token:", token)
}

验证 JWT

// ParseToken 解析和验证 JWT
func ParseToken(tokenString string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
        // 验证签名算法
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
        }
        return jwtSecret, nil
    })
    
    if err != nil {
        return nil, err
    }
    
    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }
    
    return nil, fmt.Errorf("invalid token")
}

func main() {
    token := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
    
    claims, err := ParseToken(token)
    if err != nil {
        fmt.Println("Invalid token:", err)
        return
    }
    
    fmt.Printf("User ID: %d, Username: %s\n", claims.UserID, claims.Username)
}

JWT 中间件

package main

import (
    "context"
    "net/http"
    "strings"
)

type contextKey string

const userContextKey contextKey = "user"

// JWTMiddleware JWT 认证中间件
func JWTMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // 从 Authorization 头获取 token
        authHeader := r.Header.Get("Authorization")
        if authHeader == "" {
            http.Error(w, "Missing authorization header", http.StatusUnauthorized)
            return
        }
        
        // 验证 Bearer 格式
        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || parts[0] != "Bearer" {
            http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
            return
        }
        
        tokenString := parts[1]
        
        // 解析 token
        claims, err := ParseToken(tokenString)
        if err != nil {
            http.Error(w, "Invalid token", http.StatusUnauthorized)
            return
        }
        
        // 将用户信息添加到 context
        ctx := context.WithValue(r.Context(), userContextKey, claims)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

// GetUserFromContext 从 context 获取用户信息
func GetUserFromContext(r *http.Request) *Claims {
    if claims, ok := r.Context().Value(userContextKey).(*Claims); ok {
        return claims
    }
    return nil
}

完整的认证系统

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "sync"
    "time"
    
    "github.com/golang-jwt/jwt/v5"
    "golang.org/x/crypto/bcrypt"
)

type User struct {
    ID       int    `json:"id"`
    Username string `json:"username"`
    Password string `json:"-"` // 不序列化密码
}

type LoginRequest struct {
    Username string `json:"username"`
    Password string `json:"password"`
}

type LoginResponse struct {
    Token        string `json:"token"`
    RefreshToken string `json:"refresh_token"`
    ExpiresIn    int    `json:"expires_in"`
}

type UserStore struct {
    mu    sync.RWMutex
    users map[string]*User // username -> user
}

func NewUserStore() *UserStore {
    return &UserStore{
        users: make(map[string]*User),
    }
}

var (
    store      = NewUserStore()
    jwtSecret  = []byte("your-secret-key")
    refreshSecret = []byte("your-refresh-secret-key")
)

// 注册
func registerHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }
    
    var req LoginRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "Invalid request body", http.StatusBadRequest)
        return
    }
    
    // 检查用户名是否已存在
    store.mu.RLock()
    if _, exists := store.users[req.Username]; exists {
        store.mu.RUnlock()
        http.Error(w, "Username already exists", http.StatusConflict)
        return
    }
    store.mu.RUnlock()
    
    // 哈希密码
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
    if err != nil {
        http.Error(w, "Internal server error", http.StatusInternalServerError)
        return
    }
    
    // 创建用户
    user := &User{
        ID:       len(store.users) + 1,
        Username: req.Username,
        Password: string(hashedPassword),
    }
    
    store.mu.Lock()
    store.users[req.Username] = user
    store.mu.Unlock()
    
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(map[string]string{"message": "User registered successfully"})
}

// 登录
func loginHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }
    
    var req LoginRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "Invalid request body", http.StatusBadRequest)
        return
    }
    
    // 查找用户
    store.mu.RLock()
    user, exists := store.users[req.Username]
    store.mu.RUnlock()
    
    if !exists {
        http.Error(w, "Invalid credentials", http.StatusUnauthorized)
        return
    }
    
    // 验证密码
    if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
        http.Error(w, "Invalid credentials", http.StatusUnauthorized)
        return
    }
    
    // 生成 Access Token(短期)
    accessToken, err := generateAccessToken(user)
    if err != nil {
        http.Error(w, "Internal server error", http.StatusInternalServerError)
        return
    }
    
    // 生成 Refresh Token(长期)
    refreshToken, err := generateRefreshToken(user)
    if err != nil {
        http.Error(w, "Internal server error", http.StatusInternalServerError)
        return
    }
    
    response := LoginResponse{
        Token:        accessToken,
        RefreshToken: refreshToken,
        ExpiresIn:    3600, // 1 小时
    }
    
    json.NewEncoder(w).Encode(response)
}

func generateAccessToken(user *User) (string, error) {
    claims := Claims{
        UserID:   user.ID,
        Username: user.Username,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Issuer:    "myapp",
        },
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtSecret)
}

func generateRefreshToken(user *User) (string, error) {
    claims := jwt.RegisteredClaims{
        Subject:   string(rune(user.ID)),
        ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)), // 7 天
        IssuedAt:  jwt.NewNumericDate(time.Now()),
    }
    
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(refreshSecret)
}

// 刷新 Token
func refreshTokenHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }
    
    var req struct {
        RefreshToken string `json:"refresh_token"`
    }
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, "Invalid request body", http.StatusBadRequest)
        return
    }
    
    // 验证 refresh token
    token, err := jwt.ParseWithClaims(req.RefreshToken, &jwt.RegisteredClaims{}, func(token *jwt.Token) (interface{}, error) {
        return refreshSecret, nil
    })
    
    if err != nil {
        http.Error(w, "Invalid refresh token", http.StatusUnauthorized)
        return
    }
    
    claims, ok := token.Claims.(*jwt.RegisteredClaims)
    if !ok || !token.Valid {
        http.Error(w, "Invalid refresh token", http.StatusUnauthorized)
        return
    }
    
    // 查找用户
    store.mu.RLock()
    var user *User
    for _, u := range store.users {
        if string(rune(u.ID)) == claims.Subject {
            user = u
            break
        }
    }
    store.mu.RUnlock()
    
    if user == nil {
        http.Error(w, "User not found", http.StatusNotFound)
        return
    }
    
    // 生成新的 access token
    newAccessToken, err := generateAccessToken(user)
    if err != nil {
        http.Error(w, "Internal server error", http.StatusInternalServerError)
        return
    }
    
    json.NewEncoder(w).Encode(map[string]interface{}{
        "token":      newAccessToken,
        "expires_in": 3600,
    })
}

// 受保护的 API
func profileHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodGet {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }
    
    claims := GetUserFromContext(r)
    if claims == nil {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }
    
    store.mu.RLock()
    user := store.users[claims.Username]
    store.mu.RUnlock()
    
    json.NewEncoder(w).Encode(user)
}

func main() {
    mux := http.NewServeMux()
    
    // 公开路由
    mux.HandleFunc("/register", registerHandler)
    mux.HandleFunc("/login", loginHandler)
    mux.HandleFunc("/refresh", refreshTokenHandler)
    
    // 受保护的路由
    protectedMux := http.NewServeMux()
    protectedMux.HandleFunc("/profile", profileHandler)
    
    mux.Handle("/api/", JWTMiddleware(protectedMux))
    
    log.Println("Server starting on :8080")
    http.ListenAndServe(":8080", mux)
}

安全最佳实践

1. 使用强密钥

// ❌ 弱密钥
var jwtSecret = []byte("secret")

// ✅ 强密钥(至少 32 字节)
var jwtSecret = []byte("your-very-long-and-random-secret-key-at-least-32-bytes")

2. 设置合理的过期时间

// Access Token:短期(15 分钟 - 1 小时)
accessTokenClaims := Claims{
    ExpiresAt: jwt.NewNumericDate(time.Now().Add(15 * time.Minute)),
}

// Refresh Token:长期(7 天 - 30 天)
refreshTokenClaims := Claims{
    ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)),
}

3. 使用 HTTPS

JWT 在传输过程中可能被截获,必须使用 HTTPS:

// 生产环境
http.ListenAndServeTLS(":443", "cert.pem", "key.pem", handler)

4. 不在 Token 中存储敏感信息

// ❌ 不要存储敏感信息
type Claims struct {
    Password string `json:"password"` // 危险!
    CreditCard string `json:"credit_card"` // 危险!
}

// ✅ 只存储必要的标识信息
type Claims struct {
    UserID int `json:"user_id"`
    Role   string `json:"role"`
}

5. 验证签名算法

token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
    // 确保使用预期的算法
    if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
        return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
    }
    return jwtSecret, nil
})

6. 实现 Token 黑名单

对于需要主动使 Token 失效的场景:

type TokenBlacklist struct {
    mu     sync.RWMutex
    tokens map[string]time.Time // token -> expiration
}

var blacklist = &TokenBlacklist{
    tokens: make(map[string]time.Time),
}

// 添加 token 到黑名单
func (b *TokenBlacklist) Add(token string, exp time.Time) {
    b.mu.Lock()
    defer b.mu.Unlock()
    b.tokens[token] = exp
    
    // 清理过期条目
    go b.cleanup()
}

func (b *TokenBlacklist) cleanup() {
    time.Sleep(1 * time.Hour)
    b.mu.Lock()
    defer b.mu.Unlock()
    
    now := time.Now()
    for token, exp := range b.tokens {
        if now.After(exp) {
            delete(b.tokens, token)
        }
    }
}

// 检查 token 是否在黑名单中
func (b *TokenBlacklist) Contains(token string) bool {
    b.mu.RLock()
    defer b.mu.RUnlock()
    _, exists := b.tokens[token]
    return exists
}

// 在中间件中使用
func JWTMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tokenString := extractToken(r)
        
        if blacklist.Contains(tokenString) {
            http.Error(w, "Token revoked", http.StatusUnauthorized)
            return
        }
        
        // 继续验证...
    })
}

总结

JWT 是现代 Web API 认证的主流方案,它提供了无状态、可扩展的认证机制。

关键要点:

  1. 理解 JWT 结构:Header、Payload、Signature
  2. 使用双 Token 机制:短期 Access Token + 长期 Refresh Token
  3. 保护好密钥:使用强密钥,不要硬编码
  4. 设置合理过期时间:Access Token 短期,Refresh Token 长期
  5. 验证签名算法:防止算法混淆攻击
  6. 使用 HTTPS:防止 Token 被截获
  7. 考虑 Token 黑名单:支持主动失效场景

记住:安全是一个持续的过程,要定期审查和更新你的认证系统。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页