在线咨询
Skip to content

API Key 安全最佳实践 ​

API Key 用于申请登录 token,属于敏感资源。生产环境中,请勿将 API Key 内置到客户端代码、配置文件或安装包中。

客户端交付后,应用包、运行时内存和网络调用链路都可能被分析。即使对 API Key 做混淆或本地加密,也不能从根本上避免密钥泄漏风险。API Key 一旦泄漏,可能被非授权客户端使用,影响应用的正常服务和安全边界。

生产环境推荐方案 ​

建议通过您的业务服务器隔离 API Key:

  1. 客户端向您的业务服务器请求登录 token。
  2. 业务服务器根据实际业务需要完成用户、设备或请求来源校验。
  3. 业务服务器使用保存在服务端的 API Key 调用登录 Token 签发 API接口。
  4. 业务服务器将申请到的登录 token 返回给客户端。
  5. 客户端使用登录 token 调用 xl_dl_login 完成 SDK 登录。

采用该方案后,客户端只需要接触登录 token,不需要持有 API Key。请将 API Key 放在业务服务器的安全配置中,并避免在客户端日志、服务端日志、异常信息和监控上报中输出密钥内容。

JavaScript 示例 ​

客户端 ​

客户端应从业务服务器获取登录 token,再将 token 传给 SDK 登录接口。以下示例基于 Node.js SDK 接入流程:

js
import fs from 'node:fs';
import {
  XLDownloadAPI,
  ERROR_ALREADY_INIT,
  ERROR_SUCCESS,
  TASK_STATUS_SUCCEEDED,
  TASK_STATUS_FAILED,
} from '@xunlei-open/dlsdk';

const appId = 'your-app-id';
const configPath = '/tmp/xl_dl_sdk_conf';
const savePath = '/tmp/ThunderDownload';

async function getLoginTokenFromBusinessServer() {
  const response = await fetch('https://your-business-server.example.com/sdk/login-token', {
    method: 'POST',
  });
  if (!response.ok) {
    throw new Error(`get login token failed: ${response.status}`);
  }

  const result = await response.json();
  return result.token;
}

fs.mkdirSync(configPath, { recursive: true });
fs.mkdirSync(savePath, { recursive: true });

const sdk = new XLDownloadAPI();

let initialized = false;
try {
  const initResult = sdk.init({
    appId,
    appVersion: '1.0',
    configPath,
    saveTasks: true,
  });
  if (initResult !== ERROR_SUCCESS && initResult !== ERROR_ALREADY_INIT) {
    throw new Error(`init failed: ${initResult}`);
  }
  initialized = true;

  // 生产环境中,loginToken 应由业务服务器提供。
  const loginToken = await getLoginTokenFromBusinessServer();

  // Demo 接入阶段可以临时在客户端使用 API Key 申请 loginToken:
  // const apiKey = 'your-api-key';
  // const token = await sdk.getLoginToken(apiKey);
  // const loginToken = token.token;

  const login = sdk.login(loginToken);
  if (login.result !== ERROR_SUCCESS) {
    throw new Error(`login failed: ${login.result}`);
  }

  const task = sdk.createP2spTask({
    url: 'https://example.com/file.zip',
    savePath,
    saveName: 'file.zip',
  });

  if (task.result !== ERROR_SUCCESS) {
    throw new Error(`create task failed: ${task.result}`);
  }

  const startResult = sdk.startTask(task.taskId);
  if (startResult !== ERROR_SUCCESS) {
    throw new Error(`start task failed: ${startResult}`);
  }

  while (true) {
    const state = sdk.getTaskState(task.taskId);
    process.stdout.write(
      `\rstate:${state.state.stateCode} downloaded:${state.state.downloadedSize}/${state.state.totalSize} speed:${state.state.speed}`,
    );
    if (
      state.result !== ERROR_SUCCESS ||
      state.state.stateCode === TASK_STATUS_SUCCEEDED ||
      state.state.stateCode === TASK_STATUS_FAILED
    ) {
      console.log();
      break;
    }
    await new Promise((resolve) => setTimeout(resolve, 1000));
  }
} finally {
  if (initialized) {
    sdk.uninit();
  }
}

业务服务器 ​

业务服务器负责保存 API Key、签发登录 token,并按 JWT 中的 exp 过期时间做本地缓存。以下代码为伪代码,requestOuterLoginToken 需要按登录 Token 签发 API接口的鉴权要求携带 API Key:

js
import jwt from 'jsonwebtoken';

const apiKey = process.env.XUNLEI_API_KEY;
const refreshSkewSeconds = 60;
let loginTokenCache = null;

export async function handleSdkLoginTokenRequest(_request, response) {
  const loginToken = await getCachedLoginToken();
  response.json({ token: loginToken });
}

async function getCachedLoginToken() {
  const nowSeconds = Math.floor(Date.now() / 1000);
  if (loginTokenCache && loginTokenCache.expiresAtSeconds - refreshSkewSeconds > nowSeconds) {
    return loginTokenCache.token;
  }

  const result = await requestOuterLoginToken({
    apiKey,
    url: 'https://open.xunlei.com/api/v1/sdk/login_token',
    body: {
      expires_in: 86400,
      scopes: ['xtask.p2sp', 'xindex.query'],
    },
  });

  const claims = jwt.decode(result.token);
  if (!claims || typeof claims !== 'object' || !Number.isInteger(claims.exp)) {
    throw new Error('login token does not contain a valid exp claim');
  }

  loginTokenCache = {
    token: result.token,
    expiresAtSeconds: claims.exp,
  };
  return loginTokenCache.token;
}

async function requestOuterLoginToken({ apiKey, url, body }) {
  // 伪代码:通过 API Key 调用登录 Token 签发 API,获取登录 token。
  return outerLoginTokenApi.request({
    apiKey,
    url,
    method: 'POST',
    body,
  });
}

该示例使用 jsonwebtoken 读取 JWT 中的 exp 做进程内缓存,并在 token 过期前提前刷新。业务服务器有多个实例时,可根据业务需要改为共享缓存,或让各实例独立维护缓存。

Demo 接入说明 ​

在前期 demo 接入和联调阶段,为了快速验证 SDK 接入流程,可以暂时将 API Key 内置到客户端中,直接申请登录 token。

该方式仅适用于开发验证。进入测试发布、灰度或生产环境前,应切换为业务服务器代为签发登录 token 的方案,并移除客户端中内置的 API Key。