示例说明
令牌校验的坑比想象多:不检查签名方法,攻击者可用 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")
}