跳至主要內容

mybatis数据加密组件组件

杨轩-国实信息大约 7 分钟常用组件数据加解密

git地址:

http://10.16.202.103:8089/component/component-ser/mybatis-encrypt-starter

适用于在项目中使用mybatis-plus进行数据查询及保存的场景,使用 MybatisMybatis-plus 的拦截器对数据进行加解密。

主要实现两个功能:

  • 数据机密性保护:防止数据泄露,使用对字段进行加密的方式实现
  • 数据完整性保护:防止数据被篡改,使用对数据计算消息摘要的方式实现

使用

引入依赖:

<dependency>
    <groupId>com.gosci.tech</groupId>
    <artifactId>mybatis-encrypt-starter</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</dependency>

在项目配置文件中添加配置,启动加解密配置,并选择具体算法:

encrypt:
  enable: true
  algorithm:
    secrecy: aes
    integrity: md5

上面这段配置,选择的是使用aes算法进行加解密,使用md5算法校验数据完整性。默认的实现是sm2和md5(可以不在配置文件中显式的指明)。本地测试过程中,因为无法连到dmz或金宏网,所以加密和签名只能使用这两种算法,切换到政务网后,需要将两者切换为gov模式。

更新:

本地测试推荐使用 aes 算法,因为aes算法对于相同内容多次加密的结果相同。如果对加密字段进行查询的话,每次加密结果不同的话会无法查询。

具体算法扩展可以看后面的部分,例如在使用政务云dmz区密评服务时,就需要把配置文件修改为:

encrypt:
  enable: true
  algorithm:
    secrecy: gov
    integrity: gov
  properties:
    # DMZ 使用这个地址
    govApiHost: http://192.168.207.150:8880/miping
    # 金宏网使用这个地址
    # govApiHost: https://10.213.9.2:10006
    appId: APP_C069372E7DB944258255A3DA001949FC
    deviceID: DEV_57AA53A21ADC484E980EA43BB63A81D6
    secret: E9CzOebl9qa0mYsAte6iWTC63RD09e0W
    pstKey: "AQAAACAAAAB+PR2pafJ9DIBRzwaxyxuaeb3HDL9afPJr0/zLat4WlgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADuCb3Z8fF0GDOBbvdsHEDTilxGrIzeDBIdzDJ9d7fSkQ=="
    keyId: 0253c934199f47adb2ac75bc5b0eb345
    iv: Z29zY2lvY2VhbjEyMDAwMA==

注意,并不是在这里就对所有数据进行完整性和机密性保护了,如果需要做保护,需要在实体类或字段上添加对应注解,**这里只是确定加密算法使用什么 **。

建表语句

如果需要做完整性校验的话,需要在业务系统中添加一张新表 sys_data_hmac ,用来保存完整性校验的值:

-- mysql建表语句
CREATE TABLE `sys_data_hmac` (
  `id` bigint(20) NOT NULL COMMENT '主键',
  `table_name` varchar(64) DEFAULT NULL COMMENT '表名',
  `data_id` bigint(20) DEFAULT NULL COMMENT '数据id',
  `digest` varchar(256) DEFAULT NULL COMMENT '摘要',
  `create_by` varchar(32) DEFAULT NULL COMMENT '创建人',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `update_by` varchar(32) DEFAULT NULL COMMENT '更新人',
  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
  `del_flag` tinyint(2) NOT NULL DEFAULT '0' COMMENT '删除标志',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='数据摘要关联关系';

CREATE INDEX data_id_idx ON sys_data_hmac (`data_id`,`table_name`);
-- 达梦建表语句
CREATE TABLE "sys_data_hmac" (
  "id" BIGINT  IDENTITY(1,1) NOT NULL,
  "table_name" VARCHAR(64) DEFAULT NULL NULL,
  "data_id" BIGINT DEFAULT NULL NULL,
  "digest" VARCHAR(256) DEFAULT NULL NULL,
  "create_by" VARCHAR(32) DEFAULT NULL NULL,
  "create_time" DATETIME DEFAULT NULL NULL,
  "update_by" VARCHAR(32) DEFAULT NULL NULL,
  "update_time" DATETIME DEFAULT NULL NULL,
  "del_flag" TINYINT DEFAULT 0 NOT NULL,
  PRIMARY KEY ("id")
);
CREATE INDEX SYS_DATA_HMAC_DATA_ID_IDX ON "sys_data_hmac" ("data_id","table_name");

完整性校验

在Po实体类上添加 @IntegrityCheck 注解,例如:

@Data
@IntegrityCheck
@TableName("system_dict")
public class SystemDictPo implements Serializable{
    //...
}

一般,@IntegrityCheck 都会与加了 @TableName 的实体类配合使用,所以不需要添加额外的参数。如果在其他情况下使用时,需要添加一个参数:

@IntegrityCheck(tableName = "system_dict")

不参与签名的字段

数据库的通用字段实际业务意义不大,所以不需要进行签名计算,组件中预置了一些通用字段,在计算签名的过程中会自动去除调:

List<String> DEFAULT_FIELD =Arrays.asList("serialVersionUID",
        "createBy","createTime","updateBy","updateTime","delFlag");

如果系统中还有其他的通用字段,可以在配置文件中的encrypt.properties.stopWord中进行扩充,示例:

encrypt:
  enable: true
  properties:
    stopWord:
      - deleted
      - createId
      - createName
      - updateId
      - updateName
      - remark

机密性校验

在需要加密的实体类字段上添加 @EncryptedColumn 注解,例如:

@EncryptedColumn
@ApiModelProperty(value = "字典描述")
@TableField("dict_desc")
private String dictDesc;

说明

在政府云密评项目改造中,要求机密性保护数据必须做完整性保护,所以在机密性保护的实体类中要添加上面两种注解

但考虑到在其他情况下,数据可能不需要同时进行这两种保护,所以这两个注解可以分开使用,达到不同的保护目的。

数据初始化加密

全量初始化

项目启动后,直接调用内置接口 /data/encrypt/init 即可,数据量大的情况可能时间会比较长。

curl -X POST http://127.0.0.1:8888/data/encrypt/init

加密前建议先对加密列进行长度的扩展,达梦sql语句示例:

ALTER TABLE BOAT_NAVIGATION_EQUIPMENT.EQUIPMENT_INFO MODIFY EQUIPMENT_CODE VARCHAR(128) DEFAULT '' NOT NULL;
ALTER TABLE BOAT_NAVIGATION_EQUIPMENT.EQUIPMENT_INFO MODIFY BOAT_NUM VARCHAR(128) DEFAULT '' NOT NULL;

单张表初始化

如果某张表后续进行了调整,可以调用接口/data/encrypt/initPo接口,只对这一张表再次进行初始化。请求方式:

curl http://127.0.0.1:10100/data/encrypt/initPo?poName=AuthUserPo

注意事项

查询参数中包含加密列的情况

分为两种情况,默认情况下,会有一个拦截器自动对参数进行加密,但是不排除复杂sql情况下可能会有问题。所以提供了两种方式供大家使用

1、自动加密方式

不需要做任何改动。

2、手动加密方式

需要对原有查询语句进行改造,改造前的语句:

SystemDictPo record = BeanUtil.copyProperties(systemDictDto, SystemDictPo.class);
List<SystemDictPo> list = this.list(record);

下面进行改造。

先注入SecrecyService

@Service
@RequiredArgsConstructor
public class SystemDictServiceImpl extends BaseServiceImpl<ISystemDictMapper, SystemDictPo> implements ISystemDictService {

    private final SecrecyService secrecyService;
}

改业务查询代码:

SystemDictPo record = BeanUtil.copyProperties(systemDictDto, SystemDictPo.class);
// 手动方式,对参数手动加密
ManualSwitch.getInstance().setStatus(true);
record.setDictName(secrecyService.encrypt(record.getDictName()));
record.setDictDesc(secrecyService.encrypt(record.getDictDesc()));
List<SystemDictPo> list = this.list(record);
ManualSwitch.getInstance().remove();

或者,如果传的是Po的话,提供了方法直接对Po加密,可以简化为这样:

SystemDictPo record = BeanUtil.copyProperties(systemDictDto, SystemDictPo.class);
// 手动方式,对参数对象自动加密
ManualSwitch.getInstance().setStatus(true);
EntityUtil.encrypt(record);
List<SystemDictPo> list = this.list(record);
ManualSwitch.getInstance().remove();

注意,调用EntityUtil.encrypt()这种方式传的参数一定要是Po,因为注解@EncryptedColumn是加在Po上的,像Dto什么的是没有添加字段加密注解的。

目前存在的问题

saveBatch

因为mybatisplus的saveBatch()方法无法获取到数据插入后的主键id,所以拦截器中无法进行hmac表的插入,如果有使用saveBatch() 的地方,目前只能暂时在循环中逐条插入。

线程池

测试过程中,有在线程池中执行sql,发现会有插入sys_data_hmac表失败的情况,需酌情决定是否使用线程池。

如果使用线程池,建议减小线程数量,最大线程数不要超过10,创建线程池示例:

ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        5,
        10,
        60,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(10),
        new ThreadPoolExecutor.CallerRunsPolicy()
);

只能对字符串加密

因为目前加密时候用的都是反射,加密后的东西全是字符串类型,如果原类型不为字符串会产生异常。所以如果想对非字符串加密的话,需要先改数据库中的数据类型,再改java代码中的数据类型。

加密列不支持模糊查询

因为加密算法的原因,加密后无法进行模糊查询。如果想要对加密列实现模糊查询,需要自己扩展加密算法,可以参考:

淘宝密文字段检索方案open in new window

只查询部分字段

例如下面这种情况,只查询一个字段:

queryWrapper.select("administrative_division");
List<OceanRanchBigScreenPo> list = this.list(queryWrapper);

查询出的Po中数据是不完整的,做完整性校验的时候肯定通不过,需要手动关闭校验:

SimplifySwitch.getInstance().setStatus(true);
queryWrapper.select("administrative_division");
List<OceanRanchBigScreenPo> list = this.list(queryWrapper);
SimplifySwitch.getInstance().remove();

数据库中设置了默认值

导致的问题:第一次插入时对象属性为null,插入数据库后数据库自己加了默认值,所以在下一次计算完整性时会报错。

解决方式:

  • 删除数据库默认值
  • 在代码中进行校验,为null时手动写入默认值

数值问题

Decimal类型会根据小数点后的精度自动补充0,导致插入和查询时计算hash时可能不一样,需要插入代码时手动设置精度。

扩展方式

扩展加解密算法

自己写一个类实现SecrecyService接口,实现 encryptdecrypt 方法。然后在 EncryptConfig 配置类中进行配置,需要指定一个启动的命名字符串。

以政务云加密方式GovSecrecyService为例:

@Bean
@ConditionalOnProperty(value = "encrypt.algorithm.secrecy", havingValue = "gov")
public GovSecrecyService govSecrecyService() {
    log.info("use gov secrecy algorithm");
    return new GovSecrecyService();
}

扩展消息摘要算法

自己写一个类实现IntegrityService接口,实现 digestcheck 方法。然后在 EncryptConfig 配置类中进行配置,需要指定一个启动的命名字符串。

以政务云校验方式GovIntegrityService为例:

@Bean
@ConditionalOnProperty(value = "encrypt.algorithm.integrity", havingValue = "gov")
public GovIntegrityService govIntegrityService() {
    log.info("use gov integrity algorithm");
    return new GovIntegrityService();
}