Skip to content

令牌刷新与轮换

CyberGo JWT 采用双层令牌设计:短期访问令牌(access token)用于接口认证,长期刷新令牌(refresh token)用于在访问令牌过期后换取新令牌。这种设计平衡了安全性与用户体验。

双层令牌模型

令牌类型签发方法默认 TTL用途
访问令牌Create15 分钟接口认证,频繁验证
刷新令牌CreateRefresh7 天换取新访问令牌,使用频率低

令牌类型通过 token_type 声明标记(access / refresh)。Refresh 方法会拒绝 access 类型的令牌,防止访问令牌被用于刷新。

TTL 配置

go
cfg := jwt.DefaultConfig()
cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
cfg.AccessTokenTTL = 15 * time.Minute    // 访问令牌有效期
cfg.RefreshTokenTTL = 7 * 24 * time.Hour // 刷新令牌有效期(必须 > AccessTokenTTL)

约束

Config.Validate() 要求 RefreshTokenTTL > AccessTokenTTL,否则返回 ErrInvalidConfig

基本刷新流程

1. 签发令牌对

go
package main

import (
    "fmt"
    "time"

    "github.com/cybergodev/jwt"
)

func main() {
    cfg := jwt.DefaultConfig()
    cfg.SecretKey = "hmac-key-that-has-at-least-32-bytes!"
    cfg.AccessTokenTTL = 15 * time.Minute
    cfg.RefreshTokenTTL = 7 * 24 * time.Hour

    processor, err := jwt.New(cfg)
    if err != nil {
        panic(err)
    }
    defer processor.Close()

    claims := &jwt.Claims{UserID: "user123", Username: "alice"}

    // 访问令牌(短期)
    accessToken, err := processor.Create(claims)
    if err != nil {
        panic(err)
    }

    // 刷新令牌(长期)
    refreshToken, err := processor.CreateRefresh(claims)
    if err != nil {
        panic(err)
    }

    fmt.Println("Access Token:", accessToken)
    fmt.Println("Refresh Token:", refreshToken)
}

2. 刷新获取新访问令牌

当访问令牌过期后,用刷新令牌换取新的访问令牌:

go
// refreshToken 是之前通过 CreateRefresh 签发的令牌
newAccessToken, err := processor.Refresh(refreshToken)
if err != nil {
    switch {
    case errors.Is(err, jwt.ErrTokenExpired):
        // 刷新令牌已过期,用户需要重新登录
    case errors.Is(err, jwt.ErrTokenRevoked):
        // 刷新令牌已被吊销
    case errors.Is(err, jwt.ErrTokenTypeMismatch):
        // 传入了访问令牌而非刷新令牌
    default:
        // 其他错误
    }
    return
}
fmt.Println("New Access Token:", newAccessToken)

Refresh 会完整校验刷新令牌:签名、过期、签发者、受众、黑名单状态。

自定义 Claims 刷新

使用自定义 Claims 类型时,用 RefreshInto 将解析结果填充到自定义结构:

go
type MyClaims struct {
    UserID string `json:"user_id"`
    Role   string `json:"role"`
    jwt.RegisteredClaims
}

func (c *MyClaims) GetRegisteredClaims() *jwt.RegisteredClaims {
    return &c.RegisteredClaims
}

func (c *MyClaims) Validate() error {
    if c.UserID == "" {
        return errors.New("user_id is required")
    }
    return nil
}
go
// 用自定义 Claims 签发刷新令牌
refreshToken, err := processor.CreateRefresh(&MyClaims{UserID: "123", Role: "admin"})

// 刷新到自定义结构
result := &MyClaims{}
newToken, err := processor.RefreshInto(refreshToken, result)

轮换策略

重用模式(默认)

默认情况下,Refresh 不会吊销原刷新令牌。原令牌在过期或被显式吊销前保持有效,可以多次使用:

go
// 第一次刷新
token1, err := processor.Refresh(refreshToken)
if err != nil {
    panic(err)
}

// 同一个 refreshToken 仍然有效,可以再次刷新
token2, err := processor.Refresh(refreshToken)
if err != nil {
    panic(err)
}

适用场景:移动端 App、单设备登录。用户无需频繁重新登录,刷新令牌可在 TTL 内反复使用。

一次性轮换

对安全性要求更高的场景,每次刷新后立即吊销旧刷新令牌,实现一次性使用:

go
// 刷新并立即吊销旧令牌
newAccessToken, err := processor.Refresh(refreshToken)
if err != nil {
    panic(err)
}

// 吊销旧刷新令牌,使其不可再次使用
if err := processor.Revoke(refreshToken); err != nil {
    panic(err)
}

适用场景:Web 应用、高安全要求系统。每次刷新后旧令牌立即失效,降低令牌泄露风险。

策略对比

维度重用模式一次性轮换
安全性较低(令牌泄露后可反复使用)较高(泄露令牌仅限单次使用)
用户体验较好(无需频繁重新登录)一般(刷新失败需重新登录)
实现复杂度零额外代码需调用 Revoke
黑名单压力较高(每次刷新产生一条记录)

刷新令牌泄露检测

一次性轮换模式下,如果攻击者使用了已被吊销的刷新令牌,Refresh 会返回 ErrTokenRevoked。应用层可据此检测令牌泄露并强制用户重新认证。

类型安全

CyberGo JWT 通过 token_type 声明区分令牌类型。RefreshRefreshInto 会拒绝访问令牌:

go
// 用访问令牌尝试刷新会被拒绝
_, err := processor.Refresh(accessToken)
// err 包装了 ErrTokenTypeMismatch: expected refresh token, got access token

这防止了访问令牌被用于获取新令牌,确保双层模型的类型隔离。

没有 token_type 声明的令牌(旧版本签发的令牌)会被接受,保持向后兼容。

安全注意事项

  • Refresh 不自动吊销:原刷新令牌在 Refresh 后仍然有效,需要手动调用 Revoke 实现一次性轮换。
  • Claims 不深度校验Refresh 验证标准 JWT 字段(签名、过期、签发者、受众、黑名单)和基本结构(UserID 或 Username 非空),但不重新检查字段长度限制和注入模式,信任创建时已校验。
  • 签名一致性:新访问令牌使用与刷新令牌相同的签名算法和密钥,不支持跨算法刷新。

下一步