截止本文写作时,已经可以在 Sonatype | maven central repository 搜索到项目。
一个系统中可能存在某些属性是仅供内部使用的。
举个不太恰当的例子,查询符合条件 A 的用户列表返回了 ID 为 1234
的用户,查询符合条件 B 的用户列表也返回了 ID 为 1234
的用户,那么一个第三方就可以根据这两个互不相关的功能推断出存在一个 ID 为 1234
的用户既符合条件 A 又符合条件 B 的事实,而这个信息可能是系统原本不打算提供的。
有些系统会为一个实体创建多个 ID 避免这个问题,例如上面这个用户在查询 A 的结果中 ID 为 4567
,在 B 查询的结果中 ID 为 8901
,而内部系统继续使用 1234
。
如果值为 1234
的这个属性没有意外透出,就不会产生问题。通常的做法是为各个公开接口构造和返回专门的 Response
,Result
或是 VO
对象。但在一些短平快项目中,或是由于代码生成工具完成了工作,或是由于不熟悉项目的开发、测试人员忽略了这种需求,导致某些功能直接返回 Entity
对象,或是通过 BeanUtils 工具类 copy 了不该透露的属性,或是使用了其他什么基于反射的工具整活。
本项目参考 tink-crypto/tink-java - GitHub 中的 com.google.crypto.tink.util.SecretBytes
实现,提供了专门的包装类处理这种问题。通过不提供默认的 getter 方法避免属性被序列化或被拷贝,并提供了相应的 TypeHandler 类帮助实现数据库的读写。
使用方法
使用方法可参考本项目 showcase/safebox-playground/src/test/java/cc/ddrpa/playground/safeboxplayground
目录下的单元测试代码。
在项目中引用 cc.ddrpa.repack.safebox:safebox-core
,使用 SecureLong
,SecureBytes
和 SecureString
类型替换原有的属性类型设计数据对象:
class Client {
private SecureLong secretInnerId;
// ……
public Client setSecretInnerId(Long secretInnerId) {
this.secretInnerId = new SecureLong(secretInnerId);
return this;
}
由于这些包装类型提供了 equals
,length
或 size
方法,大部分情况下并不需要取回原始值,不过你还是可以使用 get(SecureAccess)
方法:
var acutalId = client.getSecretInnerId().get(SecureAccess.gain());
使用 Jackson 等 JSON 序列化工具序列化该对象,会抛出异常,约束后续接手的人员不能在接口直接返回实体对象。如果使用 BeanUtils 等工具复制该对象,Secure 类型不会被拷贝。
读写数据库
如果使用 Mybatis-plus,需要在项目中添加 cc.ddrpa.repack.safebox:safebox-mybatis
依赖,然后为类型注册 TypeHandler。一种简单的方法是在项目的配置文件中添加:
mybatis-plus:
type-handlers-package: cc.ddrpa.repack.safebox.typehandler
可以正常使用 Mybatis-plus 的 LambdaQueryWrapper
以及其他方法读写数据库。
var clientFromDB = clientMapper.selectList(
Wrappers.<Client>lambdaQuery()
.eq(Client::getId, new SecureLong(acutalId)));
assertTrue(clientFromDB.get(0).getId().equals(acutalId));
// or
var clientById = clientMapper.selectById(acutalId).getId();
assertTure(clientById.equals(acutalId));