Go 中最主流 JWT 库 jwt -go
JWT(JSON Web Token)是目前前后端分离项目中最主流的身份认证方案,它通过在客户端存储加密 Token,实现了无状态的身份验证,完美适配分布式系统和微服务架构。
- 自定义声明:通过嵌入
jwt.RegisteredClaims定义 Token 中的数据。 - 生成 Token:使用
jwt.NewWithClaims()和SignedString()生成 Token。 - 解析 Token:使用
jwt.ParseWithClaims()解析并验证 Token。 - 中间件集成:作为 Gin 中间件保护受保护接口。
- Refresh Token:实现无感刷新 Token,提升用户体验。
什么是 JWT
JWT 是一种开放标准(RFC 7519),它通过 JSON 对象在各方之间安全地传输信息,这些信息可以被验证和信任,因为它是数字签名的。
JWT 的结构 JWT 由三部分组成,用点(.)分隔。
示例:xxxxx.yyyyy.zzzzz
- Header(头部) :包含令牌类型(JWT)和签名算法(如 HS256)
- Payload(载荷) :包含声明(Claims),即实际传输的数据(如用户 ID、过期时间)
- Signature(签名) :使用密钥对 Header 和 Payload 进行签名,防止数据被篡改
JWT 的工作流程
- 用户登录成功后,服务器生成 JWT Token 并返回给客户端
- 客户端将 Token 存储在本地(如 LocalStorage、Cookie)
- 客户端后续请求时在请求头(如
Authorization: Bearer <token>)中携带 Token - 服务器验证 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(刷新令牌)。
核心思路
- 登录时同时生成 Access Token(有效期 2 小时)和 Refresh Token(有效期 7 天)
- Access Token 用于访问受保护接口
- Access Token 过期后,使用 Refresh Token 换取新的 Access Token
- 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 或短时间内防重放。