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路由中。
- 定义JWT载荷结构:创建一个结构体来存储用户ID和角色,这是JWT的核心数据。
- 实现Token生成器:提供一个函数,根据用户信息生成签名的JWT字符串。
- 编写认证中间件:这是一个Gin中间件,它从请求头中提取Token,验证其签名和有效期,并将解析出的用户信息存入上下文。
- 集成到路由:创建受保护的路由和公开的登录路由,测试中间件是否生效。
核心代码实现
首先,我们在项目根目录下创建 `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` 命令来模拟客户端请求。
- 启动服务器:在项目根目录运行 `go run main.go`。
- 登录获取Token:使用 `admin` 用户登录,获取JWT Token。
- 访问受保护路由:使用获取到的Token访问 `/api/protected`。
- 访问Admin路由:使用 `admin` 用户的Token访问 `/api/admin/dashboard`。
- 测试普通用户权限:使用 `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提供坚实的安全基础。