以下是为 Gin 框架集成 JWT 的完整教程,包含详细的代码注释和中间件实现。我们将创建一个完整的身份验证流程示例:


1. 项目结构

.
├── main.go          # 主入口文件
├── go.mod
├── handlers         # 业务处理
│   └── auth.go
└── middleware       # 中间件
    └── jwt.go

2. 完整代码实现

main.go - 主程序

package main

import (
    "net/http"
    "your-project/handlers"
    "your-project/middleware"

    "github.com/gin-gonic/gin"
)

func main() {
    r := gin.Default()

    // 公开路由:不需要认证
    public := r.Group("/api")
    {
        public.POST("/login", handlers.Login) // 登录获取Token
    }

    // 私有路由:需要JWT认证
    private := r.Group("/api")
    private.Use(middleware.JWTAuthMiddleware()) // 应用JWT中间件
    {
        private.GET("/profile", handlers.Profile) // 获取用户信息
    }

    r.Run(":8080")
}

handlers/auth.go - 业务处理

package handlers

import (
    "net/http"
    "time"

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

// 密钥(生产环境应从配置读取)
var jwtSecret = []byte("your-secret-key-32bytes-long")

// 用户登录请求结构
type LoginRequest struct {
    Username string `json:"username"`
    Password string `json:"password"`
}

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

func Login(c *gin.Context) {
    // 1. 绑定请求数据
    var req LoginRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "无效请求"})
        return
    }

    // 2. 模拟用户验证(实际应查数据库)
    if req.Username != "admin" || req.Password != "password" {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
        return
    }

    // 3. 生成JWT
    claims := CustomClaims{
        UserID:   1,
        Username: "admin",
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(time.Now().Add(1 * time.Hour)), // 过期时间
            Issuer:    "my-gin-app",                                      // 签发者
        },
    }

    // 4. 创建Token
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    
    // 5. 生成签名字符串
    tokenString, err := token.SignedString(jwtSecret)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "生成Token失败"})
        return
    }

    // 6. 返回Token
    c.JSON(http.StatusOK, gin.H{
        "token": tokenString,
    })
}

func Profile(c *gin.Context) {
    // 从上下文中获取Claims
    claims, exists := c.Get("claims")
    if !exists {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "认证信息无效"})
        return
    }

    // 类型断言获取具体Claims
    customClaims, ok := claims.(*CustomClaims)
    if !ok {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "解析认证信息失败"})
        return
    }

    c.JSON(http.StatusOK, gin.H{
        "user_id":  customClaims.UserID,
        "username": customClaims.Username,
    })
}

middleware/jwt.go - JWT中间件

package middleware

import (
    "net/http"
    "strings"

    "github.com/gin-gonic/gin"
    "github.com/golang-jwt/jwt/v5"
    "your-project/handlers"
)

func JWTAuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 1. 从Header获取Token
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "缺少认证Token"})
            return
        }

        // 2. 检查Token格式(Bearer Token)
        parts := strings.SplitN(authHeader, " ", 2)
        if !(len(parts) == 2 && parts[0] == "Bearer") {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Token格式错误"})
            return
        }

        // 3. 解析Token
        tokenString := parts[1]
        claims := &handlers.CustomClaims{} // 使用自定义Claims类型

        token, err := jwt.ParseWithClaims(
            tokenString,
            claims,
            func(token *jwt.Token) (interface{}, error) {
                // 验证签名方法
                if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
                    return nil, jwt.ErrSignatureInvalid
                }
                return handlers.JwtSecret, nil
            },
            jwt.WithIssuer("my-gin-app"), // 验证签发者
        )

        // 4. 处理解析错误
        if err != nil {
            handleJWTError(c, err)
            return
        }

        // 5. 验证Token是否有效
        if !token.Valid {
            c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "无效Token"})
            return
        }

        // 6. 将Claims存入上下文
        c.Set("claims", claims)
        c.Next()
    }
}

// 处理JWT错误
func handleJWTError(c *gin.Context, err error) {
    switch {
    case errors.Is(err, jwt.ErrTokenMalformed):
        c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "非法Token格式"})
    case errors.Is(err, jwt.ErrTokenExpired):
        c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Token已过期"})
    case errors.Is(err, jwt.ErrTokenSignatureInvalid):
        c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "签名验证失败"})
    default:
        c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "认证失败: " + err.Error()})
    }
}

3. 使用教程

步骤1:获取Token

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

# 返回示例
{
    "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6ImFkbWluIiwiaXNzIjoibXktZ2luLWFwcCIsImV4cCI6MTY5MzI2MDAwMH0.6Q9k_2XnRU0JzrqC7Z4Xqo4T7d8tVv6l1y3w0qWY3M4"
}

步骤2:访问受保护路由

curl http://localhost:8080/api/profile \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."

# 成功返回示例
{
    "user_id": 1,
    "username": "admin"
}

# 错误返回示例
{
    "error": "Token已过期"
}

4. 核心机制解析

中间件工作流程

  1. 请求拦截:检查请求头中的 Authorization
  2. 格式验证:必须为 Bearer <token> 格式
  3. 签名验证:使用相同密钥验证签名有效性
  4. Claims解析:将解码后的用户信息存入上下文
  5. 错误处理:统一处理各种JWT验证错误

关键设计点

  1. 上下文传递:通过 c.Set("claims", claims) 传递用户信息
  2. 错误处理封装handleJWTError 统一处理各类JWT错误
  3. 安全验证

    • 强制验证签名方法 (SigningMethodHMAC)
    • 验证签发者 (WithIssuer)
    • 自动验证过期时间

5. 最佳实践建议

  1. 密钥管理

    // 从环境变量获取(推荐)
    jwtSecret = []byte(os.Getenv("JWT_SECRET"))
  2. 刷新令牌机制

    • 当access token过期时,使用refresh token获取新token
    • Refresh token应有更长有效期,存储于数据库
  3. 敏感操作保护

    // 在关键操作前验证用户状态
    func TransferMoney(c *gin.Context) {
        claims := c.MustGet("claims").(*CustomClaims)
        if claims.UserStatus != "active" {
            c.AbortWithStatusJSON(403, gin.H{"error": "账户已被冻结"})
        }
        // ...业务逻辑
    }
  4. 日志记录

    // 在中间件中添加日志
    log.Printf("JWT验证失败: %s | IP: %s", err.Error(), c.ClientIP())

6. 常见问题解答

Q1: 如何设置不同用户角色的权限?

// 在Claims中添加角色字段
type CustomClaims struct {
    Role string `json:"role"`
    // ...其他字段
}

// 创建角色验证中间件
func RequireRole(role string) gin.HandlerFunc {
    return func(c *gin.Context) {
        claims := c.MustGet("claims").(*CustomClaims)
        if claims.Role != role {
            c.AbortWithStatusJSON(403, gin.H{"error": "权限不足"})
        }
        c.Next()
    }
}

// 使用示例
private.GET("/admin", RequireRole("admin"), AdminHandler)

Q2: Token应该存储在客户端哪里?

  • Web应用:推荐使用 HttpOnly Cookie
  • 移动端:Secure Storage + 内存临时存储
  • 避免使用 localStorage(易受XSS攻击)

Q3: 如何实现Token强制失效?

  • 维护一个失效Token的黑名单(Redis)
  • 在中间件中校验Token是否在黑名单中

    func CheckTokenRevoked(tokenString string) bool {
      // 查询Redis或数据库
      return isRevoked
    }
    
    // 在中间件中添加检查
    if CheckTokenRevoked(tokenString) {
      c.AbortWithStatusJSON(401, gin.H{"error": "Token已失效"})
      return
    }
每日更新-免费小火箭账号
不要错过任何机会,探索最新的应用和游戏,就在我们的平台。
立即访问
最后修改:2025 年 03 月 30 日
如果觉得我的文章对你有用,请随意赞赏