构建难以破解的软件授权系统:API计费网关的安全分发实践

当开源遇上商业化

相信不少开发者都遇到过这样的问题:开发的系统,有人愿意付费使用,但你担心源码一旦交付就无法控制其传播?或者你希望限制软件只能在特定环境中运行,防止被用于未经授权的场景?

最近,我们的API计费网关系统在技术社区分享后,收到了不少商业化的需求。这让我们思考:如何在提供软件使用权的同时,保护知识产权并控制软件的使用范围?

经过深思熟虑和技术攻关,我们设计了一套基于非对称加密的域名授权系统。这套系统不仅解决了上述问题,还为软件授权提供了一种全新的思路。今天,我想与大家分享这个方案的技术细节和实现过程。

image

设计目标:安全与可控的平衡

在设计这套授权系统时,我们设定了以下目标:

  • 防止未授权使用:确保软件只能在授权的环境中运行
  • 限制部署范围:将软件使用限定在特定的域名或服务器上
  • 远程控制能力:保留随时撤销授权的能力
  • 安全可靠:采用强加密算法,防止授权被破解
  • 低干扰性:授权机制不应影响软件的正常功能

技术方案:非对称加密的优雅方案

核心架构

我们的授权系统基于RSA非对称加密,采用主站-授权站的架构模式:

1
2
3
4

主站(持有私钥) <----授权数据推送----> 授权站(持有公钥)

`

这种架构的优势在于:

  • 主站持有私钥,可以签名授权数据
  • 授权站只持有公钥,可以验证签名但无法伪造
  • 通过信任链,杜绝“假私钥”签名+替换公钥文件绕过授权校验
  • 授权数据包含时效信息,确保授权的时效性

授权数据结构

授权数据包含以下关键信息:

1
2
3
4
5
6
7
8
9
{
"domain": "example.com", // 授权域名
"issuedAt": 1715654321000, // 签发时间戳
"expiresAt": 1715740721000, // 过期时间戳
"appId": "api.example.com", // 应用标识
"nonce": "random-string-here", // 随机字符串,防重放
"signature": "base64-signature-data" // 使用私钥生成的签名
}
`

每个授权站点拥有独立的RSA密钥对,确保即使一个站点的授权被破解,也不会影响其他站点的安全。

授权验证流程

  • 启动验证:授权站点在启动时检查授权数据
  • 请求验证:每个API请求都会经过授权验证中间件
  • 定期更新:主站定期向授权站点推送新的授权数据
  • 密钥轮换:支持密钥轮换机制,增强长期安全性

信任链机制

为了支持密钥更新和轮换,我们设计了信任链(Trust Chain)机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"keys": [
{
"id": "0",
"publicKey": "-----BEGIN PUBLIC KEY-----\n...",
"fingerprint": "sha256-fingerprint",
"issuedAt": "2025-05-10T08:00:00Z",
"isBootstrap": true,
"description": "初始引导公钥"
}
// 更多密钥记录...
],
"currentKeyId": "0" // 当前使用的密钥ID
}

信任链记录了所有历史密钥,并标记当前使用的密钥,使系统能够平滑地进行密钥轮换。

技术挑战与解决方案

挑战一:公钥同步问题

问题:当主站发起密钥轮替,重置密钥对时,信任链中的公钥与重置后的公钥不匹配时,验证会失败。

解决方案:我们实现了自动同步机制,识别到主站推送了新的信任链则更新本地。确保信任链与公钥文件保持一致:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function syncTrustChainWithPublicKey() {
// 读取公钥文件
const publicKeyFromFile = fs.readFileSync(PUBLIC_KEY_FILE, 'utf8');

// 读取信任链
const trustChain = JSON.parse(fs.readFileSync(TRUST_CHAIN_FILE, 'utf8'));

// 检查是否一致,不一致则更新
// 注意:这里的 currentKey 需要根据实际逻辑获取,例如 trustChain.keys.find(key => key.id === trustChain.currentKeyId)
const currentKey = trustChain.keys.find(key => key.id === trustChain.currentKeyId);
if (currentKey && currentKey.publicKey !== publicKeyFromFile) {
// 备份现有信任链
createBackupFile(TRUST_CHAIN_FILE, '.backup');

// 更新信任链
// ... (具体的更新逻辑,例如更新 currentKey.publicKey 或添加新的 key 并更新 currentKeyId)
}
}

挑战二:备份文件管理

问题:系统生成的备份文件会无限增长,占用磁盘空间。

解决方案:我们实现了备份文件管理机制,只保留最新的三份备份:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function manageBackupFiles(filePattern, directory) {
// 获取所有匹配的文件
const files = fs.readdirSync(directory)
.filter(file => new RegExp(filePattern).test(file))
.map(file => ({
name: file,
path: path.join(directory, file),
time: fs.statSync(path.join(directory, file)).mtime.getTime()
}));

// 按时间排序(从新到旧)
files.sort((a, b) => b.time - a.time);

// 如果文件数量超过3个,删除旧的文件
if (files.length > 3) {
for (let i = 3; i < files.length; i++) {
fs.unlinkSync(files[i].path);
}
}
}

挑战三:授权推送的可靠性

问题:网络不稳定可能导致授权推送失败。

解决方案:我们实现了重试机制,确保授权数据能够可靠地推送到授权站点:

1
2
3
4
5
6
7
8
9
// 假设 retryCount 初始为 0
if (retryCount < 3) {
// 安排重试,每次间隔时间翻倍
const retryDelay = Math.pow(2, retryCount) * 30000; // 30秒、1分钟、2分钟 (原文笔误,应为1分钟、2分钟)
setTimeout(async () => {
// 重试逻辑
// ... call pushAuthDataFunction(..., retryCount + 1)
}, retryDelay);
}

注意:`Math.pow(2, retryCount) 30000retryCount` 为0, 1, 2 时,分别是 30秒, 1分钟, 2分钟。*

实现细节:安全与效率的平衡

白名单字段签名

为了确保授权验证的稳定性,我们采用了白名单方式来选择需要签名的字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 只对这些字段进行签名
const fieldsToSign = [
'domain', 'issuedAt', 'expiresAt', 'appId', 'nonce'
];

// 构建签名字符串
// const signatureData = fieldsToSign
// .map(field => `${field}=${authData[field]}`)
// .join('&');

// 更安全的实现方式,确保字段顺序一致性,并处理值中可能存在的特殊字符
const signaturePayload = fieldsToSign.sort().reduce((acc, field) => {
acc[field] = authData[field];
return acc;
}, {});
const signatureString = JSON.stringify(signaturePayload); // 或其他规范化的序列化方式

注:原始的 join('&') 方式如果字段值的顺序不固定或者值中包含 &=,可能会产生问题。使用排序后的JSON字符串或类似规范化格式通常更稳健。

这种方式确保即使授权数据结构发生变化,只要核心字段保持不变,验证就不会受到影响。

缓存机制

为了提高性能,我们实现了授权验证缓存:

1
2
3
4
5
6
7
8
9
// 授权缓存,避免频繁读取文件
let authCache = {
data: null,
isValid: false,
lastChecked: 0
};

// 缓存有效期(5分钟)
const CACHE_TTL = 5 * 60 * 1000;

这样,系统不需要在每个请求中都进行密集的加密运算,大大提高了性能。

主站身份识别

为了区分主站和授权站,我们实现了基于哈希验证的身份识别机制:

1
2
3
4
5
6
7
8
9
10
11
12
function isMainServer() {
const mainServerKey = process.env.IS_MAIN_SERVER;
if (!mainServerKey) return false;

// 计算环境变量的哈希值
const hash = crypto.createHash('sha256')
.update(Buffer.from(mainServerKey).toString('base64')) // 确保一致的编码处理
.digest('hex');

// 与预设的哈希值比较
return hash === MAIN_SERVER_HASH; // MAIN_SERVER_HASH 需要预先计算并存储
}

这种方式比简单的环境变量检查更安全,防止未授权的服务器冒充主站。

部署与实践

这套授权系统已经成功部署在我们的API计费网关产品中,并交付给客户使用。实践证明,它能够有效地:

  • 保护知识产权:即使客户获得了完整的源码,没有有效的授权也无法运行
  • 控制使用范围:软件只能在授权的域名和服务器上运行
  • 保留控制权:我们可以随时撤销授权,防止滥用
  • 简化部署:客户无需复杂的许可证管理,系统自动处理授权

未来展望

这套授权系统还有很多可以改进的地方:

  • 授权状态监控:开发一个集中式的授权状态监控平台
  • 多级授权:支持不同级别的授权,满足不同客户的需求
  • 离线授权:在特定场景下支持离线授权模式
  • 授权转移:支持授权在不同服务器间的安全转移

结语:安全与开放的平衡

在软件分发的世界里,安全与开放一直是一对矛盾。过度强调安全会限制软件的使用体验,而过度开放则可能导致知识产权流失。

我们的授权系统尝试在这两者之间找到平衡点:既保护了开发者的权益,又不过分限制用户的使用体验。这种平衡或许正是软件授权的未来方向。

通过这套系统,我们的API计费网关产品能够以更灵活的方式提供给更多有需求的客户,而不必担心源码泄露或未授权使用的问题。

如果你也面临类似的软件分发挑战,希望这篇文章能给你一些启发。安全与开放并非对立,而是可以共存的。

本文介绍的授权系统已经在实际项目中得到验证,如果你对这套系统有兴趣,欢迎在评论区留言交流,或通过邮件联系我们。

分享到:

评论完整模式加载中...如果长时间无法加载,请针对 disq.us | disquscdn.com | disqus.com 启用代理