Skip to content

Token 管理详解

text
相关代码: server/tokenManager.ts

设计目标

  1. 按需创建 - 只在需要时才创建用户 Token
  2. 缓存复用 - 同一用户多次请求复用 Token
  3. 自动清理 - 过期 Token 及时删除
  4. 重启安全 - 服务重启后清理残留 Token

数据结构

TokenEntry

typescript
interface TokenEntry {
  userId: string   // 用户 ID
  aid: string      // AccessToken ID(Ghippo 返回)
  token: string    // 实际 Token 值
  expiry: number   // 过期时间戳(ms)
}

文件格式

json
{
  "user123": {
    "userId": "user123",
    "aid": "accesstoken-abc123",
    "expiry": 1705123456000
  }
}

安全设计

文件不存储实际 token,只存储 aid。用途是重启时清理 Ghippo 侧的 Token 资源。

核心流程

getOrCreateToken

代码实现

typescript
async getOrCreateToken(userId: string): Promise<string> {
  // 1. 检查缓存
  const cached = tokenCache.get(userId)
  if (cached && cached.expiry > Date.now()) {
    return cached.token
  }

  // 2. 创建新 Token
  const ttlHours = parseInt(process.env.USER_TOKEN_TTL_HOURS || '1', 10)
  const expiry = Date.now() + ttlHours * 60 * 60 * 1000
  const result = await httpRequest(
    apiBaseUrl,
    'POST',
    `/apis/ghippo.io/v1alpha1/users/${userId}/accesstoken`,
    adminToken,
    { name: `pitstop-auto-${Date.now()}`, expiredAt: String(expiry) }
  )

  // 3. 缓存
  const entry = { userId, aid: result.id, token: result.token, expiry }
  tokenCache.set(userId, entry)
  appendToFile(userId, { userId, aid: result.id, expiry })

  return result.token
}

清理机制

定时清理

typescript
// 每 10 分钟执行一次
setInterval(() => this.cleanup(), 10 * 60 * 1000)

async cleanup() {
  const now = Date.now()
  for (const [userId, entry] of tokenCache.entries()) {
    if (entry.expiry <= now) {
      // 调用 Ghippo 删除 Token
      await httpRequest(
        apiBaseUrl,
        'DELETE',
        `/apis/ghippo.io/v1alpha1/users/${userId}/accesstokens/${entry.aid}`,
        adminToken
      )
      tokenCache.delete(userId)
      removeFromFile(userId)
    }
  }
}

重启清理

typescript
async init(baseUrl: string, token: string) {
  // 读取上次残留的 Token 记录
  const records = loadTokensFile()

  for (const userId of Object.keys(records)) {
    const record = records[userId]
    // 调用 Ghippo 删除
    await httpRequest(
      apiBaseUrl,
      'DELETE',
      `/apis/ghippo.io/v1alpha1/users/${userId}/accesstokens/${record.aid}`,
      adminToken
    )
  }

  // 清空文件
  saveTokensFile({})
}

配置项

环境变量默认值说明
USER_TOKEN_TTL_HOURS1Token 有效期(小时)

监控日志

[TokenManager] Initializing...
[TokenManager] Cleaning up 3 tokens from previous run
[TokenManager] Creating token for user user123
[TokenManager] Using cached token for user user123, expires in 45 min
[TokenManager] Running cleanup...
[TokenManager] Token expired for user user456

设计权衡

为什么用文件而不是 Redis?

  1. 部署简单 - 单节点部署无需额外依赖
  2. 数据量小 - 通常只有几十个活跃用户
  3. 一致性要求低 - 最坏情况是 Token 泄漏(短生命周期,影响有限)

为什么不在 Ghippo 查询现有 Token?

  1. Ghippo 没有批量查询用户 Token 的 API
  2. 逐个查询开销大
  3. 文件记录是可信的(我们自己创建的)

已知问题

  1. 多实例部署 - 当前设计不支持,需要引入 Redis
  2. Token 泄漏 - 服务崩溃时可能遗留 Token(重启会清理)