JWT签名密钥必须用[]byte而非字符串直传,因jwt.Parse内部对密钥做[]byte比较;正确做法是signingKey := []byte("my-secret")或从环境变量读取并校验非空;HS256密钥建议≥32字节,RS256需用rsa.PrivateKey/rsa.PublicKey;解析时须手动校验exp/nbf,因token.Valid不检查时间;提取Bearer Token应使用strings.Fields和strings.EqualFold处理格式异常;微服务间透传JWT需剥离敏感字段并用专用密钥重签,exp应显著缩短,密钥须物理隔离。

JWT签名密钥必须用 []byte而非字符串直传
很多人直接把字符串如"my-secret"传给jwt.SigningMethodHS256的SigningKey,结果签名失败或验签始终报token is invalid。这是因为jwt.Parse内部对密钥做[]byte比较,而字符串字面量在Go中不是可寻址的[]byte。正确做法是显式转成字节切片:
signingKey := []byte("my-secret")更安全的做法是读取环境变量后强制转换,避免硬编码:
signingKey := []byte(os.Getenv("JWT_SECRET"))- 如果密钥为空或只有空白字符,
jwt.Parse不会报错但会静默失败,建议提前校验len(signingKey) > 0 - 使用
HS256时密钥长度建议≥32字节;若用RS256,则必须用*rsa.PrivateKey/*rsa.PublicKey,不能用[]byte
解析Token时必须显式设置ValidFunc校验exp和nbf
默认jwt.Parse只做签名验证,不检查过期(exp)、生效时间(nbf)或签发者(iss)。微服务间调用若跳过这些,会导致已过期Token仍被接受。
必须配合jwt.WithValidator(v5+)或手动在Parse回调里判断:
立即学习“go语言免费学习笔记(深入)”;
token, err := jwt.Parse(tokenString, 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 signingKey, nil
})
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
if float64(time.Now().Unix()) > claims["exp"].(float64) {
return errors.New("token expired")
}
}-
jwt.MapClaims里的数值默认是float64,不是int64,直接断言int64(claims["exp"])会panic - 不要依赖
token.Valid就认为时间有效——它只表示签名和结构合法,exp/nbf需手动比对 - 微服务集群时间不同步时,建议在
exp校验中加几秒宽松窗口(如±2s),避免因NTP漂移误拒
HTTP中间件中提取Bearer Token要处理空格与前缀缺失
前端常把Token写成Authorization: Bearer xxxxx,但实际请求可能漏空格、多空格、大小写混用(如bearer),甚至根本没带Authorization头。直接strings.Split(r.Header.Get("Authorization"), " ")会panic或取错。
安全提取逻辑应为:
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
return errors.New("missing Authorization header")
}
parts := strings.Fields(authHeader)
if len(parts) != 2 || !strings.EqualFold(parts[0], "Bearer") {
return errors.New("invalid Authorization header format")
}
tokenString := parts[1]- 用
strings.Fields代替strings.Split(..., " "),自动处理多空格、首尾空格 - 用
strings.EqualFold做大小写不敏感比较,兼容BEARER/bearer - 别在中间件里直接
http.Error返回401——微服务间调用可能用gRPC或消息队列,应统一返回error供上层决策
微服务间透传JWT需剥离敏感字段并重签,不能原样转发
用户登录后拿到的JWT通常含user_id、email等敏感信息。若A服务直接把原始Token透传给B服务,B服务就拥有了用户全量身份上下文,违背最小权限原则,也增加泄露面。
正确做法是在网关或调用方服务中:清空原始claims,仅保留必要字段(如sub、scope),用服务间专用密钥重签:
newToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"sub": claims["sub"],
"scope": "order:read",
"iss": "service-a",
"exp": time.Now().Add(5 * time.Minute).Unix(),
})
signedToken, _ := newToken.SignedString(serviceKey)- 重签密钥(
serviceKey)必须与用户登录密钥(signingKey)物理隔离,最好分属不同KMS密钥 - 重签Token的
exp应显著短于原始Token(如5分钟),避免下游服务缓存过久 - 别把原始Token当
context.Context值向下传递——它不是状态载体,而是凭证,每次跨服务都该视为一次新授权
JWT本身不解决服务间信任链问题。真正难的是密钥轮换、失效通知和跨域aud校验,这些没法靠单个Token字段控制,得靠服务注册中心或专用鉴权服务兜底。


















