以下是为 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. 核心机制解析
中间件工作流程
- 请求拦截:检查请求头中的
Authorization
- 格式验证:必须为
Bearer <token>
格式 - 签名验证:使用相同密钥验证签名有效性
- Claims解析:将解码后的用户信息存入上下文
- 错误处理:统一处理各种JWT验证错误
关键设计点
- 上下文传递:通过
c.Set("claims", claims)
传递用户信息 - 错误处理封装:
handleJWTError
统一处理各类JWT错误 安全验证:
- 强制验证签名方法 (
SigningMethodHMAC
) - 验证签发者 (
WithIssuer
) - 自动验证过期时间
- 强制验证签名方法 (
5. 最佳实践建议
密钥管理:
// 从环境变量获取(推荐) jwtSecret = []byte(os.Getenv("JWT_SECRET"))
刷新令牌机制:
- 当access token过期时,使用refresh token获取新token
- Refresh token应有更长有效期,存储于数据库
敏感操作保护:
// 在关键操作前验证用户状态 func TransferMoney(c *gin.Context) { claims := c.MustGet("claims").(*CustomClaims) if claims.UserStatus != "active" { c.AbortWithStatusJSON(403, gin.H{"error": "账户已被冻结"}) } // ...业务逻辑 }
日志记录:
// 在中间件中添加日志 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 }