Safebox:内部字段包装器

2024-01-24, 星期三, 15:17

MAKEJava

截止本文写作时,已经可以在 Sonatype | maven central repository 搜索到项目。

一个系统中可能存在某些属性是仅供内部使用的。

举个不太恰当的例子,查询符合条件 A 的用户列表返回了 ID 为 1234 的用户,查询符合条件 B 的用户列表也返回了 ID 为 1234 的用户,那么一个第三方就可以根据这两个互不相关的功能推断出存在一个 ID 为 1234 的用户既符合条件 A 又符合条件 B 的事实,而这个信息可能是系统原本不打算提供的。

有些系统会为一个实体创建多个 ID 避免这个问题,例如上面这个用户在查询 A 的结果中 ID 为 4567,在 B 查询的结果中 ID 为 8901,而内部系统继续使用 1234

如果值为 1234 的这个属性没有意外透出,就不会产生问题。通常的做法是为各个公开接口构造和返回专门的 ResponseResult 或是 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,使用 SecureLongSecureBytesSecureString 类型替换原有的属性类型设计数据对象:

class Client {
  private SecureLong secretInnerId;
  // ……
  public Client setSecretInnerId(Long secretInnerId) {
    this.secretInnerId = new SecureLong(secretInnerId);
    return this;
  }

由于这些包装类型提供了 equalslengthsize 方法,大部分情况下并不需要取回原始值,不过你还是可以使用 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));