API Key 安全最佳实践
API Key 用于申请登录 token,属于敏感资源。生产环境中,请勿将 API Key 内置到客户端代码、配置文件或安装包中。
客户端交付后,应用包、运行时内存和网络调用链路都可能被分析。即使对 API Key 做混淆或本地加密,也不能从根本上避免密钥泄漏风险。API Key 一旦泄漏,可能被非授权客户端使用,影响应用的正常服务和安全边界。
生产环境推荐方案
建议通过您的业务服务器隔离 API Key:
- 客户端向您的业务服务器请求登录 token。
- 业务服务器根据实际业务需要完成用户、设备或请求来源校验。
- 业务服务器使用保存在服务端的 API Key 调用登录 Token 签发 API接口。
- 业务服务器将申请到的登录 token 返回给客户端。
- 客户端使用登录 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。


