API Key 认证

要点

  • API Key 是服务端到服务端的认证方式,适合开发者调用 API
  • API Key 通常是长字符串,通过 Header 或 Query 传递
  • Key 需要安全生成、存储和验证,不能明文存储
  • Key 应该关联权限、配额和过期时间
  • Key 泄露后的撤销机制是安全的关键
  • AI 项目的 API 服务(类似 OpenAI API)需要 API Key 认证

1. API Key 特点

1.1 与 JWT 的区别

特性API KeyJWT
用途开发者调用 API用户登录
有效期长期或永不过期短期(需要刷新)
撤销需要查数据库黑名单或短过期时间
传递方式Header 或 QueryHeader(Bearer)
权限关联角色和配额自包含

1.2 使用场景

  • 开发者调用 AI API(如 OpenAI、Anthropic)
  • 服务间调用(Worker 调用业务 API)
  • CLI 工具调用
  • 第三方集成

2. Key 生成

2.1 安全生成

使用 crypto.randomBytes 生成安全的随机字符串:

import { randomBytes } from 'crypto'
 
function generateApiKey(): string {
  // 生成 32 字节随机数,转换为 hex 字符串
  return randomBytes(32).toString('hex')
}
 
// 生成: 7f9a8b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8
const apiKey = generateApiKey()

2.2 前缀标识

为了方便识别和调试,可以添加前缀:

function generateApiKey(prefix: string = 'sk'): string {
  const key = randomBytes(32).toString('hex')
  return `${prefix}_${key}`
}
 
// 生成: sk_7f9a8b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8
const apiKey = generateApiKey('sk')

常见前缀:

  • sk_:Secret Key(如 OpenAI)
  • pk_:Public Key
  • ak_:API Key
  • rk_:Read Key
  • wk_:Write Key

2.3 可读格式

如果需要用户手动输入,可以使用更易读的格式:

function generateReadableKey(): string {
  const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789'  // 排除易混淆字符
  const segments = 5
  const segmentLength = 4
 
  const segments = Array.from({ length: segments }, () => {
    return Array.from({ length: segmentLength }, () => {
      return chars[Math.floor(Math.random() * chars.length)]
    }).join('')
  })
 
  return segments.join('-')
}
 
// 生成: A3K7-B9M2-P4Q8-R6T1-W5X3
const readableKey = generateReadableKey()

3. Key 存储

3.1 数据库 Schema

export const apiKeys = pgTable('api_keys', {
  id: uuid('id').primaryKey().defaultRandom(),
  userId: uuid('user_id').notNull().references(() => users.id),
  name: text('name').notNull(),  // 用户给 Key 起的名字
  keyHash: text('key_hash').notNull().unique(),  // 存储哈希值
  keyPrefix: text('key_prefix').notNull(),  // 前 8 位,用于显示
  permissions: jsonb('permissions').default(['all']).notNull(),  // 权限列表
  rateLimit: integer('rate_limit').default(1000),  // 每分钟请求数
  expiresAt: timestamp('expires_at'),  // 过期时间
  lastUsedAt: timestamp('last_used_at'),
  createdAt: timestamp('created_at').defaultNow().notNull(),
})

3.2 安全存储

API Key 应该像密码一样存储,只保存哈希值:

import { createHash } from 'crypto'
 
function hashApiKey(key: string): string {
  return createHash('sha256').update(key).digest('hex')
}
 
// 创建 API Key
app.post('/api-keys', async (c) => {
  const user = c.get('user')
  const { name, permissions } = await c.req.json()
 
  // 生成 Key
  const apiKey = generateApiKey('sk')
 
  // 存储哈希值
  await db.insert(apiKeys).values({
    userId: user.id,
    name,
    keyHash: hashApiKey(apiKey),
    keyPrefix: apiKey.substring(0, 8),
    permissions: permissions || ['all'],
  })
 
  // 只在创建时返回完整 Key
  return c.json({
    apiKey,  // 只在这里返回一次
    name,
    permissions,
    createdAt: new Date(),
  })
})

3.3 为什么存储哈希值

如果数据库泄露,攻击者无法恢复 API Key。如果存储明文,攻击者可以直接使用。

4. Key 验证

4.1 验证逻辑

import { createMiddleware } from 'hono/factory'
 
type ApiKeyVariables = {
  apiKey: {
    id: string
    userId: string
    permissions: string[]
  }
}
 
const apiKeyAuth = createMiddleware<{ Variables: ApiKeyVariables }>(
  async (c, next) => {
    // 从 Header 获取 Key
    const authHeader = c.req.header('Authorization')
 
    if (!authHeader?.startsWith('Bearer ')) {
      return c.json({ error: 'Missing API key' }, 401)
    }
 
    const apiKey = authHeader.substring(7)
 
    // 查询 Key
    const keyHash = hashApiKey(apiKey)
    const key = await db.query.apiKeys.findFirst({
      where: eq(apiKeys.keyHash, keyHash),
    })
 
    if (!key) {
      return c.json({ error: 'Invalid API key' }, 401)
    }
 
    // 检查是否过期
    if (key.expiresAt && key.expiresAt < new Date()) {
      return c.json({ error: 'API key expired' }, 401)
    }
 
    // 更新最后使用时间
    await db
      .update(apiKeys)
      .set({ lastUsedAt: new Date() })
      .where(eq(apiKeys.id, key.id))
 
    // 设置上下文
    c.set('apiKey', {
      id: key.id,
      userId: key.userId,
      permissions: key.permissions as string[],
    })
 
    await next()
  }
)

4.2 性能优化

每次请求都计算哈希值并查询数据库,性能开销大。可以优化:

缓存前缀:先根据前缀缩小查询范围:

const keyPrefix = apiKey.substring(0, 8)
 
const keys = await db.query.apiKeys.findMany({
  where: eq(apiKeys.keyPrefix, keyPrefix),
})
 
// 遍历匹配的 Key
for (const key of keys) {
  if (key.keyHash === hashApiKey(apiKey)) {
    // 找到匹配的 Key
    c.set('apiKey', { ... })
    await next()
    return
  }
}

Redis 缓存:把 Key 哈希缓存到 Redis:

// 验证时先查缓存
const cached = await redis.get(`apikey:${hashApiKey(apiKey)}`)
 
if (cached) {
  const keyData = JSON.parse(cached)
  c.set('apiKey', keyData)
  await next()
  return
}
 
// 缓存未命中,查数据库
const key = await db.query.apiKeys.findFirst({
  where: eq(apiKeys.keyHash, hashApiKey(apiKey)),
})
 
if (!key) {
  return c.json({ error: 'Invalid API key' }, 401)
}
 
// 缓存到 Redis,1 小时过期
await redis.setex(
  `apikey:${hashApiKey(apiKey)}`,
  3600,
  JSON.stringify({
    id: key.id,
    userId: key.userId,
    permissions: key.permissions,
  })
)
 
c.set('apiKey', { ... })
await next()

5. Key 管理

5.1 列出 Key

app.get('/api-keys', async (c) => {
  const user = c.get('user')
 
  const keys = await db.query.apiKeys.findMany({
    where: eq(apiKeys.userId, user.id),
    columns: {
      id: true,
      name: true,
      keyPrefix: true,
      permissions: true,
      lastUsedAt: true,
      createdAt: true,
    },
  })
 
  return c.json({
    apiKeys: keys.map((k) => ({
      id: k.id,
      name: k.name,
      key: `${k.keyPrefix}...`,  // 只显示前缀
      permissions: k.permissions,
      lastUsedAt: k.lastUsedAt,
      createdAt: k.createdAt,
    })),
  })
})

5.2 撤销 Key

app.delete('/api-keys/:id', async (c) => {
  const user = c.get('user')
  const keyId = c.req.param('id')
 
  // 删除 Key
  await db
    .delete(apiKeys)
    .where(and(
      eq(apiKeys.id, keyId),
      eq(apiKeys.userId, user.id)
    ))
 
  // 清除缓存
  // 注意:需要知道 Key 哈希才能清除,可以维护一个映射
 
  return c.json({ message: 'API key revoked' })
})

5.3 轮换 Key

定期轮换 Key 是安全最佳实践:

app.post('/api-keys/:id/rotate', async (c) => {
  const user = c.get('user')
  const keyId = c.req.param('id')
 
  // 查找旧 Key
  const oldKey = await db.query.apiKeys.findFirst({
    where: and(
      eq(apiKeys.id, keyId),
      eq(apiKeys.userId, user.id)
    ),
  })
 
  if (!oldKey) {
    return c.json({ error: 'API key not found' }, 404)
  }
 
  // 生成新 Key
  const newApiKey = generateApiKey('sk')
 
  // 更新 Key
  await db
    .update(apiKeys)
    .set({
      keyHash: hashApiKey(newApiKey),
      keyPrefix: newApiKey.substring(0, 8),
    })
    .where(eq(apiKeys.id, keyId))
 
  // 只在轮换时返回新 Key
  return c.json({
    apiKey: newApiKey,
    message: 'API key rotated successfully',
  })
})

6. 权限控制

6.1 基于 Key 的权限

// 检查 Key 是否有特定权限
function requireApiKeyPermission(permission: string) {
  return async (c: Context, next: Next) => {
    const apiKey = c.get('apiKey')
 
    if (!apiKey.permissions.includes(permission) && !apiKey.permissions.includes('all')) {
      return c.json({ error: 'Insufficient permissions' }, 403)
    }
 
    await next()
  }
}
 
// 使用
app.post('/v1/chat/completions',
  apiKeyAuth,
  requireApiKeyPermission('chat:write'),
  async (c) => {
    // 调用 AI API
  }
)

6.2 配额限制

async function checkApiKeyQuota(apiKeyId: string) {
  const now = new Date()
  const windowStart = new Date(now.getTime() - 60 * 1000)  // 1 分钟窗口
 
  // 统计请求数
  const count = await db
    .select({ count: sql<number>`count(*)` })
    .from(apiRequests)
    .where(and(
      eq(apiRequests.apiKeyId, apiKeyId),
      gte(apiRequests.createdAt, windowStart)
    ))
 
  const key = await db.query.apiKeys.findFirst({
    where: eq(apiKeys.id, apiKeyId),
  })
 
  if (count[0].count >= (key?.rateLimit ?? 1000)) {
    throw new Error('Rate limit exceeded')
  }
}
 
// 记录请求
async function recordApiRequest(apiKeyId: string) {
  await db.insert(apiRequests).values({
    apiKeyId,
    createdAt: new Date(),
  })
}

7. AI 项目的 API Key

7.1 OpenAI 风格

AI 项目的 API 通常模仿 OpenAI 的 API Key 设计:

// 请求
POST /v1/chat/completions
Authorization: Bearer sk-proj-abc123...
 
{
  "model": "gpt-4",
  "messages": [
    { "role": "user", "content": "Hello!" }
  ]
}
 
// 响应
{
  "id": "chatcmpl-123",
  "object": "chat.completion",
  "created": 1677652288,
  "model": "gpt-4",
  "choices": [{
    "index": 0,
    "message": {
      "role": "assistant",
      "content": "Hello! How can I help you today?"
    },
    "finish_reason": "stop"
  }],
  "usage": {
    "prompt_tokens": 9,
    "completion_tokens": 12,
    "total_tokens": 21
  }
}

7.2 计费

根据 API Key 的使用量计费:

async function billApiKeyUsage(apiKeyId: string, usage: {
  promptTokens: number
  completionTokens: number
  model: string
}) {
  const pricing = await getModelPricing(usage.model)
 
  const cost =
    (usage.promptTokens / 1000) * pricing.promptPricePer1K +
    (usage.completionTokens / 1000) * pricing.completionPricePer1K
 
  // 扣费
  await db
    .update(userBalance)
    .set({
      balance: sql`balance - ${cost}`,
    })
    .where(eq(userBalance.userId, apiKey.userId))
}

总结

API Key 是服务端到服务端的认证方式,适合开发者调用 API。Key 需要安全生成、存储和验证。

这一节涉及到的几个实践:

  1. Key 生成:使用 crypto.randomBytes,添加前缀标识
  2. Key 存储:只存储哈希值,不存储明文
  3. Key 验证:从 Header 获取,查数据库或缓存
  4. Key 管理:列出、撤销、轮换
  5. 权限控制:基于 Key 的权限和配额
  6. AI 项目:模仿 OpenAI 的 API Key 设计

API Key 是 AI API 服务的标准认证方式。安全生成、安全存储、权限控制、配额限制是关键。

下一篇看 Bearer Token 认证——与 API Key 的区别、临时 Token、刷新机制。