JWT签名验证必须用ParseWithClaims而非Parse,因Parse仅解码不校验签名;敏感字段应存Claims而非URL参数,payload无加密;需设exp/iat;刷新Token须用HttpOnly Cookie存储并每次失效;应使用github.com/golang-jwt/jwt/v5库。
ParseWithClaims而非Parse
直接调用Parse只会解码token结构,不校验签名,攻击者可篡改payload后重签(若你误用对称密钥且未设SigningMethod约束,风险更高)。必须显式指定jwt.MapClaims并传入密钥和验证逻辑:
token, err := jwt.ParseWithClaims(tokenString, &jwt.MapClaims{}, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(secretKey), nil
})常见错误:漏掉token.Method类型检查,导致RSA公钥被误用于HMAC验证;或把secretKey写成硬编码字符串却没做环境隔离。
Claims而非URL参数JWT的payload是Base64Url编码、**无加密**的,任何中间人可解码查看。把user_id、role塞进自定义Claims没问题,但绝不能放密码哈希、手机号明文、邮箱全量字符串:
exp和iat必须设置,否则过期校验失效iss、sub除非真按RFC 7519语义用)scope数组比单个role字符串更易扩展示例安全写法:
claims := jwt.MapClaims{
"user_id": 123,
"scope": []string{"read:order", "write:cart"},
"exp": time.Now().Add(24 * time.Hour).Unix(),
"iat": time.Now().Unix
(),
}RefreshToken双令牌机制仅靠一个长期有效的Access Token存在泄露后无法主动作废的问题。正确做法是颁发短期Access Token(如15分钟)+ 长期Refresh Token(如7天),且Refresh Token必须:
HttpOnly + Secure Cookie中,禁止JS访问payload里刷新接口典型流程:
func refreshHandler(w http.ResponseWriter, r *http.Request) {
cookie, err := r.Cookie("refresh_token")
if err != nil {
http.Error(w, "no refresh token", http.StatusUnauthorized)
return
}
// 1. 校验cookie中的refresh_token是否有效且未被使用
// 2. 生成新access_token(含新exp)
// 3. 在DB中标记该refresh_token为已使用
// 4. 返回新access_token(不返回新refresh_token,除非显式请求轮换)
}github.com/golang-jwt/jwt/v5而非旧版旧版github.com/dgrijalva/jwt-go已被弃用,存在关键漏洞(如CVE-2025-26160:None算法绕过)。v5版强制要求显式声明SigningMethod,默认禁用none算法,并修复了时钟偏移校验缺陷:
github.com/golang-jwt/jwt/v5,不是v4或无版本号ParseWithClaims第二个参数必须是指针类型(&jwt.MapClaims{}),否则v5会panic1s误差,若需更严格,用WithValidator自定义最容易被忽略的是:v5的SigningMethodHS256.KeyFunc返回值必须是interface{},不是[]byte——传错类型会导致签名永远不匹配。