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 |