19.08-API Key保护

要点

  • API Key 是服务间认证最常用的方式——简单、无状态、适合服务端到服务端通信
  • API Key 泄露是最常见的安全事故之一——GitHub 提交、日志输出、前端代码、错误信息都可能泄露 Key
  • 保护原则:Key 只存在服务端环境变量里、前端永远不持有服务端 Key、Key 按服务/用途独立分配
  • Cloudflare Workers 的 Secrets 机制专门保护 API Key——加密存储、不会出现在日志和调试输出里
  • Key 轮转策略:定期更换 + 支持新旧 Key 同时有效(平滑过渡)
  • 给第三方发的 API Key 需要权限控制和限流——不是所有 Key 都一样

1. API Key 的使用场景

API Key 适用于服务端到服务端的认证——一个服务调用另一个服务的 API。

你的 Worker → 调用 OpenAI API(Authorization: Bearer sk-xxx)
你的 Worker → 调用 Cloudflare API(X-Auth-Key: xxx)
第三方服务 → 调用你的 API(X-API-Key: xxx)

API Key 不适合用户级认证——用户登录应该用 JWT 或 Session,不应该给每个用户发 API Key。

1.1 API Key vs JWT

维度API KeyJWT
持有者服务/应用用户
生命周期长期有效(手动轮转)短期有效(自动过期)
粒度服务级(整个服务共用一个 Key)用户级(每个用户一个 token)
撤销需要更新 Key(影响所有使用该 Key 的请求)黑名单或直接过期
传输方式Header(Authorization / X-API-KeyHeader(Authorization: Bearer
适用场景服务间调用、第三方 API 访问用户登录、前端认证

2. API Key 的常见泄露途径

2.1 代码仓库

最常见的泄露方式——把 API Key 硬编码在代码里,提交到 GitHub:

// 绝对不要这样做
const OPENAI_KEY = 'sk-xxxxxxxxxxxxxxxxxxxxxxxx'
# .gitignore 必须包含的文件
.env
.env.local
.env.*.local
# 更安全的做法:用 pre-commit hook 检测密钥
# 安装 gitleaks 或 git-secrets
git-secrets --register-aws
git-secrets --add 'sk-[a-zA-Z0-9]{20,}'

2.2 前端代码

前端代码对用户完全可见——任何放在前端的 Key 都是公开的

// 危险:前端代码里的 API Key
// src/app/page.tsx(客户端组件)
'use client'
const API_KEY = 'sk-xxx'  // ← 用户打开 DevTools 就能看到

正确做法:前端只调你自己的后端 API,后端再带 Key 调第三方服务。

// 正确:前端 → 你的后端 → 第三方服务
// 前端代码
fetch('/api/ai/chat', {
  method: 'POST',
  body: JSON.stringify({ prompt: '...' }),
})
 
// 你的 Worker(Server Component 或 Route Handler)
const OPENAI_KEY = process.env.OPENAI_KEY  // 只在服务端
await fetch('https://api.openai.com/v1/chat/completions', {
  headers: { 'Authorization': `Bearer ${OPENAI_KEY}` },
})

2.3 日志和错误信息

API Key 出现在日志里——开发者排查问题时看日志,Key 就泄露了:

// 危险:日志里打印了完整 Key
console.log('Calling API with key:', apiKey)
console.error('API error:', error, { key: apiKey, response: data })
// 正确:只打印 Key 的前几位
console.log('Calling API with key:', apiKey.slice(0, 8) + '...')
// 输出:Calling API with key: sk-abc12...

2.4 URL 参数

API Key 放在 URL 参数里,可能被浏览器历史记录、代理日志、Referer 头记录:

// 危险:Key 在 URL 里
fetch(`https://api.example.com/data?api_key=${apiKey}`)
 
// 正确:Key 在 Header 里
fetch('https://api.example.com/data', {
  headers: { 'X-API-Key': apiKey },
})

2.5 错误响应

服务端错误响应里泄露了 API Key 或类似的内部信息:

// 危险
try {
  await callExternalAPI(apiKey)
} catch (e) {
  return c.json({
    error: e.message,       // ← 可能包含 Key
    apiKey: apiKey,         // ← 直接泄露
    stack: e.stack,
  }, 500)
}

3. Cloudflare Workers Secrets

Workers 提供了专门的 Secrets 机制保护 API Key:

3.1 什么是 Secrets

Secrets 是环境变量的一种特殊类型:

  • 加密存储——代码和配置里看不到明文
  • 不会出现在日志里——console.log(env.API_KEY) 输出 [redacted]
  • 不能通过 API 读取——只能通过 Worker 运行时访问
  • 按环境隔离——production、staging、preview 各有独立的 Secrets

3.2 设置 Secrets

# 通过 wrangler CLI
wrangler secret put OPENAI_API_KEY
# 交互式输入 Key 的值
 
# 查看已设置的 Secrets 列表(只显示名字,不显示值)
wrangler secret list
 
# 删除 Secret
wrangler secret delete OPENAI_API_KEY

或者通过 Cloudflare Dashboard → Workers → Settings → Variables and Secrets → Add Secret。

3.3 在代码中使用

// wrangler.toml
# 声明 Secrets(值在 Cloudflare 里设置)
# [secrets]
# OPENAI_API_KEY
# CLOUDFLARE_R2_TOKEN
 
// src/worker.ts
export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // env 里有 Secrets,类型声明在 Env 接口里
    const apiKey = env.OPENAI_API_KEY
 
    const response = await fetch('https://api.openai.com/v1/chat/completions', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
      },
      body: JSON.stringify({ /* ... */ }),
    })
 
    return response
  }
}
 
// Env 类型
interface Env {
  OPENAI_API_KEY: string       // Secret
  CLOUDFLARE_R2_TOKEN: string  // Secret
  R2_BUCKET: R2Bucket          // Binding
}

3.4 Secrets vs 普通环境变量

特性Secrets普通环境变量
存储加密明文
日志输出[redacted]明文显示
Dashboard 可见只能看到名字能看到值
wrangler.toml不放值放值
适用API Key、数据库密码、JWT Secret功能开关、域名、配置值

原则:所有敏感值都用 Secrets,非敏感值用普通环境变量。

4. API Key 设计(给第三方发的 Key)

如果你的 API 要给第三方开发者发 API Key,需要考虑:

4.1 Key 生成

// 生成 API Key
function generateAPIKey(): string {
  // 前缀标识来源 + 随机字符串
  const prefix = 'ak'
  const random = crypto.randomUUID().replace(/-/g, '')
  return `${prefix}_${random}`
  // 输出:ak_550e8400e29b41d4a716446655440000
}
 
// 存储 Key 的 hash(不存明文)
async function hashAPIKey(key: string): Promise<string> {
  const buffer = await crypto.subtle.digest(
    'SHA-256',
    new TextEncoder().encode(key)
  )
  return Array.from(new Uint8Array(buffer))
    .map(b => b.toString(16).padStart(2, '0'))
    .join('')
}

4.2 Key 验证中间件

// API Key 认证中间件
async function apiKeyAuth(c: Context, next: Next) {
  const apiKey = c.req.header('X-API-Key')
  if (!apiKey) {
    return c.json({ error: 'Missing API Key' }, 401)
  }
 
  // 用 Key 的前 8 位快速查找(避免全表 hash)
  const prefix = apiKey.slice(0, 8)
  const records = await c.env.DB
    .prepare('SELECT * FROM api_keys WHERE key_prefix = ? AND is_active = 1')
    .bind(prefix)
    .all()
 
  // 逐个校验 hash
  for (const record of records.results) {
    const hash = await hashAPIKey(apiKey)
    if (hash === record.key_hash) {
      c.set('apiKeyOwner', record)
      return next()
    }
  }
 
  return c.json({ error: 'Invalid API Key' }, 401)
}

4.3 Key 权限控制

不同的 Key 可以有不同的权限:

// D1 schema
CREATE TABLE api_keys (
  id INTEGER PRIMARY KEY,
  key_prefix TEXT NOT NULL,
  key_hash TEXT NOT NULL,
  name TEXT NOT NULL,           -- "Production", "Testing"
  permissions TEXT NOT NULL,    -- JSON: ["read", "write"]
  rate_limit INTEGER DEFAULT 1000,  -- 每分钟请求数
  is_active INTEGER DEFAULT 1,
  created_at TEXT DEFAULT (datetime('now')),
  expires_at TEXT               -- NULL = 永不过期
);
 
// 权限检查
async function checkPermission(c: Context, permission: string) {
  const keyOwner = c.get('apiKeyOwner')
  const permissions = JSON.parse(keyOwner.permissions)
  if (!permissions.includes(permission)) {
    throw new Error(`API Key does not have ${permission} permission`)
  }
}
 
// 使用
app.post('/api/data', apiKeyAuth, async (c) => {
  try {
    await checkPermission(c, 'write')
  } catch (e) {
    return c.json({ error: e.message }, 403)
  }
 
  // 业务逻辑...
})

4.4 Key 限流

不同 Key 可以有不同的限流配额:

// API Key 限流中间件(在认证之后)
async function apiKeyRateLimit(c: Context, next: Next) {
  const keyOwner = c.get('apiKeyOwner')
  const key = `ratelimit:${keyOwner.id}:${Math.floor(Date.now() / 60000)}`
 
  const count = parseInt(
    (await c.env.RATE_LIMIT_KV.get(key)) || '0'
  )
 
  if (count >= keyOwner.rate_limit) {
    c.header('Retry-After', '60')
    return c.json({ error: 'Rate limit exceeded' }, 429)
  }
 
  await c.env.RATE_LIMIT_KV.put(key, String(count + 1), {
    expirationTtl: 120,
  })
 
  c.header('X-RateLimit-Limit', String(keyOwner.rate_limit))
  c.header('X-RateLimit-Remaining', String(keyOwner.rate_limit - count - 1))
  await next()
}

5. Key 轮转

5.1 为什么需要轮转

  • Key 可能已经泄露但不知道
  • 定期轮转是安全合规的要求
  • 员工离职,需要更换共用的 Key

5.2 平滑轮转策略

直接更换 Key 会导致所有使用该 Key 的服务立即中断。正确做法:

  1. 生成新 Key
  2. 新旧 Key 同时有效(过渡期)
  3. 通知所有使用方切换到新 Key
  4. 过渡期结束后停用旧 Key
// D1 schema:支持每个 Key 记录多个有效 hash
CREATE TABLE api_keys (
  id INTEGER PRIMARY KEY,
  key_prefix TEXT NOT NULL,
  key_hash TEXT NOT NULL,
  is_primary INTEGER DEFAULT 0,  -- 1 = 当前主 Key,0 = 过渡期旧 Key
  expires_at TEXT,
  is_active INTEGER DEFAULT 1
);
 
// 验证时接受新旧两个 Key
async function validateKey(key: string): Promise<{ valid: boolean; isPrimary: boolean }> {
  const prefix = key.slice(0, 8)
  const records = await db.prepare(
    'SELECT * FROM api_keys WHERE key_prefix = ? AND is_active = 1'
  ).bind(prefix).all()
 
  const hash = await hashAPIKey(key)
  for (const record of records.results) {
    if (hash === record.key_hash) {
      return { valid: true, isPrimary: record.is_primary === 1 }
    }
  }
  return { valid: false, isPrimary: false }
}
 
// 轮转流程
async function rotateKey(keyId: number): Promise<string> {
  // 1. 生成新 Key
  const newKey = generateAPIKey()
  const newHash = await hashAPIKey(newKey)
 
  // 2. 旧 Key 降级为过渡
  await db.prepare(
    'UPDATE api_keys SET is_primary = 0, expires_at = ? WHERE id = ?'
  ).bind(
    new Date(Date.now() + 30 * 86400000).toISOString(),  // 30 天过渡期
    keyId
  ).run()
 
  // 3. 插入新 Key
  await db.prepare(
    'INSERT INTO api_keys (key_prefix, key_hash, is_primary) VALUES (?, ?, 1)'
  ).bind(newKey.slice(0, 8), newHash).run()
 
  // 4. 返回新 Key(只在创建时返回一次)
  return newKey
}

5.3 自动过期

给 Key 设置过期时间,强制定期轮转:

// 每次验证时检查过期
if (record.expires_at && new Date(record.expires_at) < new Date()) {
  return c.json({ error: 'API Key expired' }, 401)
}

6. AI 服务 Key 的特殊考量

AI 应用的 Key 管理有一些特殊点:

6.1 多模型多 Key

一个 AI 应用可能用多个模型提供商,每个都有 Key:

interface Env {
  // AI 模型 Key
  OPENAI_API_KEY: string
  ANTHROPIC_API_KEY: string
  COHERE_API_KEY: string
  // 向量数据库 Key
  PINECONE_API_KEY: string
  // 监控 Key
  SENTRY_DSN: string
}

每个 Key 都是 Secrets,加密存储。

6.2 Key 使用量监控

AI 调用通常按 token 计费。需要监控每个 Key 的使用量:

// AI 调用中间件:记录 token 使用
async function aiUsageTracking(c: Context, next: Next) {
  await next()
 
  const usage = c.get('aiUsage')  // 业务代码里设置的 token 使用量
  if (usage) {
    const userId = c.get('user')?.id || 'anonymous'
    const date = new Date().toISOString().slice(0, 10)
 
    await c.env.DB.prepare(
      `INSERT INTO ai_usage (user_id, date, tokens, model)
       VALUES (?, ?, ?, ?)`
    ).bind(userId, date, usage.tokens, usage.model).run()
  }
}

6.3 Key 成本预算

设置每个 Key 的月度预算上限:

async function checkAIBudget(c: Context, next: Next) {
  const month = new Date().toISOString().slice(0, 7)  // "2026-06"
  const usage = await c.env.DB.prepare(
    'SELECT SUM(cost) as total FROM ai_usage WHERE month = ?'
  ).bind(month).first()
 
  const MONTHLY_BUDGET = 100  // 100 美元
  if ((usage?.total || 0) >= MONTHLY_BUDGET) {
    return c.json({ error: 'Monthly AI budget exceeded' }, 402)
  }
 
  await next()
}

7. API Key 安全检查清单

检查项说明
Key 存在 Secrets / 环境变量不硬编码在代码里
.gitignore 包含 .env防止环境文件提交到 Git
pre-commit hook 检测密钥gitleaks / git-secrets
前端不持有服务端 Key前端只调自己的后端
日志不打印完整 Key只打印前 8 位
Key 不在 URL 参数里用 Header 传 Key
错误响应不包含 Key通用错误信息,详情写日志
定期轮转新旧 Key 同时有效平滑过渡
按服务独立分配不同服务用不同 Key,泄露只影响一个
权限控制每个 Key 只给必要的权限
使用量监控异常使用及时发现
预算上限防止 Key 泄露后产生高额费用

8. 小结

API Key 泄露是最常见的安全事故。泄露途径:代码仓库、前端代码、日志、URL 参数、错误响应。

保护原则:Key 只存服务端 Secrets(Cloudflare Workers 加密存储)、前端永远不持有、按服务独立分配、日志只打印前几位。

给第三方发的 Key 需要权限控制(读写分离)、限流(每个 Key 独立配额)、平滑轮转(新旧 Key 同时有效过渡)。

AI 服务 Key 的特殊性:多模型多 Key、token 使用量监控、成本预算上限。

下一篇讲环境变量安全——比 API Key 更底层,环境变量是所有密钥的载体。