Better SaaS Docs
开发者指南

API Key开发指南

架构概述

API Key 系统由以下关键组件组成:

  • API Key 管理:创建、验证和撤销 API Key
  • 身份验证中间件:验证传入请求
  • 权限控制:基于角色的访问控制
  • 速率限制:防止滥用
  • 审计日志:跟踪 API 使用情况

数据库架构

API Key 系统使用以下数据库表:

API Key 表

CREATE TABLE api_keys (
  id TEXT PRIMARY KEY,
  user_id TEXT NOT NULL REFERENCES user(id),
  name TEXT NOT NULL,
  key_value TEXT NOT NULL UNIQUE,
  permissions JSONB DEFAULT '[]',
  last_used_at TIMESTAMP,
  expires_at TIMESTAMP,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_api_keys_key_value ON api_keys(key_value);
CREATE INDEX idx_api_keys_user_id ON api_keys(user_id);

API 使用日志表

CREATE TABLE api_usage_logs (
  id TEXT PRIMARY KEY,
  api_key_id TEXT NOT NULL REFERENCES api_keys(id),
  endpoint TEXT NOT NULL,
  method TEXT NOT NULL,
  status_code INTEGER NOT NULL,
  response_time_ms INTEGER,
  ip_address TEXT,
  user_agent TEXT,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_api_usage_logs_api_key_id ON api_usage_logs(api_key_id);
CREATE INDEX idx_api_usage_logs_created_at ON api_usage_logs(created_at);

核心服务

API Key 服务

主要的 API Key 服务处理所有 API Key 操作:

// src/lib/api-keys/api-key-service.ts
import { db } from '@/server/db'
import { apiKeys, apiUsageLogs } from '@/server/db/schema'
import { eq, and } from 'drizzle-orm'
import { v4 as uuidv4 } from 'uuid'
import crypto from 'crypto'

export class ApiKeyService {
  static generateApiKey(): string {
    const prefix = 'bs_'
    const randomBytes = crypto.randomBytes(32).toString('hex')
    return `${prefix}${randomBytes}`
  }

  static async createApiKey(
    userId: string,
    name: string,
    permissions: string[] = [],
    expiresAt?: Date
  ) {
    const keyValue = this.generateApiKey()
    const id = uuidv4()

    const [apiKey] = await db
      .insert(apiKeys)
      .values({
        id,
        userId,
        name,
        keyValue,
        permissions,
        expiresAt
      })
      .returning()

    return { ...apiKey, keyValue }
  }

  static async validateApiKey(keyValue: string) {
    const [apiKey] = await db
      .select()
      .from(apiKeys)
      .where(
        and(
          eq(apiKeys.keyValue, keyValue),
          // 检查是否过期
          apiKeys.expiresAt ? sql`expires_at > NOW()` : undefined
        )
      )
      .limit(1)

    if (!apiKey) {
      return null
    }

    // 更新最后使用时间
    await db
      .update(apiKeys)
      .set({ lastUsedAt: new Date() })
      .where(eq(apiKeys.id, apiKey.id))

    return apiKey
  }

  static async getUserApiKeys(userId: string) {
    return await db
      .select({
        id: apiKeys.id,
        name: apiKeys.name,
        permissions: apiKeys.permissions,
        lastUsedAt: apiKeys.lastUsedAt,
        expiresAt: apiKeys.expiresAt,
        createdAt: apiKeys.createdAt
      })
      .from(apiKeys)
      .where(eq(apiKeys.userId, userId))
      .orderBy(desc(apiKeys.createdAt))
  }

  static async deleteApiKey(userId: string, keyId: string) {
    await db
      .delete(apiKeys)
      .where(
        and(
          eq(apiKeys.id, keyId),
          eq(apiKeys.userId, userId)
        )
      )
  }

  static async logApiUsage(
    apiKeyId: string,
    endpoint: string,
    method: string,
    statusCode: number,
    responseTimeMs: number,
    ipAddress?: string,
    userAgent?: string
  ) {
    await db.insert(apiUsageLogs).values({
      id: uuidv4(),
      apiKeyId,
      endpoint,
      method,
      statusCode,
      responseTimeMs,
      ipAddress,
      userAgent
    })
  }
}

身份验证中间件

用于验证 API 请求的中间件:

// src/middleware/api-auth.ts
import { NextRequest, NextResponse } from 'next/server'
import { ApiKeyService } from '@/lib/api-keys/api-key-service'

export async function apiAuthMiddleware(
  request: NextRequest,
  requiredPermissions: string[] = []
) {
  const authHeader = request.headers.get('authorization')
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return NextResponse.json(
      { error: '缺少或无效的授权头' },
      { status: 401 }
    )
  }

  const apiKey = authHeader.substring(7) // 移除 'Bearer ' 前缀
  const validatedKey = await ApiKeyService.validateApiKey(apiKey)

  if (!validatedKey) {
    return NextResponse.json(
      { error: '无效或过期的 API Key' },
      { status: 401 }
    )
  }

  // 检查权限
  if (requiredPermissions.length > 0) {
    const hasPermission = requiredPermissions.every(permission =>
      validatedKey.permissions.includes(permission)
    )

    if (!hasPermission) {
      return NextResponse.json(
        { error: '权限不足' },
        { status: 403 }
      )
    }
  }

  // 将 API Key 信息添加到请求中
  request.apiKey = validatedKey
  
  return null // 继续处理请求
}

API 端点

创建 API Key

// src/app/api/api-keys/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { auth } from '@/server/auth'
import { ApiKeyService } from '@/lib/api-keys/api-key-service'

export async function POST(request: NextRequest) {
  const session = await auth.api.getSession({ headers: request.headers })
  
  if (!session?.user) {
    return NextResponse.json({ error: '未授权' }, { status: 401 })
  }

  const { name, permissions, expiresAt } = await request.json()

  if (!name || typeof name !== 'string') {
    return NextResponse.json(
      { error: '名称是必需的' },
      { status: 400 }
    )
  }

  try {
    const apiKey = await ApiKeyService.createApiKey(
      session.user.id,
      name,
      permissions,
      expiresAt ? new Date(expiresAt) : undefined
    )

    return NextResponse.json({
      id: apiKey.id,
      name: apiKey.name,
      keyValue: apiKey.keyValue, // 只在创建时返回
      permissions: apiKey.permissions,
      expiresAt: apiKey.expiresAt,
      createdAt: apiKey.createdAt
    })
  } catch (error) {
    console.error('创建 API Key 失败:', error)
    return NextResponse.json(
      { error: '创建 API Key 失败' },
      { status: 500 }
    )
  }
}

export async function GET(request: NextRequest) {
  const session = await auth.api.getSession({ headers: request.headers })
  
  if (!session?.user) {
    return NextResponse.json({ error: '未授权' }, { status: 401 })
  }

  try {
    const apiKeys = await ApiKeyService.getUserApiKeys(session.user.id)
    return NextResponse.json({ apiKeys })
  } catch (error) {
    console.error('获取 API Key 失败:', error)
    return NextResponse.json(
      { error: '获取 API Key 失败' },
      { status: 500 }
    )
  }
}

删除 API Key

// src/app/api/api-keys/[keyId]/route.ts
export async function DELETE(
  request: NextRequest,
  { params }: { params: { keyId: string } }
) {
  const session = await auth.api.getSession({ headers: request.headers })
  
  if (!session?.user) {
    return NextResponse.json({ error: '未授权' }, { status: 401 })
  }

  try {
    await ApiKeyService.deleteApiKey(session.user.id, params.keyId)
    return NextResponse.json({ success: true })
  } catch (error) {
    console.error('删除 API Key 失败:', error)
    return NextResponse.json(
      { error: '删除 API Key 失败' },
      { status: 500 }
    )
  }
}

外部 API 端点示例

// src/app/api/external/chat/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { apiAuthMiddleware } from '@/middleware/api-auth'
import { QuotaService } from '@/lib/quota/quota-service'

export async function POST(request: NextRequest) {
  // 验证 API Key
  const authError = await apiAuthMiddleware(request, ['chat:create'])
  if (authError) return authError

  const { message } = await request.json()
  
  if (!message) {
    return NextResponse.json(
      { error: '消息是必需的' },
      { status: 400 }
    )
  }

  try {
    // 检查配额
    const quota = await QuotaService.checkQuota(
      request.apiKey.userId,
      'ai_chat_message'
    )
    
    if (!quota.allowed) {
      return NextResponse.json(
        { error: '配额不足', required: quota.cost },
        { status: 402 }
      )
    }

    // 处理聊天请求
    const response = await processChatMessage(message)
    
    // 消耗配额
    await QuotaService.consumeQuota(
      request.apiKey.userId,
      'ai_chat_message'
    )

    // 记录 API 使用情况
    await ApiKeyService.logApiUsage(
      request.apiKey.id,
      '/api/external/chat',
      'POST',
      200,
      Date.now() - startTime,
      request.ip,
      request.headers.get('user-agent')
    )

    return NextResponse.json({ response })
  } catch (error) {
    console.error('聊天 API 错误:', error)
    return NextResponse.json(
      { error: '内部服务器错误' },
      { status: 500 }
    )
  }
}

React 组件

API Key 管理组件

// src/components/api-keys/api-key-manager.tsx
'use client'

import { useState, useEffect } from 'react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'

interface ApiKey {
  id: string
  name: string
  permissions: string[]
  lastUsedAt: string | null
  expiresAt: string | null
  createdAt: string
}

export function ApiKeyManager() {
  const [apiKeys, setApiKeys] = useState<ApiKey[]>([])
  const [newKeyName, setNewKeyName] = useState('')
  const [isCreating, setIsCreating] = useState(false)
  const [newApiKey, setNewApiKey] = useState<string | null>(null)

  useEffect(() => {
    fetchApiKeys()
  }, [])

  const fetchApiKeys = async () => {
    try {
      const response = await fetch('/api/api-keys')
      const data = await response.json()
      setApiKeys(data.apiKeys)
    } catch (error) {
      console.error('获取 API Key 失败:', error)
    }
  }

  const createApiKey = async () => {
    if (!newKeyName.trim()) return

    setIsCreating(true)
    try {
      const response = await fetch('/api/api-keys', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          name: newKeyName,
          permissions: ['chat:create', 'files:upload']
        })
      })

      const data = await response.json()
      
      if (response.ok) {
        setNewApiKey(data.keyValue)
        setNewKeyName('')
        fetchApiKeys()
      } else {
        console.error('创建失败:', data.error)
      }
    } catch (error) {
      console.error('创建 API Key 失败:', error)
    } finally {
      setIsCreating(false)
    }
  }

  const deleteApiKey = async (keyId: string) => {
    try {
      const response = await fetch(`/api/api-keys/${keyId}`, {
        method: 'DELETE'
      })

      if (response.ok) {
        fetchApiKeys()
      }
    } catch (error) {
      console.error('删除 API Key 失败:', error)
    }
  }

  return (
    <div className="space-y-6">
      <Card>
        <CardHeader>
          <CardTitle>创建新的 API Key</CardTitle>
        </CardHeader>
        <CardContent>
          <div className="flex gap-2">
            <Input
              placeholder="API Key 名称"
              value={newKeyName}
              onChange={(e) => setNewKeyName(e.target.value)}
            />
            <Button
              onClick={createApiKey}
              disabled={isCreating || !newKeyName.trim()}
            >
              {isCreating ? '创建中...' : '创建'}
            </Button>
          </div>
          
          {newApiKey && (
            <div className="mt-4 p-4 bg-yellow-50 border border-yellow-200 rounded">
              <p className="text-sm font-medium text-yellow-800 mb-2">
                您的新 API Key请安全保存不会再次显示):
              </p>
              <code className="block p-2 bg-white border rounded text-sm break-all">
                {newApiKey}
              </code>
            </div>
          )}
        </CardContent>
      </Card>

      <Card>
        <CardHeader>
          <CardTitle>现有 API Keys</CardTitle>
        </CardHeader>
        <CardContent>
          {apiKeys.length === 0 ? (
            <p className="text-gray-500">还没有 API Key</p>
          ) : (
            <div className="space-y-4">
              {apiKeys.map((key) => (
                <div
                  key={key.id}
                  className="flex items-center justify-between p-4 border rounded"
                >
                  <div>
                    <h3 className="font-medium">{key.name}</h3>
                    <p className="text-sm text-gray-500">
                      创建于: {new Date(key.createdAt).toLocaleDateString()}
                    </p>
                    {key.lastUsedAt && (
                      <p className="text-sm text-gray-500">
                        最后使用: {new Date(key.lastUsedAt).toLocaleDateString()}
                      </p>
                    )}
                  </div>
                  <Button
                    variant="destructive"
                    size="sm"
                    onClick={() => deleteApiKey(key.id)}
                  >
                    删除
                  </Button>
                </div>
              ))}
            </div>
          )}
        </CardContent>
      </Card>
    </div>
  )
}

安全最佳实践

  1. API Key 格式:使用可识别的前缀(如 bs_
  2. 加密存储:在数据库中哈希存储 API Key
  3. 权限控制:实现细粒度权限系统
  4. 速率限制:防止 API 滥用
  5. 审计日志:记录所有 API 使用情况
  6. 过期时间:为 API Key 设置合理的过期时间
  7. IP 白名单:可选的 IP 地址限制

使用示例

客户端使用

// 使用 API Key 调用外部 API
const response = await fetch('https://your-app.com/api/external/chat', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer bs_your_api_key_here',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    message: 'Hello, AI!'
  })
})

const data = await response.json()
console.log(data.response)

cURL 示例

curl -X POST https://your-app.com/api/external/chat \
  -H "Authorization: Bearer bs_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"message": "Hello, AI!"}'

测试

// tests/unit/api-key-service.test.ts
import { ApiKeyService } from '@/lib/api-keys/api-key-service'

describe('ApiKeyService', () => {
  it('应该生成有效的 API Key', () => {
    const apiKey = ApiKeyService.generateApiKey()
    expect(apiKey).toMatch(/^bs_[a-f0-9]{64}$/)
  })

  it('应该创建和验证 API Key', async () => {
    const userId = 'test-user'
    const name = '测试 Key'
    
    const createdKey = await ApiKeyService.createApiKey(userId, name)
    expect(createdKey.name).toBe(name)
    expect(createdKey.keyValue).toBeDefined()
    
    const validatedKey = await ApiKeyService.validateApiKey(createdKey.keyValue)
    expect(validatedKey).toBeTruthy()
    expect(validatedKey.userId).toBe(userId)
  })

  it('应该拒绝无效的 API Key', async () => {
    const invalidKey = await ApiKeyService.validateApiKey('invalid_key')
    expect(invalidKey).toBeNull()
  })
})

这个 API Key 系统为您的 SaaS 应用程序提供了安全、可扩展的程序化访问解决方案。