先说几个核心判断:JWT(JSON Web Token)是Go语言实现无状态认证的基石,但直接套用示例代码,大概率会在生产环境出问题。密钥硬编码、exp校验失败、中间件透传丢失上下文、Token被重放——这些都不是“跑通就行”的小麻烦。

JWT的核心结构,说白了就是Header.Payload.Signature这三部分。Header声明类型和算法,Payload里放声明(注意,这只是Base64URL编码,不是加密),Signature确保完整性。数据都是明牌,但怎么用,区别很大。
jwt.Parse() 必须与 ParseWithClaims 和 SigningMethod.KeyFunc 配合使用
你猜最容易栽在哪儿?是调用 jwt.ParseUnverified() 解析payload后,手动判断 exp。这等于主动放弃签名验证,攻击者可以任意修改 user_id 或 exp,伪造一个看起来合法的token。
正确的做法始终是:用 jwt.ParseWithClaims(),并且确保 KeyFunc 返回的密钥类型跟签名算法匹配。具体来说:
- HS256:必须返回
[]byte,不能传字符串。从环境变量读取时,要用[]byte(os.Getenv("JWT_SECRET"))。 - RS256:
KeyFunc需要返回*rsa.PublicKey,而私钥签名时要用对应的*rsa.PrivateKey。 - 校验时如果密钥类型搞错了(比如HS256传了个rsa.PublicKey),
jwt.ParseWithClaims()会静默失败,token.Valid为false,但err可能是nil。这很要命,因为代码如果只检查err,就会误以为token有效。
AuthMiddleware 中必须显式注入 context 并校验 token 有效性
很多中间件只做了解析,没检查 token.Claims.(jwt.MapClaims)["user_id"] 是否存在,或者忽略了 token.Header["alg"] 是否被篡改为 none(这是一个已知漏洞)。
关键逻辑一步都不能省:
- 先通过
strings.TrimPrefix(tokenStr, "Bearer ")剥离前缀,再用strings.TrimSpace()清空两端空格。 - 解析后必须同时判断
err == nil和token.Valid == true,二者缺一不可。 - 把用户标识写入
c.Request.Context()(Gin框架)或r.Context()(net/http),而不是c.Set()这类框架内部存储,避免handler里取不到。 - 如果使用自定义的
UserClaims结构体,务必实现Valid()方法来校验exp、nbf等标准字段。
refresh token 不该用 JWT 实现,而应走服务端存储 + 单次失效
把refresh token也做成JWT是一个典型误区。JWT无法主动作废,一旦泄露,就等于给了攻击者一张长期通行证。
生产环境应该这么干:
- refresh token存在Redis里,key用
refresh:,value存关联的user_id和过期时间。 - 签发新access token时,生成唯一的
jti,存其SHA256哈希到Redis,并设置跟refresh token一样的TTL。 - 每次使用refresh token,先查Redis是否存在且未被标记为已使用,成功后立即
DEL这个key,做到一用即废。 - access token的
exp控制在15–60分钟,refresh token的TTL控制在7天以内。
最容易被忽略的是时钟偏移处理。服务端和客户端系统时间差超过1秒,exp 就会校验失败。别调服务器时间,改用 jwt.WithLeeway(5 * time.Second) 传入 ParseWithClaims 的选项参数,这才是省心又靠谱的做法。