主题
Token 管理详解
text
相关代码: server/tokenManager.ts设计目标
- 按需创建 - 只在需要时才创建用户 Token
- 缓存复用 - 同一用户多次请求复用 Token
- 自动清理 - 过期 Token 及时删除
- 重启安全 - 服务重启后清理残留 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_HOURS | 1 | Token 有效期(小时) |
监控日志
[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?
- 部署简单 - 单节点部署无需额外依赖
- 数据量小 - 通常只有几十个活跃用户
- 一致性要求低 - 最坏情况是 Token 泄漏(短生命周期,影响有限)
为什么不在 Ghippo 查询现有 Token?
- Ghippo 没有批量查询用户 Token 的 API
- 逐个查询开销大
- 文件记录是可信的(我们自己创建的)
已知问题
- 多实例部署 - 当前设计不支持,需要引入 Redis
- Token 泄漏 - 服务崩溃时可能遗留 Token(重启会清理)