Go 中最主流 JWT 库 jwt -go

0 阅读6分钟

Go 中最主流 JWT 库 jwt -go

JWT(JSON Web Token)是目前前后端分离项目中最主流的身份认证方案,它通过在客户端存储加密 Token,实现了无状态的身份验证,完美适配分布式系统和微服务架构。

  1. 自定义声明:通过嵌入 jwt.RegisteredClaims 定义 Token 中的数据。
  2. 生成 Token:使用 jwt.NewWithClaims() 和 SignedString() 生成 Token。
  3. 解析 Token:使用 jwt.ParseWithClaims() 解析并验证 Token。
  4. 中间件集成:作为 Gin 中间件保护受保护接口。
  5. Refresh Token:实现无感刷新 Token,提升用户体验。

什么是 JWT

JWT 是一种开放标准(RFC 7519),它通过 JSON 对象在各方之间安全地传输信息,这些信息可以被验证和信任,因为它是数字签名的。

JWT 的结构 JWT 由三部分组成,用点(.)分隔。

示例:xxxxx.yyyyy.zzzzz

  • Header(头部) :包含令牌类型(JWT)和签名算法(如 HS256)
  • Payload(载荷) :包含声明(Claims),即实际传输的数据(如用户 ID、过期时间)
  • Signature(签名) :使用密钥对 Header 和 Payload 进行签名,防止数据被篡改

JWT 的工作流程

  1. 用户登录成功后,服务器生成 JWT Token 并返回给客户端
  2. 客户端将 Token 存储在本地(如 LocalStorage、Cookie)
  3. 客户端后续请求时在请求头(如 Authorization: Bearer <token>)中携带 Token
  4. 服务器验证 Token 的有效性,验证通过后允许访问受保护资源

核心 API 详解

自定义声明:定义 Token 中的数据

JWT 的 Payload 部分通过声明存储数据,jwt-go 提供了标准声明,同时支持自定义声明,这是 JWT 开发的第一步。

自定义声明结构体

// 必须嵌入 jwt.RegisteredClaims,它包含了 JWT 的标准声明
type UserClaims struct {
	
   // 自定义声明:根据业务需求添加
	UserID   uint   `json:"user_id"`
	Username string `json:"username"`
        
	// 嵌入标准声明
	jwt.RegisteredClaims
}

jwt.RegisteredClaims 包含了 JWT 的标准声明

字段作用示例
Issuer签发者"your_app_name"
Subject主题"user_auth"
Audience受众"your_client"
ExpiresAt过期时间jwt.NewNumericDate(time.Now().Add(2 * time.Hour))
NotBefore生效时间jwt.NewNumericDate(time.Now())
IssuedAt签发时间jwt.NewNumericDate(time.Now())
ID唯一标识uuid.NewString()

生成 Token:创建 JWT

用户登录成功后,服务器需要生成 JWT Token 并返回给客户端,这是 JWT 开发的核心功能。

API作用
jwt.NewWithClaims(method, claims)创建 Token 对象,指定签名算法和声明
token.SignedString(secretKey)使用密钥对 Token 进行签名,生成最终的 Token 字符串

生成 JWT Token

// JWT 密钥(生产环境必须存储在环境变量或配置文件中,严禁硬编码!)
var JwtSecret = []byte("your_jwt_secret_key_change_this_in_production")

func GenerateToken(userId uint, username string) (string, error) {
	// 创建声明
	claims := UserClaims{
		UserID:   userId,
		Username: username,
		RegisteredClaims: jwt.RegisteredClaims{
			Issuer:    "gin_demo",          // 签发者
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)), // 过期时间:2小时后
			NotBefore: jwt.NewNumericDate(time.Now()), // 生效时间:立即生效
			IssuedAt:  jwt.NewNumericDate(time.Now()), // 签发时间:现在
			ID:        fmt.Sprintf("%d", userId), // 唯一标识
		},
	}

	// 创建 Token 对象,使用 HS256 签名算法
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	// 使用密钥签名,生成最终的 Token 字符串
	tokenString, err := token.SignedString(JwtSecret)
	if err != nil {
		return "", fmt.Errorf("生成 Token 失败:%v", err)
	}

	return tokenString, nil
}
  • 密钥安全:JWT 密钥必须严格保密,生产环境必须存储在环境变量、配置中心或密钥管理服务中,严禁硬编码在代码里!
  • 密钥强度:密钥长度建议至少 32 位,使用随机字符串,避免使用弱密钥。
  • 过期时间:必须设置 ExpiresAt,建议 Access Token 有效期为 1-2 小时,Refresh Token 有效期为 7-30 天。

解析 Token:验证 JWT 有效性

客户端请求时携带 Token,服务器需要解析并验证 Token 的有效性,这是 JWT 开发的另一个核心功能。

API作用
jwt.ParseWithClaims(tokenString, &claims, keyFunc)解析 Token 并验证签名,同时将声明绑定到自定义结构体
token.Valid判断 Token 是否有效(签名正确、未过期、已生效)

解析并验证 JWT Token

func ParseToken(tokenString string) (*UserClaims, error) {
	// 解析 Token
	claims := &UserClaims{}
	token, err := jwt.ParseWithClaims(tokenString, claims, func(token *jwt.Token) (any, error) {
		// 验证签名算法是否为 HS256(防止算法替换攻击)
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, fmt.Errorf("签名算法不匹配:%v", token.Header["alg"])
		}
		// 返回密钥
		return JwtSecret, nil
	})

	if err != nil {
		return nil, fmt.Errorf("解析 Token 失败:%v", err)
	}

	// 验证 Token 是否有效
	if !token.Valid {
		return nil, fmt.Errorf("Token 无效")
	}

	return claims, nil
}
  • 验证签名算法:必须在 keyFunc 中验证签名算法是否为预期的算法(如 HS256),防止算法替换攻击。
  • 错误处理:解析 Token 时可能出现多种错误(签名错误、过期、未生效等),需根据错误类型返回不同的提示信息。

Gin 中间件集成

JWT 最常用的场景是作为 Gin 中间件,保护需要登录才能访问的接口,这是企业开发的必用功能。

JWT 鉴权中间件

func JWTAuthMiddleware() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 1. 从请求头获取 Token
		// 格式:Authorization: Bearer <token>
		authHeader := c.GetHeader("Authorization")
		if authHeader == "" {
			c.JSON(http.StatusUnauthorized, gin.H{
				"code": 401,
				"msg":  "请先登录",
			})
			c.Abort() // 终止请求
			return
		}

		// 2. 解析 Bearer 前缀
		var tokenString string
		_, err := fmt.Sscanf(authHeader, "Bearer %s", &tokenString)
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{
				"code": 401,
				"msg":  "Token 格式错误",
			})
			c.Abort()
			return
		}

		// 3. 解析并验证 Token
		claims, err := ParseToken(tokenString)
		if err != nil {
			c.JSON(http.StatusUnauthorized, gin.H{
				"code": 401,
				"msg":  "Token 无效或已过期",
			})
			c.Abort()
			return
		}

		// 4. 将用户信息存入 Gin 上下文,后续处理函数可获取
		c.Set("userId", claims.UserID)
		c.Set("username", claims.Username)

		// 5. 继续执行后续处理
		c.Next()
	}
}

func main() {
	r := gin.Default()

	// 公开接口:无需登录
	r.POST("/login", func(c *gin.Context) {
		……………………………………
           // 模拟登录
	})

	// 受保护接口:需要登录
	authGroup := r.Group("/api")
	authGroup.Use(JWTAuthMiddleware()) // 应用 JWT 鉴权中间件
	{
		……………………………………
           // 模拟处理逻辑与返回信息
	}

	r.Run(":8080")
}

刷新 Token

为了安全性,Access Token(访问令牌)的有效期通常较短(如 2 小时),但用户不希望每 2 小时就重新登录一次,这时就需要 Refresh Token(刷新令牌)。

核心思路

  1. 登录时同时生成 Access Token(有效期 2 小时)和 Refresh Token(有效期 7 天)
  2. Access Token 用于访问受保护接口
  3. Access Token 过期后,使用 Refresh Token 换取新的 Access Token
  4. Refresh Token 过期后,用户需要重新登录
// 生成 Access Token 和 Refresh Token
func GenerateTokens(userId uint, username string) (accessToken, refreshToken string, err error) {
	// Access Token:有效期 2 小时
	accessClaims := UserClaims{
		UserID:   userId,
		Username: username,
		RegisteredClaims: jwt.RegisteredClaims{
			Issuer:    "gin_demo",
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
		},
	}
	accessToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims).SignedString(JwtSecret)
	if err != nil {
		return
	}

	// Refresh Token:有效期 7 天
	refreshClaims := jwt.RegisteredClaims{
		Issuer:    "gin_demo",
		ExpiresAt: jwt.NewNumericDate(time.Now().Add(7 * 24 * time.Hour)),
		IssuedAt:  jwt.NewNumericDate(time.Now()),
		ID:        fmt.Sprintf("%d", userId),
	}
	refreshToken, err = jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims).SignedString(JwtSecret)
	return
}

注意事项

  • 使用 HTTPS:JWT Token 必须通过 HTTPS 传输,防止被中间人窃取。
  • Token 存储:客户端建议将 Token 存储在 HttpOnly Cookie 中,防止 XSS 攻击;如存储在 LocalStorage,需做好 XSS 防护。
  • 设置合理的过期时间:Access Token 有效期建议 1-2 小时,Refresh Token 有效期建议 7-30 天。
  • 使用强密钥:密钥长度至少 32 位,使用随机字符串,定期更换密钥。
  • 验证签名算法:解析 Token 时必须验证签名算法,防止算法替换攻击。
  • 黑名单机制:如需主动注销 Token,可使用 Redis 实现 Token 黑名单。
  • 不要在 Token 中存储敏感信息:JWT 的 Payload 部分仅 Base64 编码,未加密,任何人都可以解码查看,严禁存储密码、身份证号等敏感信息。
  • 密钥必须保密:密钥泄露会导致攻击者可以伪造任意 Token,生产环境必须严格保管密钥。
  • 防止重放攻击:可在 Token 中添加 jti(JWT ID)声明,结合 Redis 实现一次性 Token 或短时间内防重放。