背景
项目里散落着六处配置:客户端直连的对象存储密钥、推送 Token、第三方 API Key……全部硬编码在客户端代码里。今天要把它们收进统一配置中心,核心诉求一句话:
这些核心配置不能被泄露,不能在被人逆向时直接拿到。
围绕这句话,方案迭代了三轮,最终回到了一个最初根本没考虑过的答案。
方案 C:看起来很专业
上午拍板的是一套「按凭据类型区分加密」的设计:
| 凭据类型 | 前缀 | 存储 | 服务端回显 |
|---|---|---|---|
| 纯客户端凭据 | cred_ | 客户端公钥混合加密(RSA-OAEP + AES) | ❌ 服务端无私钥,不可见 |
| 服务端可管理 | scred_ | 对称加密 | ✅ 可回显编辑 |
听起来很美:客户端上传公钥,配置中心用公钥加密,数据库里全是密文,连服务端管理员都看不到明文–教科书级的端到端加密(E2E)。落地、验证、全绿。
下午做安全评估的时候,它塌了。
评估:三个致命缺陷
1. 公钥踩踏
一份配置要下发给多个客户端,加密时用谁的公钥?A 的公钥加密,B 就解不开;给每人存一份密文,配置更新就要全量重加密。E2E 假设「一对一私聊」,而配置中心是「一对多广播」–场景就不匹配。
2. 公钥冒领
公钥是公开的。任何人都能拿到公钥,加密一份假凭据提交上来,服务端无私钥无法验证内容真伪,只会原样存储、原样下发。你防住了自己人看,没防住别人写。
3. 两端都有明文,E2E 防的是谁?
这是最根本的一问。E2E 的威胁模型是「服务端不可信」(即时通讯怕服务器运营商偷看)。可我们的场景:
- 客户端拿到配置后本来就要在内存里解密成明文来用
- 服务端是自己写的、自己部署的,本来就在信任边界内
真正想防的「逆向」发生在客户端,而 E2E 加密的是客户端到服务端这一段。 加密原语堆得再高级,防的都不是你要防的那个人。
点破:物理冲突与真正的命门
继续往下问,会碰到一个无解的矛盾:
「客户端要用凭据」和「客户端逆向拿不到凭据」,物理冲突。
客户端要用,就必须持有明文(至少在内存里);能防的边界只有反编译静态分析(99% 的场景),动态调试 dump 内存防不了–这条要诚实承认,任何声称「客户端防动态逆向」的方案都是自欺。
再回头看真正的命门:就算凭据全部移到服务端,客户端代码里还硬编码着一个静态 API Key。逆向者拿到这个 Key,照样能调用接口把全部凭据拉走。前面所有加密等于白忙。
结论浮出水面:这个场景的安全关键根本不在加密,在鉴权。
最终方案:最简三步
推翻方案 C 之后,剩下的东西少得可怜:
- 删掉整个 E2E:公钥上传、混合加密、双前缀,全删
- 存储对称加密 + 后台脱敏:数据库泄露不泄明文,界面回显一律
****–这一步防的是「数据库被拖」 - 鉴权换动态登录:工号 + 密码换短期 JWT,凭据经 TLS 下发、内存用完即弃、不落盘–这一步防的是「逆向拿到静态钥匙拉走全部」
删掉的代码比新写的还多。验证清单反而更短更硬:有 JWT 才能拿配置(无 token 401)、错误密码拒绝、存储确认为密文。
同一天的另外三次「推翻」
今天的主题像是「推翻自己」,不止这一次:
二维码 404 双层修复。弹窗打不开,第一层根因是 Bootstrap 5 移除了 jQuery 插件集成,八处旧写法全部改原生 API。改完发现页面还是坏的–真正的 404 在用户实际访问的公网入口:那边只反代了 PHP 请求,静态资源落在本地目录全是 404。教训:只测内网等于没测,验证必须覆盖用户真实入口。
下载量统计全 0。不是统计代码有 bug,是客户端走渠道轮转直连渠道下载,根本没经过服务端,自然不计数。修复不是改计数逻辑,而是补一个「下载完成才上报」的端点,顺手用 MD5 投票机制(三票阈值、三天窗口、投票人去重)让多个客户端互相校验文件哈希。
32 条 FAIL 日志。扫描出 32 条失败记录,逐条分类后:31 条是设计内的拦截(二选一城市待确认、多候选客户待澄清、在保不派单……),只有 1 条是真 bug–无操作人参数时误报「凭证过期」,实际该报「写操作必须指定操作人」。红色不等于故障,先分类再动手。
总结
四件事,四个同款教训:
- 先问威胁模型,再选加密方案。E2E 防「服务端不可信」,数据库加密防「拖库」,动态鉴权防「逆向拉取」–三个威胁,三套解法,混着用只会互相拖累。
- 鉴权优先于加密。加密保护数据形态,鉴权决定数据归属。静态钥匙在手里,保险柜再厚也是空的。
- 验证要覆盖真实入口。内网全绿的系统,公网入口可能全是 404。
- 最简方案往往更安全。攻击面小、可审计、能讲清楚。复杂度本身也是一种漏洞。
方案 C 不是死于实现太糙,是死于问错了问题。安全设计的第一步不是选算法,是画威胁模型–搞清楚你到底在防谁。
本文已脱敏,不含真实域名、IP、密钥或用户数据。