六维教程

EdgeOne Makers Agents 鉴权

Agent 上线之后不能裸奔,得知道谁在调用、能调什么、花了多少。这篇讲 Agent 的访问控制方案。

为什么需要鉴权

Agent 背后是大模型调用,每次请求都有成本。如果不加鉴权,任何人都能白嫖你的 Agent,轻则花光额度,重则被恶意滥用。

风险 后果
无限制访问 额度被刷光,账单爆炸
密钥泄露 别人拿你的 Key 调模型
无身份追踪 出了问题不知道谁干的
无权限控制 普通用户能调管理员功能

鉴权方案一览

方案 适合场景 安全级别
API Key 后端服务调用
JWT Token 用户登录后的请求
OAuth 2.0 第三方应用接入
IP 白名单 内部服务互调

API Key 鉴权

最简单的方式,给调用方一个 Key,每次请求带上。

export default {
  async fetch(request, env, ctx) {
    // 从请求头读取 API Key
    const apiKey = request.headers.get('Authorization')?.replace('Bearer ', '');
    
    // 验证 Key
    if (!apiKey || !await validateApiKey(apiKey, env)) {
      return Response.json(
        { error: '无效的 API Key' },
        { status: 401 }
      );
    }
    
    // 处理正常请求
    return handleAgentRequest(request, env, apiKey);
  }
};

async function validateApiKey(key, env) {
  // 从 KV 读取 Key 信息
  const keyData = await env.KV.get(`apikey:${key}`, 'json');
  
  if (!keyData) return false;
  
  // 检查是否过期
  if (keyData.expiresAt && keyData.expiresAt < Date.now()) {
    return false;
  }
  
  // 检查是否被禁用
  if (keyData.disabled) return false;
  
  return true;
}

调用方这样使用

curl -X POST https://your-agent.makers.dev/chat \
  -H "Authorization: Bearer your-api-key-here" \
  -H "Content-Type: application/json" \
  -d '{"message": "你好"}'

Key 管理

需要一个管理界面来创建、查看、禁用 Key。

// 创建 API Key
async function createApiKey(name, permissions, env) {
  const key = 'eo_' + crypto.randomUUID().replace(/-/g, '');
  
  const keyData = {
    name,
    permissions,
    createdAt: Date.now(),
    expiresAt: null, // 永不过期
    disabled: false,
    usageCount: 0
  };
  
  await env.KV.put(`apikey:${key}`, JSON.stringify(keyData));
  
  // 维护一个 Key 列表,方便查询
  const allKeys = await env.KV.get('apikey:list', 'json') || [];
  allKeys.push({ key, name, createdAt: keyData.createdAt });
  await env.KV.put('apikey:list', JSON.stringify(allKeys));
  
  return { key, ...keyData };
}

// 列出所有 Key
async function listApiKeys(env) {
  const list = await env.KV.get('apikey:list', 'json') || [];
  
  // 补充每个 Key 的详细信息
  const keys = [];
  for (const item of list) {
    const detail = await env.KV.get(`apikey:${item.key}`, 'json');
    keys.push({ ...item, ...detail });
  }
  
  return keys;
}

// 禁用 Key
async function disableApiKey(key, env) {
  const keyData = await env.KV.get(`apikey:${key}`, 'json');
  if (!keyData) throw new Error('Key 不存在');
  
  keyData.disabled = true;
  await env.KV.put(`apikey:${key}`, JSON.stringify(keyData));
}

// 删除 Key
async function deleteApiKey(key, env) {
  await env.KV.delete(`apikey:${key}`);
  
  const list = await env.KV.get('apikey:list', 'json') || [];
  const newList = list.filter(item => item.key !== key);
  await env.KV.put('apikey:list', JSON.stringify(newList));
}

用量限制

光有 Key 不够,还得限制每个 Key 能调多少次。

async function checkRateLimit(apiKey, env) {
  const now = Date.now();
  const windowMs = 60 * 1000; // 1分钟窗口
  const maxRequests = 10; // 每分钟最多 10 次
  
  const rateKey = `rate:${apiKey}:${Math.floor(now / windowMs)}`;
  const count = parseInt(await env.KV.get(rateKey) || '0');
  
  if (count >= maxRequests) {
    return {
      allowed: false,
      limit: maxRequests,
      remaining: 0,
      resetAt: (Math.floor(now / windowMs) + 1) * windowMs
    };
  }
  
  // 计数加 1,设置过期时间
  await env.KV.put(rateKey, String(count + 1), {
    expirationTtl: 120 // 2分钟后自动清理
  });
  
  return {
    allowed: true,
    limit: maxRequests,
    remaining: maxRequests - count - 1
  };
}

// 在请求处理中使用
async function handleAgentRequest(request, env, apiKey) {
  // 检查频率限制
  const rateCheck = await checkRateLimit(apiKey, env);
  
  if (!rateCheck.allowed) {
    return Response.json(
      { 
        error: '请求过于频繁',
        retryAfter: Math.ceil((rateCheck.resetAt - Date.now()) / 1000)
      },
      { 
        status: 429,
        headers: {
          'X-RateLimit-Limit': String(rateCheck.limit),
          'X-RateLimit-Remaining': '0',
          'Retry-After': String(rateCheck.retryAfter)
        }
      }
    );
  }
  
  // 正常处理请求...
  const { message } = await request.json();
  const reply = await callAgent(message, env);
  
  return Response.json({ reply }, {
    headers: {
      'X-RateLimit-Limit': String(rateCheck.limit),
      'X-RateLimit-Remaining': String(rateCheck.remaining)
    }
  });
}

JWT 鉴权

适合有用户系统的场景。用户登录后拿到 JWT,请求时带上。

// 简单的 JWT 验证(不依赖外部库)
async function verifyJWT(token, env) {
  try {
    const [header, payload, signature] = token.split('.');
    
    // 解码 payload
    const decoded = JSON.parse(atob(payload));
    
    // 检查是否过期
    if (decoded.exp && decoded.exp < Math.floor(Date.now() / 1000)) {
      return { valid: false, error: 'Token 已过期' };
    }
    
    // 验证签名(简化版,实际要用 HMAC)
    const expectedSig = await computeSignature(
      `${header}.${payload}`, 
      env.JWT_SECRET
    );
    
    if (expectedSig !== signature) {
      return { valid: false, error: '签名无效' };
    }
    
    return { valid: true, userId: decoded.sub, role: decoded.role };
  } catch (e) {
    return { valid: false, error: 'Token 格式错误' };
  }
}

async function computeSignature(data, secret) {
  const encoder = new TextEncoder();
  const keyData = encoder.encode(secret);
  const msgData = encoder.encode(data);
  
  const key = await crypto.subtle.importKey(
    'raw', keyData,
    { name: 'HMAC', hash: 'SHA-256' },
    false, ['sign']
  );
  
  const sig = await crypto.subtle.sign('HMAC', key, msgData);
  return btoa(String.fromCharCode(...new Uint8Array(sig)));
}

// 中间件
export default {
  async fetch(request, env, ctx) {
    const token = request.headers.get('Authorization')?.replace('Bearer ', '');
    
    if (!token) {
      return Response.json({ error: '缺少 Token' }, { status: 401 });
    }
    
    const verify = await verifyJWT(token, env);
    
    if (!verify.valid) {
      return Response.json({ error: verify.error }, { status: 401 });
    }
    
    // verify.userId 就是当前用户
    return handleAgentRequest(request, env, verify.userId);
  }
};

权限控制

不同用户能调不同的工具或模型。

const PERMISSIONS = {
  // 基础用户只能用基础模型
  basic: {
    models: ['deepseek-v3'],
    tools: ['get_weather', 'calculator'],
    rateLimit: 10 // 每分钟
  },
  // 高级用户可以用高级模型和更多工具
  premium: {
    models: ['deepseek-v3', 'gpt-4o', 'claude-3.5-sonnet'],
    tools: ['get_weather', 'calculator', 'search_database', 'send_email'],
    rateLimit: 60
  },
  // 管理员全部权限
  admin: {
    models: ['*'], // 所有模型
    tools: ['*'],  // 所有工具
    rateLimit: 9999
  }
};

async function checkPermission(userId, action, env) {
  // 获取用户角色
  const userData = await env.KV.get(`user:${userId}`, 'json');
  const role = userData?.role || 'basic';
  const perms = PERMISSIONS[role];
  
  // 检查模型权限
  if (action.type === 'model') {
    if (!perms.models.includes('*') && !perms.models.includes(action.model)) {
      return { allowed: false, reason: `没有权限使用模型 ${action.model}` };
    }
  }
  
  // 检查工具权限
  if (action.type === 'tool') {
    if (!perms.tools.includes('*') && !perms.tools.includes(action.tool)) {
      return { allowed: false, reason: `没有权限使用工具 ${action.tool}` };
    }
  }
  
  return { allowed: true };
}

鉴权方案对比

方案 优点 缺点 适合
API Key 实现简单 Key 泄露风险 后端服务互调
JWT 无状态,不查库 Token 大,有过期问题 用户登录场景
OAuth 标准化,第三方友好 实现复杂 开放平台
IP 白名单 零配置 不灵活 内部服务

速查卡片

要点 说明
为什么需要鉴权 防滥用、控成本、追踪问题
API Key 鉴权 最简单,适合服务间调用
JWT 鉴权 适合有用户系统的场景
频率限制 滑动窗口或固定窗口,防刷
权限控制 不同角色对应不同模型和工具权限
Key 管理 创建、禁用、删除,维护列表
推荐方案 内部服务用 API Key,用户场景用 JWT
上一篇
EdgeOne Makers Agents 可观测性
下一篇
EdgeOne Makers 存储概览