如何防止接口的 key 泄露?

目前有一个小程序,功能上比较简单,但是需要调用其他平台提供的 AI 相关接口。为了节省服务器费用,想直接在前端请求接口,但是这样就会暴露接口的 key。有没有其他办法可以防止 key 泄露? 或者默许 key 可能的泄露,然后实时监控接口的使用量,这种方式是否可行?

阅读 2.4k
avatarAI BotBETA

直接答案:

绝对不要在前端硬编码或明文存储敏感 Key。以下方案按推荐优先级排序:


方案一:轻量级服务端代理(强烈推荐)

  1. 核心思路:在前端与AI平台之间增加一层服务端代理(如云函数/Serverless),Key存储在服务端
  2. 实现方式

    // 前端请求你的代理接口(无Key)
    fetch('https://your-proxy.com/ai-api', {
      method: 'POST',
      body: JSON.stringify({ prompt: "用户输入" })
    })
    
    // 云函数(示例:AWS Lambda)
    exports.handler = async (event) => {
      const response = await fetch('https://ai-platform.com/api', {
        headers: {
          'Authorization': `Bearer ${process.env.SECRET_KEY}` // 从环境变量读取
        },
        body: event.body
      });
      return response.json();
    };
  3. 成本:云函数费用极低(如腾讯云SCF免费额度100万次/月)

方案二:使用平台白名单机制(若支持)

  1. 域名/IP白名单:在AI平台配置HTTP Referer或IP白名单
  2. Key泄露影响:即使Key被获取,攻击者也无法从非白名单域名/IP使用

方案三:临时令牌(需要平台支持)

  1. 短期Token:通过服务端申请时效性Token(如1小时有效期)
  2. 前端限制:通过sign签名参数或请求头校验增强安全性

监控方案(仅作为补充手段)

  1. 可行性:可实施但风险极高
  2. 监控指标

    • 调用频率突增告警
    • 来源IP异常检测
    • 用量阈值自动封禁
  3. 致命缺陷:从发现异常到响应的空窗期可能已造成损失

最终建议

必须使用方案一,其他方案作为辅助。Serverless方案:

  • 成本:约¥5/月即可处理10万次请求
  • 部署时间:主流平台可在30分钟内完成配置
  • 安全性:通过环境变量隔离密钥,配合VPC网络更安全
8 个回答

服务器或者api转发,前端混淆根本解决不了问题。

代码混淆一下

还是用服务器做一下校验吧,基本上就是个代理脚本,不超过 10 行,很容易的。

云函数(Serverless)方案

适用场景: 调用量不大的小程序,希望零运维

实现方式:

  • 在腾讯云、阿里云、或Vercel上创建云函数
  • 云函数内部调用AI接口,key保存在环境变量中
  • 小程序前端调用你的云函数接口

成本优势:

  • 按调用次数计费,无固定费用
  • 免费额度通常够小程序使用
  • 无需服务器运维成本

代码示例:

// 云函数代码
exports.main = async (event) => {
  const response = await fetch('AI_API_URL', {
    headers: {
      'Authorization': `Bearer ${process.env.AI_API_KEY}`
    },
    body: JSON.stringify(event.data)
  });
  return response.json();
}

云函数特性:

  • ✅ Token认证机制(HMAC-SHA256签名)
  • ✅ 频率限制(每分钟/每天调用次数限制)
  • ✅ IP级别的访问控制
  • ✅ 错误处理和日志记录
  • ✅ CORS跨域支持

安全特性:

  • API Key完全隐藏在云函数环境变量中
  • Token有时效性(5分钟过期)
  • 防止重放攻击和滥用
  • 完整的输入验证

使用方法:

  1. 将云函数代码部署到云平台
  2. 设置环境变量(AI_API_KEY 和 APP_SECRET)
  3. 前端使用 AIApiClient 类调用

成本控制:

  • 免费额度足够小程序使用
  • 按实际调用量计费
  • 内置频率限制防止滥用

云函数完整实现方案

// ============= 云函数代码 =============
// 文件名: index.js

const crypto = require('crypto');

// 配置
const config = {
  AI_API_URL: 'https://api.openai.com/v1/chat/completions', // 替换为实际AI接口地址
  APP_SECRET: process.env.APP_SECRET || 'your-app-secret', // 应用密钥
  RATE_LIMIT: {
    perMinute: 10, // 每分钟最多调用次数
    perDay: 100    // 每天最多调用次数
  }
};

// 内存缓存(生产环境建议使用Redis)
const cache = new Map();

// 获取客户端IP
function getClientIP(event) {
  return event.headers['x-forwarded-for'] || 
         event.headers['x-real-ip'] || 
         event.requestContext?.identity?.sourceIp || 
         'unknown';
}

// 验证token
function verifyToken(token, timestamp) {
  try {
    const [appId, ts, signature] = Buffer.from(token, 'base64').toString().split(':');
    
    // 检查时间戳(5分钟内有效)
    const now = Date.now();
    if (Math.abs(now - parseInt(ts)) > 5 * 60 * 1000) {
      return { valid: false, error: 'Token expired' };
    }
    
    // 验证签名
    const expectedSig = crypto.createHmac('sha256', config.APP_SECRET)
                             .update(`${appId}:${ts}`)
                             .digest('hex');
    
    if (signature !== expectedSig) {
      return { valid: false, error: 'Invalid signature' };
    }
    
    return { valid: true, appId };
  } catch (error) {
    return { valid: false, error: 'Invalid token format' };
  }
}

// 检查频率限制
function checkRateLimit(clientId) {
  const now = Date.now();
  const minuteKey = `${clientId}:${Math.floor(now / 60000)}`;
  const dayKey = `${clientId}:${Math.floor(now / 86400000)}`;
  
  // 检查分钟限制
  const minuteCount = cache.get(minuteKey) || 0;
  if (minuteCount >= config.RATE_LIMIT.perMinute) {
    return { allowed: false, error: 'Rate limit exceeded (per minute)' };
  }
  
  // 检查日限制
  const dayCount = cache.get(dayKey) || 0;
  if (dayCount >= config.RATE_LIMIT.perDay) {
    return { allowed: false, error: 'Rate limit exceeded (per day)' };
  }
  
  // 更新计数
  cache.set(minuteKey, minuteCount + 1);
  cache.set(dayKey, dayCount + 1);
  
  // 设置过期时间(简单实现,生产环境建议用Redis TTL)
  if (minuteCount === 0) {
    setTimeout(() => cache.delete(minuteKey), 60000);
  }
  if (dayCount === 0) {
    setTimeout(() => cache.delete(dayKey), 86400000);
  }
  
  return { allowed: true };
}

// 调用AI接口
async function callAIAPI(data) {
  try {
    const response = await fetch(config.AI_API_URL, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${process.env.AI_API_KEY}`
      },
      body: JSON.stringify(data)
    });
    
    if (!response.ok) {
      throw new Error(`AI API error: ${response.status} ${response.statusText}`);
    }
    
    return await response.json();
  } catch (error) {
    console.error('AI API call failed:', error);
    throw error;
  }
}

// 主函数
exports.main = async (event, context) => {
  try {
    // 解析请求
    const { token, data } = typeof event.body === 'string' 
      ? JSON.parse(event.body) 
      : event.body || event;
    
    // 验证必要参数
    if (!token || !data) {
      return {
        statusCode: 400,
        body: JSON.stringify({
          success: false,
          error: 'Missing token or data'
        })
      };
    }
    
    // 验证token
    const tokenResult = verifyToken(token);
    if (!tokenResult.valid) {
      return {
        statusCode: 401,
        body: JSON.stringify({
          success: false,
          error: tokenResult.error
        })
      };
    }
    
    // 获取客户端标识
    const clientIP = getClientIP(event);
    const clientId = `${tokenResult.appId}:${clientIP}`;
    
    // 检查频率限制
    const rateLimitResult = checkRateLimit(clientId);
    if (!rateLimitResult.allowed) {
      return {
        statusCode: 429,
        body: JSON.stringify({
          success: false,
          error: rateLimitResult.error
        })
      };
    }
    
    // 调用AI接口
    const aiResponse = await callAIAPI(data);
    
    // 返回结果
    return {
      statusCode: 200,
      headers: {
        'Content-Type': 'application/json',
        'Access-Control-Allow-Origin': '*', // 根据需要调整CORS
        'Access-Control-Allow-Methods': 'POST',
        'Access-Control-Allow-Headers': 'Content-Type'
      },
      body: JSON.stringify({
        success: true,
        data: aiResponse
      })
    };
    
  } catch (error) {
    console.error('Function execution error:', error);
    
    return {
      statusCode: 500,
      body: JSON.stringify({
        success: false,
        error: 'Internal server error'
      })
    };
  }
};

// ============= 前端调用代码 =============
// 文件名: api-client.js

class AIApiClient {
  constructor(appId, appSecret, cloudFunctionUrl) {
    this.appId = appId;
    this.appSecret = appSecret;
    this.cloudFunctionUrl = cloudFunctionUrl;
  }
  
  // 生成认证token
  generateToken() {
    const timestamp = Date.now().toString();
    
    // 创建签名
    const crypto = require('crypto'); // Node.js环境
    // 浏览器环境需要使用crypto-js库: const crypto = require('crypto-js');
    
    const signature = crypto.createHmac('sha256', this.appSecret)
                           .update(`${this.appId}:${timestamp}`)
                           .digest('hex');
    
    // 浏览器环境: 
    // const signature = crypto.HmacSHA256(`${this.appId}:${timestamp}`, this.appSecret).toString();
    
    const tokenData = `${this.appId}:${timestamp}:${signature}`;
    return btoa(tokenData); // 浏览器环境可直接使用
    // Node.js环境: return Buffer.from(tokenData).toString('base64');
  }
  
  // 调用AI接口
  async callAI(requestData) {
    try {
      const token = this.generateToken();
      
      const response = await fetch(this.cloudFunctionUrl, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          token: token,
          data: requestData
        })
      });
      
      const result = await response.json();
      
      if (!result.success) {
        throw new Error(result.error || 'API call failed');
      }
      
      return result.data;
      
    } catch (error) {
      console.error('AI API call failed:', error);
      throw error;
    }
  }
}

// 使用示例
const client = new AIApiClient(
  'your-app-id',
  'your-app-secret', 
  'https://your-cloud-function-url'
);

// 调用示例
async function example() {
  try {
    const response = await client.callAI({
      model: "gpt-3.5-turbo",
      messages: [
        { role: "user", content: "Hello, how are you?" }
      ],
      max_tokens: 100
    });
    
    console.log('AI Response:', response);
  } catch (error) {
    console.error('Error:', error.message);
  }
}

// ============= 环境变量配置 =============
/*
在云函数平台设置以下环境变量:

AI_API_KEY=your-actual-ai-api-key
APP_SECRET=your-app-secret-key

部署说明:
1. 将云函数代码上传到云平台
2. 设置环境变量
3. 配置触发器(HTTP触发)
4. 前端使用AIApiClient调用
*/
  1. 你的想法行不通:小程序调用外部接口必须认证域名,别人平台的接口的域名你没法认证;
  2. 最佳方案:基于第1点,请求 AI 平台接口,需要再服务端转一道,要么租空间(很便宜,但是要域名备案),要么用微信云服务(收费)。

AI的接口如果本身没有提供混淆地址的方法,KEY肯定会被看到吧,建议前后端合作提高KEY的刷新频率。比如后端每隔1分钟就刷新一次KEY,前端必须重新获取KEY,然后再发起AI请求,这样看到也没多大用了。当然前后端传递KEY必须用保密点的方法,不能被轻易侦测到。

如果你放在前端,基本没法隐藏key,不管是混淆还是加密,在最终也会在你请求AI接口的时候会暴露,只要想办法抓包就行了。至于你的方案二,实时监控接口用量,理论上就不现实,你这估计也是人工监控,他半夜跑你有什么办法。个人建议你还是放服务端吧

新手上路,请多包涵

前端无论如何都会暴露的

即使你的加密做的再好也需要去请求

服务器不需要太好的

最便宜的最低核的都可以,后端程序非常小,仅利用接口,访问接口并转发即可,成本很低的;况且拥有一个服务器在很多方面都会方便很多,例如控制权限等

推荐问题