0x01. 秘钥数据特征
以我们系统的数据表levy_merchant_relation为例,该数据表存储的是商户服务商关联关系。下面是与之对应的 LevyMerchantRelation 实体类结构,除了包括商户与服务商相关字段,还包括HTTP接口通信的秘钥和口令,如RSA公私钥、加密秘钥。
LevyMerchantRelation {id, merId, merName, levyId, levyName, levyMerId, levyMerName, interfaceKey, publicKey, privateKey, loginPassword, memo, createTime, updateTime}
本文要说的是这些秘钥字段数据。
这些秘钥数据,尤其是RSA秘钥,有两个特征:
- 是比较长的字符串。
- 属于系统隐私数据。
这些秘钥数据会对系统带来如下伤害:
- 对于微服务架构系统来说,这些数据通过RPC传输,一来泄露了隐私数据,致使系统存在安全风险;其次,这些数据急剧增大RPC数据传输的payload(响应结果包含秘钥数据的list接口或page接口,所带来的影响尤其明显)。
- 程序日志方面,这些数据打印在日志文件里,一来泄露了隐私数据,致使系统存在安全风险;其次,这些大字符串数据会使日志体积变大。再者,当我们在通过分析日志来排查系统问题时,这些大字符串往往比较碍眼,会降低我们排查问题的效率。
0x02. 如何解决秘钥数据对系统产生的伤害?
*⬮*解决对RPC传输的伤害 ←RPC传输层的数据隔离
1. DTO职责分离设计:一分为二
作为RPC传输的LevyMerchantRelationDTO,其结构不能直接是LevyMerchantRelation实体类的"副本",而要排除掉秘钥字段,毕竟,90%的调用方是使用relation关系,而不需要秘钥数据。另外,对于那些需要秘钥数据的功能,如页面的CRUD,则定义单独的秘钥DTO。是的,仅包含秘钥字段。
LevyMerchantRelation {id, merId, merName, levyId, levyName, levyMerId, levyMerName, interfaceKey, publicKey, privateKey, loginPassword, memo, createTime, updateTime}
⬇ ⬇ ⬇
LevyMerchantRelationDTO {id, merId, merName, levyId, levyName, levyMerId, levyMerName, memo, createTime, updateTime}
ApiSecretDTO {interfaceKey, publicKey, privateKey, loginPassword}
一分为二后的两个DTO,一个仅包含业务字段,一个仅包含秘钥字段,职责清晰,边界分明,各司其职。
2. RPC接口的精细化设计
对应的服务接口应做相应拆分,确保"按需知密"的安全原则。
- 对于查询接口,单独定义个获取秘钥的接口(如果需要的话),其他接口均返回 LevyMerchantRelationDTO。
- 新增/修改接口则同时包含LevyMerchantRelationDTO和ApiSecretDTO这2个参数。通过RPC接口层面彻底隔离,把伤害降低至0。
*⬮*解决对日志的伤害
有了上面RPC传输层的数据隔离,LevyMerchantRelationDTO 本身不用考虑了。我们需要考虑的,是entity和ApiSecretDTO等包含秘钥的POJO。
我们在kibana日志平台经常看到长长的秘钥串,像金灿灿的油菜花田里一株突兀的、傲慢的绿草,它肆意摇曳,便将整片田野的和谐平衡,轻易撕开一道口子。

如何解决呢?
取决于我们打印日志的方式。
不外乎如下2种:
- 方式一:log.info("XX服务商】-查询企业账户信息,接口入参:relation={}", levyMerchantRelation);
- 方式二:log.info("XX服务商】-查询企业账户信息,接口入参:relation={}", JSON.toJSONString(levyMerchantRelation));
针对方式一,重写entity等模型类的 toString() 方法,在拼接字符串时去掉秘钥field。
import lombok.Data;
@Data
public class LevyMerchantRelation {
...
@Override public String toString() {
return "LevyMerchantRelation{" +
...
", interfaceKey='" + StringUtils.length(interfaceKey) + " characters'" +
", publicKey='" + StringUtils.length(publicKey) + " characters'" +
", privateKey='" + StringUtils.length(privateKey) + " characters'" +
...
'}';
}
}
针对方式二,则要说道说道了。一种办法是不用json序列化,改用方式一。但难免未来在系统迭代中又会出现这种方式的log.info。我们也不能将此纳入到团队编码规范里。毕竟,在log中查看经json序列化后的对象字符串,更利于肉眼识别关键信息。由此,JSONLog出现了。
JSONLog是什么?是一个自定义的工具类,是一个在IDE中键入"JSON"就能联想出来的工具类。
JSONLog的定义如下,它在将对象进行序列化时,过滤掉了 publicKey、privateKey、password等秘钥字段关键字。
package com.sby.common.json;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SimplePropertyPreFilter;
import com.google.common.collect.Sets;
public class JSONLog {
private static final SimplePropertyPreFilter filter = new SimplePropertyPreFilter();
static {
filter.getExcludes().addAll(Sets.newHashSet(
"privateKey", "publicKey", "password", "secret",
"accessKey", "secretKey", "token",
"interfaceKey", "encryptKey", "apiKey"
));
}
/**
* 安全序列化,自动排除 敏感字段(RSA公钥还是长字符串)
*/
public static String toJSONString(Object obj, String... fields) {
if (obj == null) return "null";
return JSON.toJSONString(obj, filter);
}
}
然后,开发团队内部广而告之,相信会比其他解决方案要好。
0x03. 总结
这种从数据模型、接口设计到工具支持的全方位思考,正是构建安全、高性能系统的关键所在。
彼时的2025年5月份,消费券系统开始立项研发,在程序设计阶段,我们要为系统的两个关键参与者------消费企业 与 核销企业------设计数据表结构,我主张为两者分别创建各自的表,至于两者的营业资质信息,则存储到一个共同的营业资质表。看来,通过这种数据隔离方式的数据结构设计,亦可以有效规避这些营业资质信息在上述两方面对系统产生的伤害。 ------------设若 levy_merchant_relation 这张表 不包含秘钥字段(关联关系与关联关系的秘钥分开存储),那么,就不存在 LevyMerchantRelation"因秘钥数据而对系统产生伤害"的事情。------------anyway,从数据设计源头规避风险,也是很重要的一件事。
ref: ⬮ 如何在程序日志中不打印LevyMerchantRelation中的privateKey、publicKey这些大字符串field? ⬮ 程序日志优化:精准捕获与日志分级,践行数字低碳 ⬮ 20231130-调用上海银行timeout,哐哐报错。日复一日月复一月,我们视而不见。近几日日志文件动辄60~70G,我们的系统往往以日志量取胜。 < 摘自公司内部confluence-wiki>