Go mod 好菜系列 - 0x08 jwt 这张票该怎么发和怎么验
更模块化地聊 jwt:签发、校验、claims、过期、刷新和中间件实践,看看它为什么高频,又为什么不能被神化。
只要项目里一提到登录态,JWT 基本迟早会出现。它常见到一种程度,很多人会下意识把“认证方案”和“JWT”直接画等号。但实际上,JWT 只是常见方案之一,而且还是一个很容易被用得过度自信的方案。
先别急着吹 JWT,先想清楚它在解决什么
它本质上是在解决“服务端怎么识别调用方身份”这件事。简单说:
- 服务端签一个 token 给客户端
- 客户端后续请求带着它来
- 服务端验证签名和 claims
这样就不需要每次都去查一次 session 存储,至少在某些场景下会更轻一点。
常见 Go JWT 库会长什么样
现在社区里比较常见的是 github.com/golang-jwt/jwt/v5 这类库。重点不在库名字,而在它把 JWT 的几个关键步骤拆得比较清楚:
- 定义 claims
- 签发 token
- 解析 token
- 验证签名和过期
签发 token 的最小示例
claims := jwt.MapClaims{
"user_id": 1,
"role": "admin",
"exp": time.Now().Add(2 * time.Hour).Unix(),
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte("secret-key"))这里最重要的不是“能不能签出来”,而是你往 claims 里放了什么。别因为它是你自己签的,就把敏感信息一股脑塞进去。
校验 token 也不只是能 parse 就算完
parsed, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte("secret-key"), nil
})
if err != nil {
return err
}
if !parsed.Valid {
return errors.New("invalid token")
}很多人一开始会把“能解析成功”和“这个 token 合法”混成一回事。实际上你还得关心签名方法是不是你预期的、claims 里有没有必要字段、过期时间是不是有效。
自定义 claims 往往更实用
type UserClaims struct {
UserID int64 `json:"user_id"`
Role string `json:"role"`
jwt.RegisteredClaims
}这样比全程 MapClaims 更可控,尤其是字段越来越多的时候。
JWT 在项目里最常见的落点:中间件
func AuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
auth := c.GetHeader("Authorization")
if auth == "" {
c.AbortWithStatusJSON(401, gin.H{"error": "unauthorized"})
return
}
// 这里解析和校验 token
c.Next()
}
}模块层面最关键的理解其实是:JWT 库本身只是工具,真正的工程接入点往往是在中间件里。
JWT 最容易让人误判的地方
- 觉得有了 JWT 就“无状态且无敌”
- 忘了 token 失效、刷新、注销怎么做
- claims 塞太多东西
- 把权限判断全寄托在 token 里,不再结合数据库和业务状态
JWT 能解决的是身份票据问题,不是整个安全体系问题。
什么时候适合用 JWT
- 前后端分离接口服务
- 多服务间需要传递调用身份
- 希望减少传统 session 存储依赖
如果你的系统本身就是强会话、强状态、强撤销控制的,JWT 不一定天然就是最顺手的方案。
小结
JWT 这道菜很常见,但最好别把它吃成信仰:
- 它适合解决身份票据传递,不等于解决所有认证问题
- 签发、解析、claims 设计、中间件接入是核心
- 别把 token 当权限真理
- 刷新、注销、失效策略必须一开始就想
下一篇我们讲 sqlx。如果你对 ORM 不完全放心,但又嫌原生 database/sql 太原始,那这道菜就很值得看看。