← 中文资源

简体中文版本

Gin JWT 声明校验中间件

在 Gin 中间件里解析并验证 JWT:签名方法白名单防算法混淆、签发方与过期声明强制校验、角色通过后声明注入上下文。

阅读英文原文

示例说明

令牌校验的坑比想象多:不检查签名方法,攻击者可用 none 算法伪造;不检查过期,旧令牌永久有效。ParseWithClaims 的密钥回调里先断言 HMAC 方法——算法白名单是防降级攻击的关键;WithIssuer 与 WithExpirationRequired 把两个声明设为强制。验证通过的声明挂到 gin 上下文,处理器零重复解析。

技术要点

  • 签名方法白名单防算法降级
  • 签发方与过期声明强制校验
  • claims 注入上下文免二次解析

代码解读

authMiddleware 是参数化高阶函数:密钥与允许角色由路由声明;Bearer 前缀解析后走 ParseWithClaims,回调里验证签名方法再给密钥。角色不匹配返回 403 与 401 区分认证失败和权限不足。处理器用 MustGet 取回声明直接访问用户与角色字段。

使用建议

密钥从环境注入并支持轮换双验。角色枚举集中定义防散落字符串。生产配令牌黑名单处理登出场景。

代码示例

package main

import (
	"errors"
	"net/http"
	"strings"
	"time"

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

var errInvalidToken = errors.New("invalid token")

type AppClaims struct {
	UserID string `json:"uid"`
	Role   string `json:"role"`
	jwt.RegisteredClaims
}

func authMiddleware(secret string, allowedRoles ...string) gin.HandlerFunc {
	return func(c *gin.Context) {
		header := c.GetHeader("Authorization")
		if !strings.HasPrefix(header, "Bearer ") {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "missing bearer token"})
			return
		}

		token, err := jwt.ParseWithClaims(
			strings.TrimPrefix(header, "Bearer "),
			&AppClaims{},
			func(t *jwt.Token) (interface{}, error) {
				if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
					return nil, errors.New("unexpected signing method")
				}
				return []byte(secret), nil
			},
			jwt.WithIssuer("auth.example.com"),
			jwt.WithExpirationRequired(),
		)
		if err != nil || !token.Valid {
			c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": errInvalidToken.Error()})
			return
		}

		claims := token.Claims.(*AppClaims)
		for _, role := range allowedRoles {
			if claims.Role == role {
				c.Set("claims", claims)
				c.Next()
				return
			}
		}
		c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "role not permitted"})
	}
}

func main() {
	r := gin.New()
	r.GET("/admin/stats", authMiddleware("secret", "admin"), func(c *gin.Context) {
		claims := c.MustGet("claims").(*AppClaims)
		c.JSON(http.StatusOK, gin.H{"for": claims.UserID, "generated": time.Now().Unix()})
	})
	r.Run(":8080")
}