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 Key | JWT |
|---|---|---|
| 用途 | 开发者调用 API | 用户登录 |
| 有效期 | 长期或永不过期 | 短期(需要刷新) |
| 撤销 | 需要查数据库 | 黑名单或短过期时间 |
| 传递方式 | Header 或 Query | Header(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 Keyak_:API Keyrk_:Read Keywk_: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 需要安全生成、存储和验证。
这一节涉及到的几个实践:
- Key 生成:使用
crypto.randomBytes,添加前缀标识 - Key 存储:只存储哈希值,不存储明文
- Key 验证:从 Header 获取,查数据库或缓存
- Key 管理:列出、撤销、轮换
- 权限控制:基于 Key 的权限和配额
- AI 项目:模仿 OpenAI 的 API Key 设计
API Key 是 AI API 服务的标准认证方式。安全生成、安全存储、权限控制、配额限制是关键。
下一篇看 Bearer Token 认证——与 API Key 的区别、临时 Token、刷新机制。