4. JWT 原理与实现
JWT(JSON Web Token)是目前分布式系统中最主流的无状态身份认证方案。在 Go 业务模块的边界权限验证中,JWT 承担着“通行证”的角色:用户登录后获得一个签发的 Token,后续每次请求携带该 Token,服务端无需查询会话存储即可完成身份与权限校验。
4.1 JWT 结构:三部分拆解
一个标准的 JWT 由三个 Base64URL 编码的片段组成,以点号(.)分隔:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
| 部分 | 名称 | 内容说明 | 是否加密 |
|---|---|---|---|
| Header | 头部 | 令牌类型(typ)和签名算法(alg),如 HS256、RS256 | 仅 Base64 编码,不加密 |
| Payload | 载荷 | 声明(claims):用户标识、角色、过期时间等业务数据 | 仅 Base64 编码,不加密 |
| Signature | 签名 | 对 Header + Payload 使用密钥和指定算法生成的签名 | 防篡改 |
关键认知:JWT 默认是签名验证而非加密。Payload 中的敏感信息(如密码、身份证号)不应直接放入,必须使用时需额外加密。
4.2 生成 Token:签名流程
生成 JWT 的核心步骤:构造 Header → 构造 Payload → 使用密钥签名。Go 中推荐使用 github.com/golang-jwt/jwt/v5 库(原 dgrijalva/jwt-go 的官方维护分支)。
4.2.1 安装与导入
go get github.com/golang-jwt/jwt/v5
4.2.2 生成 Token 代码示例
package main
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
)
// 自定义 Claims 结构体
type CustomClaims struct {
UserID string `json:"user_id"`
Roles []string `json:"roles"`
jwt.RegisteredClaims
}
func GenerateToken(secret string) (string, error) {
claims := CustomClaims{
UserID: "u_10086",
Roles: []string{"admin", "editor"},
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)), // 过期时间
IssuedAt: jwt.NewNumericDate(time.Now()), // 签发时间
Issuer: "go-auth-system", // 签发者
Subject: "access_token", // 主题
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString([]byte(secret))
if err != nil {
return "", err
}
return tokenString, nil
}
func main() {
secret := "my-256-bit-secret-key"
token, _ := GenerateToken(secret)
fmt.Println("Generated JWT:", token)
}
最佳实践:密钥长度必须与算法匹配。HS256 要求密钥至少 256 位(32 字节);生产环境密钥应从环境变量或密钥管理服务获取,禁止硬编码。
4.3 解析与验证 Token
验证 Token 时,服务端需要完成:签名校验 → 过期时间检查 → 声明提取。使用 jwt-go 的 ParseWithClaims 方法可以一步完成。
4.3.1 验证代码示例
func ValidateToken(tokenString string, secret string) (*CustomClaims, error) {
token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
// 1. 校验签名算法是否匹配
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(secret), nil
})
if err != nil {
return nil, err
}
// 2. 类型断言提取 Claims
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}
func main() {
secret := "my-256-bit-secret-key"
tokenStr := "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
claims, err := ValidateToken(tokenStr, secret)
if err != nil {
fmt.Println("Token validation failed:", err)
return
}
fmt.Printf("Valid! UserID: %s, Roles: %v\n", claims.UserID, claims.Roles)
}
4.3.2 常见验证错误处理
| 错误类型 | 错误原因 | 处理方式 |
|---|---|---|
jwt.ErrSignatureInvalid |
签名不匹配,Token 被篡改 | 返回 401,拒绝请求 |
jwt.ErrTokenExpired |
Token 已超过 ExpiresAt 时间 |
返回 401,提示重新登录或使用 Refresh Token |
jwt.ErrTokenNotValidYet |
Token 的 NotBefore 时间未到 |
返回 401,等待 Token 生效 |
jwt.ErrTokenMalformed |
Token 格式错误(非三段式) | 返回 400,拒绝解析 |
4.4 jwt-go 库核心 API 速查
| API | 功能 | 使用场景 |
|---|---|---|
jwt.NewWithClaims(method, claims) |
创建 Token 对象,指定签名算法和 Claims | 生成 Token |
token.SignedString(key) |
使用密钥对 Token 签名,返回完整 JWT 字符串 | 生成 Token |
jwt.ParseWithClaims(tokenStr, claims, keyFunc) |
解析并验证 Token,返回 *Token 和错误 |
验证 Token |
jwt.NewNumericDate(t) |
将 time.Time 转换为 JWT 标准时间格式 |
设置过期/签发时间 |
jwt.SigningMethodHS256 |
HMAC-SHA256 对称签名方法 | 单服务/内网信任环境 |
jwt.SigningMethodRS256 |
RSA-SHA256 非对称签名方法 | 微服务/跨系统签名分发 |
安全警告:切勿在 Payload 中存储明文密码、信用卡号等敏感信息。JWT 的 Payload 仅做 Base64 编码,任何持有 Token 的人都可以解码读取。如需加密载荷,请使用 JWE(JSON Web Encryption)标准。
4.5 对称 vs 非对称签名选择
| 特性 | HS256(对称) | RS256(非对称) |
|---|---|---|
| 密钥 | 单一密钥,签名和验证使用同一个 | 私钥签名,公钥验证 |
| 性能 | 快(HMAC 计算量小) | 慢(RSA 计算量大) |
| 密钥分发 | 所有验证方必须持有密钥,泄露风险高 | 仅签名方持有私钥,公钥可公开分发 |
| 适用场景 | 单体应用、内网服务间信任 | 微服务网关、第三方开放 API |
在 Go 业务模块边界权限验证中,推荐优先使用 RS256:认证中心持有私钥签发 Token,各业务模块只持有公钥验证,即使某个模块被攻破,攻击者也无法伪造 Token。