Jwt-Golang 实践

Jwt Golang 实践

JWT(JSON Web Token)是现代Web应用中常用的身份验证和信息交换技术,本文将结合实际Go代码,深入讲解JWT的工作原理、实现方式以及最佳实践。

一、JWT基础概念

1.1 什么是JWT?

JWT是一种开放标准(RFC 7519),用于在网络应用环境间传递声明(claims)的紧凑且自包含的方式。JWT可以使用HMAC算法或RSA/ECDSA等公钥/私钥对进行签名,确保传输内容不被篡改。

1.2 JWT的结构

JWT由三部分组成,以点(.)分隔:

  • Header: 描述JWT的元数据,如类型和使用的签名算法
  • Payload: 包含声明(claims),即实体(用户)和其他数据的声明
  • Signature: 用于验证消息在传输过程中没有被篡改
1
2
xxxxx.yyyyy.zzzzz
Header.Payload.Signature

二、JWT工作原理深度解析

从代码实现角度看,JWT的工作原理可以分为以下几个环节:

2.1 创建JWT

参考我的 jwt.go 实现,具体代码在 ``,经过测试可以正常工作 使用了 github.com/golang-jwt/jwt/v5

我们看jwt.go中的CreateToken方法实现:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func (j *JWT) CreateToken(claims JWTClaims) (string, int64, error) {
    // 设置token有效期
    expiresAt := time.Now().Add(j.config.TokenExpireDuration)
    claims.ExpiresAt = jwt.NewNumericDate(expiresAt)
    claims.IssuedAt = jwt.NewNumericDate(time.Now())
    claims.Issuer = j.config.Issuer

    // 可选:设置NotBefore,提高安全性
    claims.NotBefore = jwt.NewNumericDate(time.Now())

    // 创建token
    token := jwt.NewWithClaims(j.config.SigningMethod, claims)
    tokenString, err := token.SignedString(j.config.SigningKey)

    return tokenString, expiresAt.Unix(), err
}

该方法展示了JWT创建的核心步骤:

  1. 设置有效期(ExpiresAt)、发布时间(IssuedAt)和发行人(Issuer)
  2. 设置令牌生效时间(NotBefore),增强安全性
  3. 使用指定签名算法创建token
  4. 使用密钥对token进行签名

2.2 解析与验证JWT

JWT解析和验证是确保安全的关键步骤:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
func (j *JWT) ParseToken(tokenString string) (*JWTClaims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (any, error) {
        // 验证签名方法
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, ErrTokenInvalid
        }
        return j.config.SigningKey, nil
    })

    // 错误处理省略...

    if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
        return claims, nil
    }
    return nil, ErrTokenInvalid
}

这段代码展示了JWT验证的几个关键点:

  1. 验证签名方法是否匹配
  2. 使用密钥验证签名
  3. 检查token是否有效(包括过期时间等)
  4. 将验证结果转换为自定义声明结构

2.3 JWT的错误处理机制

错误处理对于安全认证至关重要:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
if err != nil {
    // 使用 jwt/v5 的错误处理机制
    if errors.Is(err, jwt.ErrTokenMalformed) {
        return nil, ErrTokenMalformed
    } else if errors.Is(err, jwt.ErrTokenExpired) {
        return nil, ErrTokenExpired
    } else if errors.Is(err, jwt.ErrTokenNotValidYet) {
        return nil, ErrTokenNotValidYet
    } else {
        return nil, ErrTokenInvalid
    }
}

这部分展示了对各种JWT错误类型的精确处理:

  • 格式错误的token
  • 已过期的token
  • 尚未生效的token
  • 其他无效token情况

jwt/v5其实有完整的错误代码,在 error.go 中,如果你熟悉 jwt 机制,完全不需要自定义

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19

var (
	ErrInvalidKey                = errors.New("key is invalid")
	ErrInvalidKeyType            = errors.New("key is of invalid type")
	ErrHashUnavailable           = errors.New("the requested hash function is unavailable")
	ErrTokenMalformed            = errors.New("token is malformed")
	ErrTokenUnverifiable         = errors.New("token is unverifiable")
	ErrTokenSignatureInvalid     = errors.New("token signature is invalid")
	ErrTokenRequiredClaimMissing = errors.New("token is missing required claim")
	ErrTokenInvalidAudience      = errors.New("token has invalid audience")
	ErrTokenExpired              = errors.New("token is expired")
	ErrTokenUsedBeforeIssued     = errors.New("token used before issued")
	ErrTokenInvalidIssuer        = errors.New("token has invalid issuer")
	ErrTokenInvalidSubject       = errors.New("token has invalid subject")
	ErrTokenNotValidYet          = errors.New("token is not valid yet")
	ErrTokenInvalidId            = errors.New("token has invalid id")
	ErrTokenInvalidClaims        = errors.New("token has invalid claims")
	ErrInvalidType               = errors.New("invalid type for claim")
)

三、JWT高级特性实现

3.1 Token刷新机制

Token刷新是JWT管理的重要部分,尤其是处理过期token时:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func (j *JWT) RefreshToken(tokenString string) (string, int64, error) {
    // 先验证旧token,忽略过期时间
    claims, err := j.ValidateTokenWithoutExpiration(tokenString)
    if err != nil {
        return "", 0, err
    }

    // 创建新token
    return j.CreateToken(*claims)
}

func (j *JWT) ValidateTokenWithoutExpiration(tokenString string) (*JWTClaims, error) {
    token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (any, error) {
        return j.config.SigningKey, nil
    }, jwt.WithoutClaimsValidation())

    // 错误处理省略...

    if claims, ok := token.Claims.(*JWTClaims); ok {
        return claims, nil
    }
    return nil, ErrTokenInvalid
}

刷新机制的核心思想是:

  1. 验证旧token的签名,但忽略过期时间
  2. 保留原始claims中的用户数据
  3. 创建新token,更新过期时间

3.2 配置链式调用设计

代码采用了流式API设计,支持链式调用:

1
2
3
4
// 链式调用示例
jwt := NewJWT("your-secret-key").
    WithExpireDuration(time.Hour * 24).
    WithIssuer("api.example.com")

这种设计让JWT配置更加灵活,测试代码中也有相应展示:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
// 测试组合配置选项
func TestCombinedOptions(t *testing.T) {
    customDuration := time.Hour * 12
    customIssuer := "combined-test-issuer"

    j := NewJWT(testSigningKey).
        WithExpireDuration(customDuration).
        WithIssuer(customIssuer)

    // 测试逻辑省略...
}

四、JWT安全性深度剖析

通过jwt_test.go中的测试用例,我们可以深入理解JWT的安全性考虑:

4.1 无效Token处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func TestInvalidToken(t *testing.T) {
    j := NewJWT(testSigningKey)

    // 1. 格式错误的 Token
    _, err := j.ParseToken("invalid-token-format")
    assert.Error(t, err)
    assert.Equal(t, ErrTokenMalformed, err)

    // 2. 篡改的 Token
    claims := JWTClaims{
        UserID:   1,
        Username: testUsername,
    }
    token, _, _ := j.CreateToken(claims)
    tamperedToken := token + "tampered"
    _, err = j.ParseToken(tamperedToken)
    assert.Error(t, err)
    assert.Equal(t, ErrTokenInvalid, err)

    // 3. 使用不同密钥签名的 Token
    j2 := NewJWT("different-signing-key")
    token2, _, _ := j2.CreateToken(claims)
    _, err = j.ParseToken(token2)
    assert.Error(t, err)
    assert.Equal(t, ErrTokenInvalid, err)
}

测试用例验证了系统能正确处理三种无效token情况:

  1. 格式错误的token
  2. 被篡改的token
  3. 使用错误密钥签名的token

4.2 过期Token处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
func TestExpiredToken(t *testing.T) {
    // 创建一个超短过期时间的 JWT
    j := NewJWT(testSigningKey).WithExpireDuration(time.Millisecond * 100)

    claims := JWTClaims{
        UserID:   1,
        Username: testUsername,
    }

    token, _, err := j.CreateToken(claims)
    assert.NoError(t, err)

    // 等待 Token 过期
    time.Sleep(time.Millisecond * 200)

    // 验证 Token 已过期
    _, err = j.ParseToken(token)
    assert.Error(t, err)
    assert.Equal(t, ErrTokenExpired, err)
}

这个测试模拟了token过期场景,确保系统能正确识别和处理过期token。

五、JWT在实际项目中的应用

从提供的代码可以看出,JWT在实际项目中的典型应用流程:

  • 用户登录时创建JWT
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 创建JWT实例
jwt := utils.NewJWT(l.svcCtx.Config.Auth.AccessSecret)
// 设置过期时间
duration := time.Duration(l.svcCtx.Config.Auth.AccessExpire) * time.Second
jwt.WithExpireDuration(duration)

// 创建JWT声明
claims := utils.JWTClaims{
    UserID:   user.ID,
    Username: user.Username,
    Role:     strconv.Itoa(user.Role), // 将角色转换为字符串
}

// 生成令牌
token, expiresAt, err := jwt.CreateToken(claims)
  • API访问时验证JWT: 在中间件中验证token并提取用户信息
  • 刷新过期或即将过期的JWT: 使用RefreshToken机制延长用户会话

六、JWT最佳实践

基于代码分析,我们可以总结以下JWT最佳实践:

使用合适的过期时间:根据安全需求设置合理的过期时间

1
jwt.WithExpireDuration(time.Hour * 24) // 24小时有效期

实现刷新机制:允许无缝刷新过期token

1
newToken, _, err := jwt.RefreshToken(oldToken)

密钥保护:使用环境变量或安全服务存储签名密钥

1
2
// 从配置中读取密钥,而非硬编码
jwt := utils.NewJWT(l.svcCtx.Config.Auth.AccessSecret)

使用多种声明:包含足够信息用于权限验证,但避免敏感信息

1
2
3
4
5
claims := utils.JWTClaims{
    UserID:   user.ID,
    Username: user.Username,
    Role:     strconv.Itoa(user.Role),
}
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus