3.9.1 JWT 认证机制

3.9.1 JWT 认证机制 #

JSON Web Token(JWT)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息。JWT 是无状态的,这使得它特别适合分布式系统和微服务架构。

JWT 基础概念 #

JWT 结构 #

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

Header.Payload.Signature

Header(头部):包含令牌类型和签名算法

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload(载荷):包含声明(claims)

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622
}

Signature(签名):用于验证令牌的完整性

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

标准声明 #

JWT 定义了一些标准声明:

  • iss(Issuer):令牌发行者
  • sub(Subject):令牌主题
  • aud(Audience):令牌受众
  • exp(Expiration Time):过期时间
  • nbf(Not Before):生效时间
  • iat(Issued At):发行时间
  • jti(JWT ID):令牌唯一标识

Go 中的 JWT 实现 #

安装依赖 #

go mod init jwt-demo
go get github.com/golang-jwt/jwt/v5
go get github.com/gin-gonic/gin

JWT 工具包 #

首先创建一个 JWT 工具包:

// pkg/jwt/jwt.go
package jwt

import (
    "errors"
    "time"

    "github.com/golang-jwt/jwt/v5"
)

var (
    ErrTokenExpired     = errors.New("token已过期")
    ErrTokenNotValidYet = errors.New("token尚未生效")
    ErrTokenMalformed   = errors.New("token格式错误")
    ErrTokenInvalid     = errors.New("token无效")
)

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

// JWTManager JWT管理器
type JWTManager struct {
    secretKey     []byte
    tokenDuration time.Duration
}

// NewJWTManager 创建JWT管理器
func NewJWTManager(secretKey string, tokenDuration time.Duration) *JWTManager {
    return &JWTManager{
        secretKey:     []byte(secretKey),
        tokenDuration: tokenDuration,
    }
}

// GenerateToken 生成JWT令牌
func (manager *JWTManager) GenerateToken(userID uint, username, role string) (string, error) {
    now := time.Now()
    claims := Claims{
        UserID:   userID,
        Username: username,
        Role:     role,
        RegisteredClaims: jwt.RegisteredClaims{
            Issuer:    "jwt-demo",
            Subject:   username,
            Audience:  []string{"web"},
            ExpiresAt: jwt.NewNumericDate(now.Add(manager.tokenDuration)),
            NotBefore: jwt.NewNumericDate(now),
            IssuedAt:  jwt.NewNumericDate(now),
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(manager.secretKey)
}

// VerifyToken 验证JWT令牌
func (manager *JWTManager) VerifyToken(tokenString string) (*Claims, error) {
    token, err := jwt.ParseWithClaims(
        tokenString,
        &Claims{},
        func(token *jwt.Token) (interface{}, error) {
            return manager.secretKey, nil
        },
    )

    if err != nil {
        if ve, ok := err.(*jwt.ValidationError); ok {
            switch {
            case ve.Errors&jwt.ValidationErrorMalformed != 0:
                return nil, ErrTokenMalformed
            case ve.Errors&jwt.ValidationErrorExpired != 0:
                return nil, ErrTokenExpired
            case ve.Errors&jwt.ValidationErrorNotValidYet != 0:
                return nil, ErrTokenNotValidYet
            default:
                return nil, ErrTokenInvalid
            }
        }
        return nil, err
    }

    if claims, ok := token.Claims.(*Claims); ok && token.Valid {
        return claims, nil
    }

    return nil, ErrTokenInvalid
}

// RefreshToken 刷新令牌
func (manager *JWTManager) RefreshToken(tokenString string) (string, error) {
    claims, err := manager.VerifyToken(tokenString)
    if err != nil {
        return "", err
    }

    // 检查令牌是否即将过期(在过期前30分钟内可以刷新)
    if time.Until(claims.ExpiresAt.Time) > 30*time.Minute {
        return "", errors.New("令牌尚未到刷新时间")
    }

    return manager.GenerateToken(claims.UserID, claims.Username, claims.Role)
}

用户模型和服务 #

// models/user.go
package models

import (
    "golang.org/x/crypto/bcrypt"
    "gorm.io/gorm"
)

type User struct {
    ID       uint   `json:"id" gorm:"primaryKey"`
    Username string `json:"username" gorm:"unique;not null"`
    Email    string `json:"email" gorm:"unique;not null"`
    Password string `json:"-" gorm:"not null"`
    Role     string `json:"role" gorm:"default:user"`
    IsActive bool   `json:"is_active" gorm:"default:true"`
    gorm.Model
}

// HashPassword 加密密码
func (u *User) HashPassword() error {
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
    if err != nil {
        return err
    }
    u.Password = string(hashedPassword)
    return nil
}

// CheckPassword 验证密码
func (u *User) CheckPassword(password string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
    return err == nil
}

// UserService 用户服务
type UserService struct {
    db *gorm.DB
}

func NewUserService(db *gorm.DB) *UserService {
    return &UserService{db: db}
}

// CreateUser 创建用户
func (s *UserService) CreateUser(user *User) error {
    if err := user.HashPassword(); err != nil {
        return err
    }
    return s.db.Create(user).Error
}

// GetUserByUsername 根据用户名获取用户
func (s *UserService) GetUserByUsername(username string) (*User, error) {
    var user User
    err := s.db.Where("username = ? AND is_active = ?", username, true).First(&user).Error
    return &user, err
}

// GetUserByID 根据ID获取用户
func (s *UserService) GetUserByID(id uint) (*User, error) {
    var user User
    err := s.db.Where("id = ? AND is_active = ?", id, true).First(&user).Error
    return &user, err
}

认证中间件 #

// middleware/auth.go
package middleware

import (
    "net/http"
    "strings"

    "github.com/gin-gonic/gin"
    "your-project/pkg/jwt"
)

// AuthMiddleware JWT认证中间件
func AuthMiddleware(jwtManager *jwt.JWTManager) gin.HandlerFunc {
    return func(c *gin.Context) {
        // 从Header中获取Authorization
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "缺少Authorization头",
            })
            c.Abort()
            return
        }

        // 检查Bearer前缀
        parts := strings.SplitN(authHeader, " ", 2)
        if len(parts) != 2 || parts[0] != "Bearer" {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": "Authorization头格式错误",
            })
            c.Abort()
            return
        }

        // 验证令牌
        claims, err := jwtManager.VerifyToken(parts[1])
        if err != nil {
            c.JSON(http.StatusUnauthorized, gin.H{
                "error": err.Error(),
            })
            c.Abort()
            return
        }

        // 将用户信息存储到上下文
        c.Set("user_id", claims.UserID)
        c.Set("username", claims.Username)
        c.Set("role", claims.Role)
        c.Set("claims", claims)

        c.Next()
    }
}

// RequireRole 角色验证中间件
func RequireRole(roles ...string) gin.HandlerFunc {
    return func(c *gin.Context) {
        userRole, exists := c.Get("role")
        if !exists {
            c.JSON(http.StatusForbidden, gin.H{
                "error": "无法获取用户角色",
            })
            c.Abort()
            return
        }

        role := userRole.(string)
        for _, requiredRole := range roles {
            if role == requiredRole {
                c.Next()
                return
            }
        }

        c.JSON(http.StatusForbidden, gin.H{
            "error": "权限不足",
        })
        c.Abort()
    }
}

认证控制器 #

// controllers/auth.go
package controllers

import (
    "net/http"
    "time"

    "github.com/gin-gonic/gin"
    "your-project/models"
    "your-project/pkg/jwt"
)

type AuthController struct {
    userService *models.UserService
    jwtManager  *jwt.JWTManager
}

func NewAuthController(userService *models.UserService, jwtManager *jwt.JWTManager) *AuthController {
    return &AuthController{
        userService: userService,
        jwtManager:  jwtManager,
    }
}

// LoginRequest 登录请求
type LoginRequest struct {
    Username string `json:"username" binding:"required"`
    Password string `json:"password" binding:"required"`
}

// RegisterRequest 注册请求
type RegisterRequest struct {
    Username string `json:"username" binding:"required,min=3,max=20"`
    Email    string `json:"email" binding:"required,email"`
    Password string `json:"password" binding:"required,min=6"`
}

// LoginResponse 登录响应
type LoginResponse struct {
    Token     string      `json:"token"`
    ExpiresAt time.Time   `json:"expires_at"`
    User      *models.User `json:"user"`
}

// Register 用户注册
func (ctrl *AuthController) Register(c *gin.Context) {
    var req RegisterRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }

    user := &models.User{
        Username: req.Username,
        Email:    req.Email,
        Password: req.Password,
        Role:     "user",
    }

    if err := ctrl.userService.CreateUser(user); err != nil {
        c.JSON(http.StatusConflict, gin.H{
            "error": "用户名或邮箱已存在",
        })
        return
    }

    c.JSON(http.StatusCreated, gin.H{
        "message": "注册成功",
        "user":    user,
    })
}

// Login 用户登录
func (ctrl *AuthController) Login(c *gin.Context) {
    var req LoginRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": err.Error(),
        })
        return
    }

    // 验证用户
    user, err := ctrl.userService.GetUserByUsername(req.Username)
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{
            "error": "用户名或密码错误",
        })
        return
    }

    if !user.CheckPassword(req.Password) {
        c.JSON(http.StatusUnauthorized, gin.H{
            "error": "用户名或密码错误",
        })
        return
    }

    // 生成JWT令牌
    token, err := ctrl.jwtManager.GenerateToken(user.ID, user.Username, user.Role)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{
            "error": "生成令牌失败",
        })
        return
    }

    c.JSON(http.StatusOK, LoginResponse{
        Token:     token,
        ExpiresAt: time.Now().Add(24 * time.Hour),
        User:      user,
    })
}

// RefreshToken 刷新令牌
func (ctrl *AuthController) RefreshToken(c *gin.Context) {
    authHeader := c.GetHeader("Authorization")
    if authHeader == "" {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "缺少Authorization头",
        })
        return
    }

    parts := strings.SplitN(authHeader, " ", 2)
    if len(parts) != 2 || parts[0] != "Bearer" {
        c.JSON(http.StatusBadRequest, gin.H{
            "error": "Authorization头格式错误",
        })
        return
    }

    newToken, err := ctrl.jwtManager.RefreshToken(parts[1])
    if err != nil {
        c.JSON(http.StatusUnauthorized, gin.H{
            "error": err.Error(),
        })
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "token":      newToken,
        "expires_at": time.Now().Add(24 * time.Hour),
    })
}

// GetProfile 获取用户信息
func (ctrl *AuthController) GetProfile(c *gin.Context) {
    userID, _ := c.Get("user_id")

    user, err := ctrl.userService.GetUserByID(userID.(uint))
    if err != nil {
        c.JSON(http.StatusNotFound, gin.H{
            "error": "用户不存在",
        })
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "user": user,
    })
}

主程序 #

// main.go
package main

import (
    "log"
    "time"

    "github.com/gin-gonic/gin"
    "gorm.io/driver/sqlite"
    "gorm.io/gorm"

    "your-project/controllers"
    "your-project/middleware"
    "your-project/models"
    "your-project/pkg/jwt"
)

func main() {
    // 初始化数据库
    db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
    if err != nil {
        log.Fatal("数据库连接失败:", err)
    }

    // 自动迁移
    db.AutoMigrate(&models.User{})

    // 初始化服务
    userService := models.NewUserService(db)
    jwtManager := jwt.NewJWTManager("your-secret-key", 24*time.Hour)

    // 初始化控制器
    authController := controllers.NewAuthController(userService, jwtManager)

    // 初始化路由
    r := gin.Default()

    // 公开路由
    public := r.Group("/api/v1")
    {
        public.POST("/register", authController.Register)
        public.POST("/login", authController.Login)
        public.POST("/refresh", authController.RefreshToken)
    }

    // 需要认证的路由
    protected := r.Group("/api/v1")
    protected.Use(middleware.AuthMiddleware(jwtManager))
    {
        protected.GET("/profile", authController.GetProfile)

        // 需要管理员权限的路由
        admin := protected.Group("/admin")
        admin.Use(middleware.RequireRole("admin"))
        {
            admin.GET("/users", func(c *gin.Context) {
                c.JSON(200, gin.H{"message": "管理员专用接口"})
            })
        }
    }

    log.Println("服务器启动在 :8080")
    r.Run(":8080")
}

JWT 最佳实践 #

1. 安全考虑 #

// 使用强密钥
const SecretKey = "your-very-long-and-random-secret-key-here"

// 设置合理的过期时间
const TokenDuration = 15 * time.Minute  // 访问令牌15分钟
const RefreshDuration = 7 * 24 * time.Hour  // 刷新令牌7天

2. 令牌黑名单 #

// pkg/jwt/blacklist.go
package jwt

import (
    "sync"
    "time"
)

// TokenBlacklist 令牌黑名单
type TokenBlacklist struct {
    tokens map[string]time.Time
    mutex  sync.RWMutex
}

func NewTokenBlacklist() *TokenBlacklist {
    bl := &TokenBlacklist{
        tokens: make(map[string]time.Time),
    }

    // 定期清理过期令牌
    go bl.cleanup()
    return bl
}

// Add 添加令牌到黑名单
func (bl *TokenBlacklist) Add(tokenID string, expiry time.Time) {
    bl.mutex.Lock()
    defer bl.mutex.Unlock()
    bl.tokens[tokenID] = expiry
}

// IsBlacklisted 检查令牌是否在黑名单中
func (bl *TokenBlacklist) IsBlacklisted(tokenID string) bool {
    bl.mutex.RLock()
    defer bl.mutex.RUnlock()

    expiry, exists := bl.tokens[tokenID]
    if !exists {
        return false
    }

    // 如果令牌已过期,从黑名单中移除
    if time.Now().After(expiry) {
        delete(bl.tokens, tokenID)
        return false
    }

    return true
}

// cleanup 清理过期令牌
func (bl *TokenBlacklist) cleanup() {
    ticker := time.NewTicker(1 * time.Hour)
    defer ticker.Stop()

    for range ticker.C {
        bl.mutex.Lock()
        now := time.Now()
        for tokenID, expiry := range bl.tokens {
            if now.After(expiry) {
                delete(bl.tokens, tokenID)
            }
        }
        bl.mutex.Unlock()
    }
}

3. 双令牌机制 #

// TokenPair 令牌对
type TokenPair struct {
    AccessToken  string    `json:"access_token"`
    RefreshToken string    `json:"refresh_token"`
    ExpiresAt    time.Time `json:"expires_at"`
}

// GenerateTokenPair 生成令牌对
func (manager *JWTManager) GenerateTokenPair(userID uint, username, role string) (*TokenPair, error) {
    // 生成访问令牌(短期)
    accessToken, err := manager.generateToken(userID, username, role, 15*time.Minute, "access")
    if err != nil {
        return nil, err
    }

    // 生成刷新令牌(长期)
    refreshToken, err := manager.generateToken(userID, username, role, 7*24*time.Hour, "refresh")
    if err != nil {
        return nil, err
    }

    return &TokenPair{
        AccessToken:  accessToken,
        RefreshToken: refreshToken,
        ExpiresAt:    time.Now().Add(15 * time.Minute),
    }, nil
}

测试示例 #

# 注册用户
curl -X POST http://localhost:8080/api/v1/register \
  -H "Content-Type: application/json" \
  -d '{
    "username": "testuser",
    "email": "[email protected]",
    "password": "password123"
  }'

# 用户登录
curl -X POST http://localhost:8080/api/v1/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "testuser",
    "password": "password123"
  }'

# 访问受保护的接口
curl -X GET http://localhost:8080/api/v1/profile \
  -H "Authorization: Bearer YOUR_JWT_TOKEN"

JWT 认证机制为现代 Web 应用提供了一种无状态、可扩展的认证解决方案。通过合理的设计和安全实践,可以构建出既安全又高效的认证系统。