Go Gin JWT 认证授权中间件实战教程

配图:标题:Go Gin JWT 认证授权中间件实战教程;副标题:从零构建可复

环境准备

在开始编码之前,我们需要确保开发环境就绪。本教程基于Go 1.21及以上版本,使用Gin框架作为Web服务器。JWT(JSON Web Token)是一种开放标准,用于在各方之间安全地传输信息 [来源#2]。我们将使用 `golang-jwt/jwt` 库来处理JWT的生成与验证。

# 初始化Go模块
mkdir gin-jwt-tutorial
cd gin-jwt-tutorial
go mod init gin-jwt-tutorial

# 安装Gin和JWT库
go get -u github.com/gin-gonic/gin
go get -u github.com/golang-jwt/jwt/v5

初始化项目并安装依赖

执行上述命令后,你的项目结构应如下所示。这是一个清晰的起点,便于后续代码组织。

gin-jwt-tutorial/
├── go.mod
├── go.sum
└── main.go  # 我们将在此文件中编写核心逻辑

项目初始结构

步骤拆解:实现JWT认证中间件

我们将分步实现一个完整的JWT认证流程。首先,定义JWT的载荷结构,然后创建Token生成函数,接着编写核心的认证中间件,最后将其集成到Gin路由中。

  1. 定义JWT载荷结构:创建一个结构体来存储用户ID和角色,这是JWT的核心数据。
  2. 实现Token生成器:提供一个函数,根据用户信息生成签名的JWT字符串。
  3. 编写认证中间件:这是一个Gin中间件,它从请求头中提取Token,验证其签名和有效期,并将解析出的用户信息存入上下文。
  4. 集成到路由:创建受保护的路由和公开的登录路由,测试中间件是否生效。

核心代码实现

首先,我们在项目根目录下创建 `main.go` 文件。这个文件将包含JWT载荷定义、Token生成函数和主路由设置。为了演示,我们使用一个硬编码的用户和密码。在实际项目中,你应该从数据库或用户服务中获取。

package main

import (
    "fmt"
    "net/http"
    "time"

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

// 定义JWT载荷结构
 type Claims struct {
    UserID uint   `json:"user_id"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

// 密钥,实际项目中应从环境变量或配置文件读取
var jwtKey = []byte("my_secret_key")

// 模拟用户数据库
var users = map[string]string{
    "admin": "admin123",
    "user":  "user123",
}

// 生成JWT Token
func generateToken(userID uint, role string) (string, error) {
    expirationTime := time.Now().Add(24 * time.Hour)
    claims := &Claims{
        UserID: userID,
        Role:   role,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(expirationTime),
            IssuedAt:  jwt.NewNumericDate(time.Now()),
            Subject:   fmt.Sprintf("%d", userID),
        },
    }

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

// 登录处理函数
func loginHandler(c *gin.Context) {
    var loginData struct {
        Username string `json:"username"`
        Password string `json:"password"`
    }

    if err := c.ShouldBindJSON(&loginData); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
        return
    }

    // 验证用户(实际项目中应使用密码哈希验证)
    if storedPwd, ok := users[loginData.Username]; !ok || storedPwd != loginData.Password {
        c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
        return
    }

    // 确定用户ID和角色(模拟)
    var userID uint
    var role string
    if loginData.Username == "admin" {
        userID = 1
        role = "admin"
    } else {
        userID = 2
        role = "user"
    }

    token, err := generateToken(userID, role)
    if err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate token"})
        return
    }

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

// 受保护的路由处理函数
func protectedHandler(c *gin.Context) {
    claims, _ := c.Get("claims")
    c.JSON(http.StatusOK, gin.H{
        "message": "Access granted to protected resource",
        "claims":  claims,
    })
}

// Admin路由处理函数
func adminHandler(c *gin.Context) {
    claims, _ := c.Get("claims")
    c.JSON(http.StatusOK, gin.H{
        "message": "Welcome to the admin dashboard",
        "claims":  claims,
    })
}

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

    // 公开路由
    r.POST("/login", loginHandler)

    // 创建受保护的路由组
    api := r.Group("/api")
    {
        // 应用认证中间件(稍后实现)
        api.Use(authMiddleware())
        api.GET("/protected", protectedHandler)

        // 创建Admin路由组,并应用授权中间件
        admin := api.Group("/admin")
        {
            admin.Use(roleMiddleware("admin"))
            admin.GET("/dashboard", adminHandler)
        }
    }

    r.Run(":8080")
}

// 占位函数,将在auth.go中实现
func authMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
    }
}

// 占位函数,将在auth.go中实现
func roleMiddleware(requiredRole string) gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Next()
    }
}

main.go - 基础结构与路由设置

现在,我们来实现核心的认证中间件。在项目根目录下创建 `middleware` 目录,并在其中创建 `auth.go` 文件。这个中间件将负责从请求头中提取 `Authorization: Bearer <token>`,并验证Token的有效性。

package middleware

import (
    "net/http"
    "strings"

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

// 与main.go中相同的密钥
var jwtKey = []byte("my_secret_key")

// Claims结构体需要与main.go中一致
 type Claims struct {
    UserID uint   `json:"user_id"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

// AuthMiddleware 认证中间件
func AuthMiddleware() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 从请求头获取Authorization字段
        authHeader := c.GetHeader("Authorization")
        if authHeader == "" {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Authorization header missing"})
            c.Abort()
            return
        }

        // 检查Bearer格式
        if !strings.HasPrefix(authHeader, "Bearer ") {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid authorization format"})
            c.Abort()
            return
        }

        // 提取Token字符串
        tokenString := strings.TrimPrefix(authHeader, "Bearer ")

        // 解析Token
        claims := &Claims{}
        token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (interface{}, error) {
            return jwtKey, nil
        })

        // 验证Token
        if err != nil || !token.Valid {
            c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid token"})
            c.Abort()
            return
        }

        // 将解析出的claims存入上下文,供后续中间件或处理器使用
        c.Set("claims", claims)
        c.Next()
    }
}

// RoleMiddleware 授权中间件
func RoleMiddleware(requiredRole string) gin.HandlerFunc {
    return func(c *gin.Context) {
        // 从上下文中获取claims
        claimsInterface, exists := c.Get("claims")
        if !exists {
            c.JSON(http.StatusForbidden, gin.H{"error": "Claims not found"})
            c.Abort()
            return
        }

        claims, ok := claimsInterface.(*Claims)
        if !ok {
            c.JSON(http.StatusForbidden, gin.H{"error": "Invalid claims format"})
            c.Abort()
            return
        }

        // 检查角色
        if claims.Role != requiredRole {
            c.JSON(http.StatusForbidden, gin.H{"error": "Insufficient permissions"})
            c.Abort()
            return
        }

        c.Next()
    }
}

middleware/auth.go - 认证与授权中间件实现

现在,我们需要更新 `main.go` 文件,将认证中间件应用到受保护的路由组上。同时,我们还需要一个简单的授权中间件来检查用户角色(例如,只有 `admin` 角色才能访问某些端点)。

// 在main.go文件顶部添加导入
import (
    // ... 其他导入
    "gin-jwt-tutorial/middleware" // 替换为你的模块名
)

// 替换main.go中原来的占位函数
func authMiddleware() gin.HandlerFunc {
    return middleware.AuthMiddleware()
}

func roleMiddleware(requiredRole string) gin.HandlerFunc {
    return middleware.RoleMiddleware(requiredRole)
}

// main函数保持不变,路由设置已正确引用中间件

更新main.go以使用中间件

结果验证

现在,让我们启动服务器并测试整个认证流程。我们将使用 `curl` 命令来模拟客户端请求。

  1. 启动服务器:在项目根目录运行 `go run main.go`。
  2. 登录获取Token:使用 `admin` 用户登录,获取JWT Token。
  3. 访问受保护路由:使用获取到的Token访问 `/api/protected`。
  4. 访问Admin路由:使用 `admin` 用户的Token访问 `/api/admin/dashboard`。
  5. 测试普通用户权限:使用 `user` 用户登录,尝试访问Admin路由,应被拒绝。
# 1. 启动服务器(在新终端中)
go run main.go

# 2. 使用admin用户登录获取Token(在另一个终端中)
TOKEN=$(curl -s -X POST http://localhost:8080/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin123"}' | jq -r '.token')
echo "获取到的Token: $TOKEN"

# 3. 使用Token访问受保护的路由
curl -s -X GET http://localhost:8080/api/protected \
  -H "Authorization: Bearer $TOKEN" | jq .

# 4. 使用admin Token访问Admin路由
curl -s -X GET http://localhost:8080/api/admin/dashboard \
  -H "Authorization: Bearer $TOKEN" | jq .

# 5. 测试普通用户权限
USER_TOKEN=$(curl -s -X POST http://localhost:8080/login \
  -H "Content-Type: application/json" \
  -d '{"username":"user","password":"user123"}' | jq -r '.token')

curl -s -X GET http://localhost:8080/api/admin/dashboard \
  -H "Authorization: Bearer $USER_TOKEN" | jq .
# 预期输出:{"error":"Insufficient permissions"}

使用curl进行端到端测试

如果一切顺利,你应该能看到预期的输出。这证明了JWT认证和授权中间件已经成功集成到你的Gin应用中。

常见错误与排查

在开发过程中,你可能会遇到一些常见问题。以下是三个典型错误及其排查方法。

  • 错误1:Token签名无效 (Invalid signature)。原因:用于验证Token的密钥与生成Token的密钥不一致。排查:检查 `jwtKey` 变量在 `main.go` 和 `middleware/auth.go` 中是否完全相同。确保没有拼写错误或多余的空格。这是JWT安全的核心 [来源#2]。
  • 错误2:Token已过期 (Token has expired)。原因:Token的 `exp` 声明时间已过。排查:检查生成Token时设置的过期时间(`expirationTime`),确保客户端在有效期内使用Token。如果需要,可以实现Token刷新机制。
  • 错误3:缺少Authorization头或格式错误。原因:客户端请求未携带 `Authorization` 头,或格式不是 `Bearer <token>`。排查:使用 `curl` 或Postman时,确保请求头设置正确。在中间件中,我们已经对格式进行了严格检查,并返回了明确的错误信息。

Gin框架的中间件执行顺序很重要。认证中间件必须放在路由组的最前面,以确保在执行任何业务逻辑之前完成身份验证 [来源#1]。


通过本教程,你已经从零开始构建了一个完整的Go Gin JWT认证授权中间件。你学会了如何生成和验证JWT,如何在中间件中解析用户信息,以及如何将其应用到不同的路由上进行授权控制。这个中间件是可复用的,你可以将其应用到任何Gin项目中,为你的API提供坚实的安全基础。

参考链接

阅读剩余
THE END