Go~JWT登陆授权

4 天前(已编辑)
5

阅读此文章之前,你可能需要首先阅读以下的文章才能更好的理解上下文。

Go~JWT登陆授权

前言

在现代 Web 应用中,JWT(JSON Web Token)已经成为最流行的身份认证方案之一。相比传统的 Session 认证,JWT 具有无状态、可扩展、跨域支持等优势,特别适合微服务架构和前后端分离的应用。

本文将使用 github.com/golang-jwt/jwt/v5 库,从零开始构建一个完整的 JWT 认证系统,包括用户注册、登录、Token 刷新、权限验证等核心功能。

JWT 基础知识

什么是 JWT

JWT 是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。

JWT 的结构

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

Header.Payload.Signature

示例:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
  1. Header(头部):包含令牌类型和签名算法

    {
      "alg": "HS256",
      "typ": "JWT"
    }
    
  2. Payload(负载):包含声明(Claims),即用户信息和元数据

    {
      "user_id": 123,
      "username": "john_doe",
      "role": "admin",
      "exp": 1516239022
    }
    
  3. Signature(签名):用于验证消息在传输过程中没有被篡改

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

JWT 的优势

  • 无状态:服务器不需要存储 Session,易于水平扩展
  • 跨域支持:可以在不同域之间使用
  • 移动友好:适合移动端应用
  • 性能好:减少数据库查询
  • 灵活性:可以携带自定义信息

项目初始化

1. 创建项目结构

jwt-auth-system/
├── main.go
├── config/
│   └── config.go
├── models/
│   └── user.go
├── middleware/
│   └── auth.go
├── handlers/
│   ├── auth.go
│   └── user.go
├── services/
│   └── jwt.go
├── database/
│   └── db.go
└── go.mod

2. 安装依赖

go mod init jwt-auth-system

# 安装最新版 JWT 库
go get -u github.com/golang-jwt/jwt/v5

# 安装 Echo 框架
go get -u github.com/labstack/echo/v4

# 安装数据库驱动
go get -u gorm.io/gorm
go get -u gorm.io/driver/mysql

# 安装密码加密库
go get -u golang.org/x/crypto/bcrypt

# 安装环境变量库
go get -u github.com/joho/godotenv

核心模块实现

1. 配置管理 (config/config.go)

package config

import (
    "os"
    "time"
)

type Config struct {
    // 服务器配置
    ServerPort string
    
    // JWT 配置
    JWTSecret           string
    JWTAccessExpiry     time.Duration
    JWTRefreshExpiry    time.Duration
    
    // 数据库配置
    DBHost     string
    DBPort     string
    DBUser     string
    DBPassword string
    DBName     string
}

func LoadConfig() *Config {
    return &Config{
        ServerPort: getEnv("SERVER_PORT", "8080"),
        
        // JWT 配置
        JWTSecret:        getEnv("JWT_SECRET", "your-secret-key-change-this-in-production"),
        JWTAccessExpiry:  time.Hour * 1,      // Access Token 1小时
        JWTRefreshExpiry: time.Hour * 24 * 7, // Refresh Token 7天
        
        // 数据库配置
        DBHost:     getEnv("DB_HOST", "localhost"),
        DBPort:     getEnv("DB_PORT", "3306"),
        DBUser:     getEnv("DB_USER", "root"),
        DBPassword: getEnv("DB_PASSWORD", "password"),
        DBName:     getEnv("DB_NAME", "jwt_auth"),
    }
}

func getEnv(key, defaultValue string) string {
    if value := os.Getenv(key); value != "" {
        return value
    }
    return defaultValue
}

2. 用户模型 (models/user.go)

package models

import (
    "time"
    "gorm.io/gorm"
)

// User 用户模型
type User struct {
    ID        uint           `gorm:"primarykey" json:"id"`
    Username  string         `gorm:"uniqueIndex;size:50;not null" json:"username"`
    Email     string         `gorm:"uniqueIndex;size:100;not null" json:"email"`
    Password  string         `gorm:"size:255;not null" json:"-"` // 不在 JSON 中显示
    Role      string         `gorm:"size:20;default:user" json:"role"`
    IsActive  bool           `gorm:"default:true" json:"is_active"`
    CreatedAt time.Time      `json:"created_at"`
    UpdatedAt time.Time      `json:"updated_at"`
    DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}

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

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

// TokenResponse Token 响应
type TokenResponse struct {
    AccessToken  string `json:"access_token"`
    RefreshToken string `json:"refresh_token"`
    TokenType    string `json:"token_type"`
    ExpiresIn    int64  `json:"expires_in"`
}

// RefreshTokenRequest 刷新 Token 请求
type RefreshTokenRequest struct {
    RefreshToken string `json:"refresh_token" validate:"required"`
}

3. JWT 服务 (services/jwt.go)

这是核心的 JWT 处理逻辑,使用最新版 JWT 库:

package services

import (
    "errors"
    "fmt"
    "time"

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

// JWTService JWT 服务
type JWTService struct {
    secretKey        string
    accessExpiry     time.Duration
    refreshExpiry    time.Duration
}

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

// RefreshClaims Refresh Token 声明
type RefreshClaims struct {
    UserID uint `json:"user_id"`
    jwt.RegisteredClaims
}

// NewJWTService 创建 JWT 服务实例
func NewJWTService(secretKey string, accessExpiry, refreshExpiry time.Duration) *JWTService {
    return &JWTService{
        secretKey:     secretKey,
        accessExpiry:  accessExpiry,
        refreshExpiry: refreshExpiry,
    }
}

// GenerateAccessToken 生成 Access Token
func (s *JWTService) GenerateAccessToken(userID uint, username, email, role string) (string, error) {
    now := time.Now()
    
    claims := Claims{
        UserID:   userID,
        Username: username,
        Email:    email,
        Role:     role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(now.Add(s.accessExpiry)),
            IssuedAt:  jwt.NewNumericDate(now),
            NotBefore: jwt.NewNumericDate(now),
            Issuer:    "jwt-auth-system",
            Subject:   fmt.Sprintf("%d", userID),
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString([]byte(s.secretKey))
}

// GenerateRefreshToken 生成 Refresh Token
func (s *JWTService) GenerateRefreshToken(userID uint) (string, error) {
    now := time.Now()
    
    claims := RefreshClaims{
        UserID: userID,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(now.Add(s.refreshExpiry)),
            IssuedAt:  jwt.NewNumericDate(now),
            NotBefore: jwt.NewNumericDate(now),
            Issuer:    "jwt-auth-system",
            Subject:   fmt.Sprintf("%d", userID),
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString([]byte(s.secretKey))
}

// ValidateAccessToken 验证 Access Token
func (s *JWTService) ValidateAccessToken(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 []byte(s.secretKey), nil
    })

    if err != nil {
        return nil, err
    }

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

    return nil, errors.New("invalid token")
}

// ValidateRefreshToken 验证 Refresh Token
func (s *JWTService) ValidateRefreshToken(tokenString string) (*RefreshClaims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &RefreshClaims{}, 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 []byte(s.secretKey), nil
    })

    if err != nil {
        return nil, err
    }

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

    return nil, errors.New("invalid refresh token")
}

// GetAccessTokenExpiry 获取 Access Token 过期时间(秒)
func (s *JWTService) GetAccessTokenExpiry() int64 {
    return int64(s.accessExpiry.Seconds())
}

4. 数据库连接 (database/db.go)

package database

import (
    "fmt"
    "log"

    "jwt-auth-system/config"
    "jwt-auth-system/models"
    
    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/logger"
)

var DB *gorm.DB

// InitDB 初始化数据库连接
func InitDB(cfg *config.Config) error {
    dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local",
        cfg.DBUser,
        cfg.DBPassword,
        cfg.DBHost,
        cfg.DBPort,
        cfg.DBName,
    )

    var err error
    DB, err = gorm.Open(mysql.Open(dsn), &gorm.Config{
        Logger: logger.Default.LogMode(logger.Info),
    })
    
    if err != nil {
        return fmt.Errorf("failed to connect database: %w", err)
    }

    // 自动迁移数据库表
    if err := DB.AutoMigrate(&models.User{}); err != nil {
        return fmt.Errorf("failed to migrate database: %w", err)
    }

    log.Println("Database connected and migrated successfully")
    return nil
}

// GetDB 获取数据库实例
func GetDB() *gorm.DB {
    return DB
}

5. 认证中间件 (middleware/auth.go)

package middleware

import (
    "net/http"
    "strings"

    "jwt-auth-system/services"
    
    "github.com/labstack/echo/v4"
)

// JWTMiddleware JWT 认证中间件
func JWTMiddleware(jwtService *services.JWTService) echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            // 从请求头获取 Token
            authHeader := c.Request().Header.Get("Authorization")
            if authHeader == "" {
                return echo.NewHTTPError(http.StatusUnauthorized, "missing authorization header")
            }

            // 检查 Bearer 前缀
            parts := strings.SplitN(authHeader, " ", 2)
            if len(parts) != 2 || parts[0] != "Bearer" {
                return echo.NewHTTPError(http.StatusUnauthorized, "invalid authorization header format")
            }

            tokenString := parts[1]

            // 验证 Token
            claims, err := jwtService.ValidateAccessToken(tokenString)
            if err != nil {
                return echo.NewHTTPError(http.StatusUnauthorized, "invalid or expired token")
            }

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

            return next(c)
        }
    }
}

// RoleMiddleware 角色权限中间件
func RoleMiddleware(allowedRoles ...string) echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            // 获取用户角色
            userRole, ok := c.Get("role").(string)
            if !ok {
                return echo.NewHTTPError(http.StatusUnauthorized, "user role not found")
            }

            // 检查角色权限
            for _, role := range allowedRoles {
                if userRole == role {
                    return next(c)
                }
            }

            return echo.NewHTTPError(http.StatusForbidden, "insufficient permissions")
        }
    }
}

6. 认证处理器 (handlers/auth.go)

package handlers

import (
    "net/http"

    "jwt-auth-system/database"
    "jwt-auth-system/models"
    "jwt-auth-system/services"
    
    "github.com/labstack/echo/v4"
    "golang.org/x/crypto/bcrypt"
)

type AuthHandler struct {
    jwtService *services.JWTService
}

func NewAuthHandler(jwtService *services.JWTService) *AuthHandler {
    return &AuthHandler{
        jwtService: jwtService,
    }
}

// Register 用户注册
func (h *AuthHandler) Register(c echo.Context) error {
    var req models.RegisterRequest
    
    if err := c.Bind(&req); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, "invalid request body")
    }

    // 验证请求参数
    if err := c.Validate(&req); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }

    // 检查用户名是否已存在
    var existingUser models.User
    if err := database.GetDB().Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
        return echo.NewHTTPError(http.StatusConflict, "username already exists")
    }

    // 检查邮箱是否已存在
    if err := database.GetDB().Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
        return echo.NewHTTPError(http.StatusConflict, "email already exists")
    }

    // 加密密码
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
    if err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, "failed to hash password")
    }

    // 创建用户
    user := models.User{
        Username: req.Username,
        Email:    req.Email,
        Password: string(hashedPassword),
        Role:     "user", // 默认角色
        IsActive: true,
    }

    if err := database.GetDB().Create(&user).Error; err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, "failed to create user")
    }

    return c.JSON(http.StatusCreated, map[string]interface{}{
        "message": "user registered successfully",
        "user": map[string]interface{}{
            "id":       user.ID,
            "username": user.Username,
            "email":    user.Email,
            "role":     user.Role,
        },
    })
}

// Login 用户登录
func (h *AuthHandler) Login(c echo.Context) error {
    var req models.LoginRequest
    
    if err := c.Bind(&req); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, "invalid request body")
    }

    if err := c.Validate(&req); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }

    // 查找用户
    var user models.User
    if err := database.GetDB().Where("username = ?", req.Username).First(&user).Error; err != nil {
        return echo.NewHTTPError(http.StatusUnauthorized, "invalid username or password")
    }

    // 检查用户是否激活
    if !user.IsActive {
        return echo.NewHTTPError(http.StatusForbidden, "user account is disabled")
    }

    // 验证密码
    if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
        return echo.NewHTTPError(http.StatusUnauthorized, "invalid username or password")
    }

    // 生成 Access Token
    accessToken, err := h.jwtService.GenerateAccessToken(user.ID, user.Username, user.Email, user.Role)
    if err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate access token")
    }

    // 生成 Refresh Token
    refreshToken, err := h.jwtService.GenerateRefreshToken(user.ID)
    if err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate refresh token")
    }

    // 返回 Token
    response := models.TokenResponse{
        AccessToken:  accessToken,
        RefreshToken: refreshToken,
        TokenType:    "Bearer",
        ExpiresIn:    h.jwtService.GetAccessTokenExpiry(),
    }

    return c.JSON(http.StatusOK, response)
}

// RefreshToken 刷新 Token
func (h *AuthHandler) RefreshToken(c echo.Context) error {
    var req models.RefreshTokenRequest
    
    if err := c.Bind(&req); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, "invalid request body")
    }

    if err := c.Validate(&req); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, err.Error())
    }

    // 验证 Refresh Token
    claims, err := h.jwtService.ValidateRefreshToken(req.RefreshToken)
    if err != nil {
        return echo.NewHTTPError(http.StatusUnauthorized, "invalid or expired refresh token")
    }

    // 查找用户
    var user models.User
    if err := database.GetDB().First(&user, claims.UserID).Error; err != nil {
        return echo.NewHTTPError(http.StatusUnauthorized, "user not found")
    }

    // 检查用户是否激活
    if !user.IsActive {
        return echo.NewHTTPError(http.StatusForbidden, "user account is disabled")
    }

    // 生成新的 Access Token
    accessToken, err := h.jwtService.GenerateAccessToken(user.ID, user.Username, user.Email, user.Role)
    if err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate access token")
    }

    // 生成新的 Refresh Token
    newRefreshToken, err := h.jwtService.GenerateRefreshToken(user.ID)
    if err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, "failed to generate refresh token")
    }

    response := models.TokenResponse{
        AccessToken:  accessToken,
        RefreshToken: newRefreshToken,
        TokenType:    "Bearer",
        ExpiresIn:    h.jwtService.GetAccessTokenExpiry(),
    }

    return c.JSON(http.StatusOK, response)
}

// Logout 用户登出
func (h *AuthHandler) Logout(c echo.Context) error {
    // 在实际生产环境中,可以将 Token 加入黑名单(使用 Redis 等)
    // 这里简单返回成功消息
    return c.JSON(http.StatusOK, map[string]string{
        "message": "logged out successfully",
    })
}

7. 用户处理器 (handlers/user.go)

package handlers

import (
    "net/http"

    "jwt-auth-system/database"
    "jwt-auth-system/models"
    
    "github.com/labstack/echo/v4"
)

type UserHandler struct{}

func NewUserHandler() *UserHandler {
    return &UserHandler{}
}

// GetProfile 获取当前用户信息
func (h *UserHandler) GetProfile(c echo.Context) error {
    userID := c.Get("user_id").(uint)

    var user models.User
    if err := database.GetDB().First(&user, userID).Error; err != nil {
        return echo.NewHTTPError(http.StatusNotFound, "user not found")
    }

    return c.JSON(http.StatusOK, map[string]interface{}{
        "id":         user.ID,
        "username":   user.Username,
        "email":      user.Email,
        "role":       user.Role,
        "is_active":  user.IsActive,
        "created_at": user.CreatedAt,
    })
}

// GetAllUsers 获取所有用户(仅管理员)
func (h *UserHandler) GetAllUsers(c echo.Context) error {
    var users []models.User
    
    if err := database.GetDB().Find(&users).Error; err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, "failed to fetch users")
    }

    var userList []map[string]interface{}
    for _, user := range users {
        userList = append(userList, map[string]interface{}{
            "id":         user.ID,
            "username":   user.Username,
            "email":      user.Email,
            "role":       user.Role,
            "is_active":  user.IsActive,
            "created_at": user.CreatedAt,
        })
    }

    return c.JSON(http.StatusOK, map[string]interface{}{
        "users": userList,
        "total": len(userList),
    })
}

// UpdateProfile 更新用户信息
func (h *UserHandler) UpdateProfile(c echo.Context) error {
    userID := c.Get("user_id").(uint)

    var req struct {
        Email string `json:"email" validate:"omitempty,email"`
    }

    if err := c.Bind(&req); err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, "invalid request body")
    }

    var user models.User
    if err := database.GetDB().First(&user, userID).Error; err != nil {
        return echo.NewHTTPError(http.StatusNotFound, "user not found")
    }

    // 更新邮箱
    if req.Email != "" {
        // 检查邮箱是否已被使用
        var existingUser models.User
        if err := database.GetDB().Where("email = ? AND id != ?", req.Email, userID).First(&existingUser).Error; err == nil {
            return echo.NewHTTPError(http.StatusConflict, "email already in use")
        }
        user.Email = req.Email
    }

    if err := database.GetDB().Save(&user).Error; err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, "failed to update user")
    }

    return c.JSON(http.StatusOK, map[string]interface{}{
        "message": "profile updated successfully",
        "user": map[string]interface{}{
            "id":       user.ID,
            "username": user.Username,
            "email":    user.Email,
            "role":     user.Role,
        },
    })
}

8. 主程序 (main.go)

package main

import (
    "log"

    "jwt-auth-system/config"
    "jwt-auth-system/database"
    "jwt-auth-system/handlers"
    "jwt-auth-system/middleware"
    "jwt-auth-system/services"
    
    "github.com/go-playground/validator/v10"
    "github.com/labstack/echo/v4"
    echomiddleware "github.com/labstack/echo/v4/middleware"
)

// CustomValidator 自定义验证器
type CustomValidator struct {
    validator *validator.Validate
}

func (cv *CustomValidator) Validate(i interface{}) error {
    return cv.validator.Struct(i)
}

func main() {
    // 加载配置
    cfg := config.LoadConfig()

    // 初始化数据库
    if err := database.InitDB(cfg); err != nil {
        log.Fatalf("Failed to initialize database: %v", err)
    }

    // 创建 JWT 服务
    jwtService := services.NewJWTService(
        cfg.JWTSecret,
        cfg.JWTAccessExpiry,
        cfg.JWTRefreshExpiry,
    )

    // 创建 Echo 实例
    e := echo.New()

    // 注册自定义验证器
    e.Validator = &CustomValidator{validator: validator.New()}

    // 全局中间件
    e.Use(echomiddleware.Logger())
    e.Use(echomiddleware.Recover())
    e.Use(echomiddleware.CORS())

    // 创建处理器
    authHandler := handlers.NewAuthHandler(jwtService)
    userHandler := handlers.NewUserHandler()

    // 公开路由(无需认证)
    public := e.Group("/api/v1")
    {
        public.POST("/register", authHandler.Register)
        public.POST("/login", authHandler.Login)
        public.POST("/refresh", authHandler.RefreshToken)
    }

    // 受保护的路由(需要认证)
    protected := e.Group("/api/v1")
    protected.Use(middleware.JWTMiddleware(jwtService))
    {
        // 用户相关
        protected.GET("/profile", userHandler.GetProfile)
        protected.PUT("/profile", userHandler.UpdateProfile)
        protected.POST("/logout", authHandler.Logout)
        
        // 管理员路由
        admin := protected.Group("/admin")
        admin.Use(middleware.RoleMiddleware("admin"))
        {
            admin.GET("/users", userHandler.GetAllUsers)
        }
    }

    // 健康检查
    e.GET("/health", func(c echo.Context) error {
        return c.JSON(200, map[string]string{
            "status": "ok",
        })
    })

    // 启动服务器
    log.Printf("Server starting on port %s", cfg.ServerPort)
    if err := e.Start(":" + cfg.ServerPort); err != nil {
        log.Fatalf("Failed to start server: %v", err)
    }
}

9. 环境变量配置 (.env)

# 服务器配置
SERVER_PORT=8080

# JWT 配置
JWT_SECRET=your-super-secret-key-change-this-in-production-min-32-chars

# 数据库配置
DB_HOST=localhost
DB_PORT=3306
DB_USER=root
DB_PASSWORD=your_password
DB_NAME=jwt_auth

完整的 API 测试

1. 用户注册

curl -X POST http://localhost:8080/api/v1/register \
  -H "Content-Type: application/json" \
  -d '{
    "username": "john_doe",
    "email": "john@example.com",
    "password": "SecurePass123!"
  }'

响应:

{
  "message": "user registered successfully",
  "user": {
    "id": 1,
    "username": "john_doe",
    "email": "john@example.com",
    "role": "user"
  }
}

2. 用户登录

curl -X POST http://localhost:8080/api/v1/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "john_doe",
    "password": "SecurePass123!"
  }'

响应:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600
}

3. 获取用户信息

curl -X GET http://localhost:8080/api/v1/profile \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

响应:

{
  "id": 1,
  "username": "john_doe",
  "email": "john@example.com",
  "role": "user",
  "is_active": true,
  "created_at": "2024-01-15T10:30:00Z"
}

4. 刷新 Token

curl -X POST http://localhost:8080/api/v1/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "YOUR_REFRESH_TOKEN"
  }'

响应:

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "Bearer",
  "expires_in": 3600
}

5. 更新用户信息

curl -X PUT http://localhost:8080/api/v1/profile \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "newemail@example.com"
  }'

6. 管理员获取所有用户

curl -X GET http://localhost:8080/api/v1/admin/users \
  -H "Authorization: Bearer ADMIN_ACCESS_TOKEN"

响应:

{
  "users": [
    {
      "id": 1,
      "username": "john_doe",
      "email": "john@example.com",
      "role": "user",
      "is_active": true,
      "created_at": "2024-01-15T10:30:00Z"
    },
    {
      "id": 2,
      "username": "admin",
      "email": "admin@example.com",
      "role": "admin",
      "is_active": true,
      "created_at": "2024-01-15T09:00:00Z"
    }
  ],
  "total": 2
}

7. 用户登出

curl -X POST http://localhost:8080/api/v1/logout \
  -H "Authorization: Bearer YOUR_ACCESS_TOKEN"

安全最佳实践

1. 密钥管理

// ❌ 不要硬编码密钥
const SECRET_KEY = "my-secret-key"

// ✅ 从环境变量读取
secretKey := os.Getenv("JWT_SECRET")
if len(secretKey) < 32 {
    log.Fatal("JWT_SECRET must be at least 32 characters")
}

2. Token 过期时间设置

// 建议的过期时间设置
const (
    AccessTokenExpiry  = 15 * time.Minute  // 短期访问令牌
    RefreshTokenExpiry = 7 * 24 * time.Hour // 长期刷新令牌
)

3. HTTPS 传输

// 生产环境必须使用 HTTPS
func main() {
    e := echo.New()
    
    // 配置路由...
    
    // 使用 TLS
    e.Logger.Fatal(e.StartTLS(":443", "cert.pem", "key.pem"))
}

4. Token 黑名单实现

使用 Redis 实现 Token 黑名单:

package services

import (
    "context"
    "time"
    
    "github.com/redis/go-redis/v9"
)

type TokenBlacklist struct {
    client *redis.Client
}

func NewTokenBlacklist(client *redis.Client) *TokenBlacklist {
    return &TokenBlacklist{client: client}
}

// AddToBlacklist 将 Token 加入黑名单
func (tb *TokenBlacklist) AddToBlacklist(token string, expiry time.Duration) error {
    ctx := context.Background()
    return tb.client.Set(ctx, "blacklist:"+token, "1", expiry).Err()
}

// IsBlacklisted 检查 Token 是否在黑名单中
func (tb *TokenBlacklist) IsBlacklisted(token string) (bool, error) {
    ctx := context.Background()
    result, err := tb.client.Get(ctx, "blacklist:"+token).Result()
    
    if err == redis.Nil {
        return false, nil
    }
    
    if err != nil {
        return false, err
    }
    
    return result == "1", nil
}

在中间件中使用:

func JWTMiddleware(jwtService *services.JWTService, blacklist *services.TokenBlacklist) echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            tokenString := extractToken(c)
            
            // 检查是否在黑名单中
            if blacklisted, err := blacklist.IsBlacklisted(tokenString); err == nil && blacklisted {
                return echo.NewHTTPError(http.StatusUnauthorized, "token has been revoked")
            }
            
            // 验证 Token
            claims, err := jwtService.ValidateAccessToken(tokenString)
            if err != nil {
                return echo.NewHTTPError(http.StatusUnauthorized, "invalid token")
            }
            
            c.Set("user_id", claims.UserID)
            return next(c)
        }
    }
}

5. 密码强度验证

package validators

import (
    "errors"
    "regexp"
)

// ValidatePasswordStrength 验证密码强度
func ValidatePasswordStrength(password string) error {
    if len(password) < 8 {
        return errors.New("password must be at least 8 characters")
    }
    
    // 至少包含一个大写字母
    if matched, _ := regexp.MatchString(`[A-Z]`, password); !matched {
        return errors.New("password must contain at least one uppercase letter")
    }
    
    // 至少包含一个小写字母
    if matched, _ := regexp.MatchString(`[a-z]`, password); !matched {
        return errors.New("password must contain at least one lowercase letter")
    }
    
    // 至少包含一个数字
    if matched, _ := regexp.MatchString(`[0-9]`, password); !matched {
        return errors.New("password must contain at least one digit")
    }
    
    // 至少包含一个特殊字符
    if matched, _ := regexp.MatchString(`[!@#$%^&*(),.?":{}|<>]`, password); !matched {
        return errors.New("password must contain at least one special character")
    }
    
    return nil
}

6. 速率限制

使用中间件限制登录尝试次数:

package middleware

import (
    "net/http"
    "sync"
    "time"
    
    "github.com/labstack/echo/v4"
)

type RateLimiter struct {
    attempts map[string]*AttemptInfo
    mu       sync.RWMutex
}

type AttemptInfo struct {
    Count     int
    ResetTime time.Time
}

func NewRateLimiter() *RateLimiter {
    rl := &RateLimiter{
        attempts: make(map[string]*AttemptInfo),
    }
    
    // 定期清理过期记录
    go rl.cleanup()
    
    return rl
}

func (rl *RateLimiter) cleanup() {
    ticker := time.NewTicker(time.Hour)
    for range ticker.C {
        rl.mu.Lock()
        now := time.Now()
        for key, info := range rl.attempts {
            if now.After(info.ResetTime) {
                delete(rl.attempts, key)
            }
        }
        rl.mu.Unlock()
    }
}

func (rl *RateLimiter) LoginRateLimitMiddleware(maxAttempts int, window time.Duration) echo.MiddlewareFunc {
    return func(next echo.HandlerFunc) echo.HandlerFunc {
        return func(c echo.Context) error {
            // 获取客户端 IP
            ip := c.RealIP()
            
            rl.mu.Lock()
            info, exists := rl.attempts[ip]
            
            if !exists || time.Now().After(info.ResetTime) {
                rl.attempts[ip] = &AttemptInfo{
                    Count:     1,
                    ResetTime: time.Now().Add(window),
                }
                rl.mu.Unlock()
                return next(c)
            }
            
            if info.Count >= maxAttempts {
                rl.mu.Unlock()
                return echo.NewHTTPError(
                    http.StatusTooManyRequests,
                    "too many login attempts, please try again later",
                )
            }
            
            info.Count++
            rl.mu.Unlock()
            
            return next(c)
        }
    }
}

在路由中使用:

rateLimiter := middleware.NewRateLimiter()

public := e.Group("/api/v1")
{
    // 登录接口限制:5次/15分钟
    public.POST("/login", authHandler.Login, 
        rateLimiter.LoginRateLimitMiddleware(5, 15*time.Minute))
    
    public.POST("/register", authHandler.Register)
    public.POST("/refresh", authHandler.RefreshToken)
}

高级功能扩展

1. 多设备登录管理

package models

type UserSession struct {
    ID           uint      `gorm:"primarykey"`
    UserID       uint      `gorm:"index"`
    RefreshToken string    `gorm:"size:500;uniqueIndex"`
    DeviceInfo   string    `gorm:"size:255"`
    IPAddress    string    `gorm:"size:45"`
    UserAgent    string    `gorm:"size:500"`
    ExpiresAt    time.Time
    CreatedAt    time.Time
}

// 在登录时保存 Session
func (h *AuthHandler) Login(c echo.Context) error {
    // ... 验证用户 ...
    
    // 生成 Token
    accessToken, _ := h.jwtService.GenerateAccessToken(...)
    refreshToken, _ := h.jwtService.GenerateRefreshToken(...)
    
    // 保存 Session
    session := models.UserSession{
        UserID:       user.ID,
        RefreshToken: refreshToken,
        DeviceInfo:   c.Request().Header.Get("Device-Info"),
        IPAddress:    c.RealIP(),
        UserAgent:    c.Request().UserAgent(),
        ExpiresAt:    time.Now().Add(7 * 24 * time.Hour),
    }
    database.GetDB().Create(&session)
    
    // 返回 Token
    return c.JSON(http.StatusOK, response)
}

2. 邮箱验证功能

package models

type EmailVerification struct {
    ID        uint      `gorm:"primarykey"`
    UserID    uint      `gorm:"index"`
    Token     string    `gorm:"size:64;uniqueIndex"`
    ExpiresAt time.Time
    CreatedAt time.Time
}

// 生成验证 Token
func GenerateVerificationToken(userID uint) (string, error) {
    token := make([]byte, 32)
    if _, err := rand.Read(token); err != nil {
        return "", err
    }
    
    tokenStr := hex.EncodeToString(token)
    
    verification := EmailVerification{
        UserID:    userID,
        Token:     tokenStr,
        ExpiresAt: time.Now().Add(24 * time.Hour),
    }
    
    if err := database.GetDB().Create(&verification).Error; err != nil {
        return "", err
    }
    
    return tokenStr, nil
}

// 验证邮箱
func (h *AuthHandler) VerifyEmail(c echo.Context) error {
    token := c.QueryParam("token")
    
    var verification EmailVerification
    if err := database.GetDB().Where("token = ? AND expires_at > ?", 
        token, time.Now()).First(&verification).Error; err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, "invalid or expired token")
    }
    
    // 更新用户状态
    database.GetDB().Model(&models.User{}).
        Where("id = ?", verification.UserID).
        Update("email_verified", true)
    
    // 删除验证记录
    database.GetDB().Delete(&verification)
    
    return c.JSON(http.StatusOK, map[string]string{
        "message": "email verified successfully",
    })
}

3. 双因素认证(2FA)

package services

import (
    "github.com/pquerna/otp/totp"
)

// Generate2FASecret 生成 2FA 密钥
func Generate2FASecret(username string) (string, string, error) {
    key, err := totp.Generate(totp.GenerateOpts{
        Issuer:      "YourApp",
        AccountName: username,
    })
    
    if err != nil {
        return "", "", err
    }
    
    return key.Secret(), key.URL(), nil
}

// Verify2FACode 验证 2FA 代码
func Verify2FACode(secret, code string) bool {
    return totp.Validate(code, secret)
}

// 在用户模型中添加
type User struct {
    // ... 其他字段 ...
    TwoFactorSecret  string `gorm:"size:64" json:"-"`
    TwoFactorEnabled bool   `gorm:"default:false" json:"two_factor_enabled"`
}

4. 社交登录集成

package handlers

import (
    "golang.org/x/oauth2"
    "golang.org/x/oauth2/google"
)

var googleOAuthConfig = &oauth2.Config{
    ClientID:     os.Getenv("GOOGLE_CLIENT_ID"),
    ClientSecret: os.Getenv("GOOGLE_CLIENT_SECRET"),
    RedirectURL:  "http://localhost:8080/api/v1/auth/google/callback",
    Scopes: []string{
        "https://www.googleapis.com/auth/userinfo.email",
        "https://www.googleapis.com/auth/userinfo.profile",
    },
    Endpoint: google.Endpoint,
}

// GoogleLogin 发起 Google 登录
func (h *AuthHandler) GoogleLogin(c echo.Context) error {
    url := googleOAuthConfig.AuthCodeURL("state", oauth2.AccessOnline)
    return c.Redirect(http.StatusTemporaryRedirect, url)
}

// GoogleCallback Google 登录回调
func (h *AuthHandler) GoogleCallback(c echo.Context) error {
    code := c.QueryParam("code")
    
    token, err := googleOAuthConfig.Exchange(context.Background(), code)
    if err != nil {
        return echo.NewHTTPError(http.StatusBadRequest, "failed to exchange token")
    }
    
    // 获取用户信息
    client := googleOAuthConfig.Client(context.Background(), token)
    resp, err := client.Get("https://www.googleapis.com/oauth2/v2/userinfo")
    if err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, "failed to get user info")
    }
    defer resp.Body.Close()
    
    var userInfo struct {
        Email string `json:"email"`
        Name  string `json:"name"`
    }
    
    if err := json.NewDecoder(resp.Body).Decode(&userInfo); err != nil {
        return echo.NewHTTPError(http.StatusInternalServerError, "failed to decode user info")
    }
    
    // 查找或创建用户
    var user models.User
    result := database.GetDB().Where("email = ?", userInfo.Email).First(&user)
    
    if result.Error != nil {
        // 创建新用户
        user = models.User{
            Username: userInfo.Name,
            Email:    userInfo.Email,
            Role:     "user",
            IsActive: true,
        }
        database.GetDB().Create(&user)
    }
    
    // 生成 JWT Token
    accessToken, _ := h.jwtService.GenerateAccessToken(
        user.ID, user.Username, user.Email, user.Role)
    refreshToken, _ := h.jwtService.GenerateRefreshToken(user.ID)
    
    // 返回 Token
    return c.JSON(http.StatusOK, models.TokenResponse{
        AccessToken:  accessToken,
        RefreshToken: refreshToken,
        TokenType:    "Bearer",
        ExpiresIn:    h.jwtService.GetAccessTokenExpiry(),
    })
}

单元测试

测试 JWT 服务

package services

import (
    "testing"
    "time"
    
    "github.com/stretchr/testify/assert"
)

func TestJWTService_GenerateAndValidateAccessToken(t *testing.T) {
    service := NewJWTService("test-secret-key", time.Hour, time.Hour*24)
    
    // 生成 Token
    token, err := service.GenerateAccessToken(1, "testuser", "test@example.com", "user")
    assert.NoError(t, err)
    assert.NotEmpty(t, token)
    
    // 验证 Token
    claims, err := service.ValidateAccessToken(token)
    assert.NoError(t, err)
    assert.Equal(t, uint(1), claims.UserID)
    assert.Equal(t, "testuser", claims.Username)
    assert.Equal(t, "test@example.com", claims.Email)
    assert.Equal(t, "user", claims.Role)
}

func TestJWTService_ValidateExpiredToken(t *testing.T) {
    service := NewJWTService("test-secret-key", time.Millisecond, time.Hour)
    
    token, _ := service.GenerateAccessToken(1, "testuser", "test@example.com", "user")
    
    // 等待 Token 过期
    time.Sleep(time.Millisecond * 10)
    
    _, err := service.ValidateAccessToken(token)
    assert.Error(t, err)
}

测试认证处理器

package handlers

import (
    "encoding/json"
    "net/http"
    "net/http/httptest"
    "strings"
    "testing"
    
    "github.com/labstack/echo/v4"
    "github.com/stretchr/testify/assert"
)

func TestAuthHandler_Register(t *testing.T) {
    // 设置测试数据库
    setupTestDB()
    defer teardownTestDB()
    
    e := echo.New()
    handler := NewAuthHandler(testJWTService)
    
    reqBody := `{
        "username": "testuser",
        "email": "test@example.com",
        "password": "TestPass123!"
    }`
    
    req := httptest.NewRequest(http.MethodPost, "/register", strings.NewReader(reqBody))
    req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
    rec := httptest.NewRecorder()
    c := e.NewContext(req, rec)
    
    err := handler.Register(c)
    
    assert.NoError(t, err)
    assert.Equal(t, http.StatusCreated, rec.Code)
    
    var response map[string]interface{}
    json.Unmarshal(rec.Body.Bytes(), &response)
    assert.Equal(t, "user registered successfully", response["message"])
}

性能优化建议

1. 连接池配置

func InitDB(cfg *config.Config) error {
    // ... 连接数据库 ...
    
    sqlDB, err := DB.DB()
    if err != nil {
        return err
    }
    
    // 设置连接池参数
    sqlDB.SetMaxIdleConns(10)           // 最大空闲连接数
    sqlDB.SetMaxOpenConns(100)          // 最大打开连接数
    sqlDB.SetConnMaxLifetime(time.Hour) // 连接最大生命周期
    
    return nil
}

2. 缓存用户信息

package cache

import (
    "context"
    "encoding/json"
    "time"
    
    "github.com/redis/go-redis/v9"
    "jwt-auth-system/models"
)

type UserCache struct {
    client *redis.Client
}

func NewUserCache(client *redis.Client) *UserCache {
    return &UserCache{client: client}
}

func (uc *UserCache) GetUser(userID uint) (*models.User, error) {
    ctx := context.Background()
    key := fmt.Sprintf("user:%d", userID)
    
    data, err := uc.client.Get(ctx, key).Result()
    if err == nil {
        var user models.User
        json.Unmarshal([]byte(data), &user)
        return &user, nil
    }
    
    // 缓存未命中,从数据库查询
    var user models.User
    if err := database.GetDB().First(&user, userID).Error; err != nil {
        return nil, err
    }
    
    // 存入缓存
    userData, _ := json.Marshal(user)
    uc.client.Set(ctx, key, userData, time.Hour)
    
    return &user, nil
}

3. 并发处理优化

// 使用 sync.Pool 复用对象
var claimsPool = sync.Pool{
    New: func() interface{} {
        return new(Claims)
    },
}

func (s *JWTService) ValidateAccessToken(tokenString string) (*Claims, error) {
    claims := claimsPool.Get().(*Claims)
    defer claimsPool.Put(claims)
    
    // ... 验证逻辑 ...
}

部署建议

1. Docker 部署

# Dockerfile
FROM golang:1.21-alpine AS builder

WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download

COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o main .

FROM alpine:latest
RUN apk --no-cache add ca-certificates

WORKDIR /root/
COPY --from=builder /app/main .
COPY --from=builder /app/.env .

EXPOSE 8080
CMD ["./main"]
# docker-compose.yml
version: '3.8'

services:
  app:
    build: .
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=mysql
      - DB_PORT=3306
      - DB_USER=root
      - DB_PASSWORD=password
      - DB_NAME=jwt_auth
      - JWT_SECRET=${JWT_SECRET}
    depends_on:
      - mysql
      - redis

  mysql:
    image: mysql:8.0
    environment:
      - MYSQL_ROOT_PASSWORD=password
      - MYSQL_DATABASE=jwt_auth
    volumes:
      - mysql_data:/var/lib/mysql
    ports:
      - "3306:3306"

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

volumes:
  mysql_data:
  redis_data:

2. Kubernetes 部署

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: jwt-auth-api
spec:
  replicas: 3
  selector:
    matchLabels:
      app: jwt-auth-api
  template:
    metadata:
      labels:
        app: jwt-auth-api
    spec:
      containers:
      - name: api
        image: your-registry/jwt-auth-api:latest
        ports:
        - containerPort: 8080
        env:
        - name: JWT_SECRET
          valueFrom:
            secretKeyRef:
              name: jwt-secret
              key: secret
        - name: DB_HOST
          value: mysql-service
        resources:
          limits:
            memory: "512Mi"
            cpu: "500m"

总结

通过本文,我们实现了一个功能完整、安全可靠的 JWT 认证系统,包括:

  1. 完整的认证流程:注册、登录、Token 刷新、登出
  2. 安全机制:密码加密、Token 签名验证、HTTPS 传输
  3. 权限管理:基于角色的访问控制(RBAC)
  4. 最新技术栈:golang-jwt/jwt/v5、Echo v4、GORM
  5. 生产就绪:错误处理、日志记录、速率限制
  6. 可扩展性:支持多设备、2FA、社交登录等扩展功能
  7. 性能优化:连接池、缓存、并发处理
  8. 测试覆盖:单元测试、集成测试

参考资源


使用社交账号登录

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...