数据加密 – 数据库隐私字段组件

数据加密 - 数据库隐私字段组件

数据加密概述

加密:将明文信息改变为难以读取的密文内容。
解密:将密文内容转化为原来数据。

分类

  • 对称加密:加密与解密密钥相同。
  • 非对称加密:加密使用公钥,公钥可公开;解密使用私钥。

相关阅读:

  • 加密 – wikipedia

数据库隐私字段加密注解组件实现

说明

  1. 数据库存储密文字段,内存可见为明文信息
  2. 可设定字段保存值支持 整体加密(仅可全部匹配查询)、模糊加密(支持模糊查询)
  3. 相关做法: 数据库隐私字段加密以及加密后的数据如何进行模糊查询? – 业余草

MyBatis-Plus 实现(做法:使用 常规2 处理)

  • 整体加密数据库字段长度与保存值长度对应(需要额外存储一个标识符)

    数据库字段长度 字符长度
    varchar(49) 12个中文字符、36个ascii字符
    varchar(97) 24个中文字符、72个ascii字符、3个ascii字符+23个中文字符、68个ascii字符+1个中文字符
    varchar(197) 49个中文字符、145个ascii字符、3个ascii字符+48个中文字符 、 142个ascii字符+1个中文字符
    varchar(253) 63个中文字符、189个ascii字符 、3个ascii字符+62个中文字符 、 186个ascii字符+1个中文字符
  • 模糊加密数据库字段长度与保存值长度对应(4个字节加密一次大致对应8个字符+1个标识符),根据4位英文字符(半角),2个中文字符(全角)为一个检索条件

    数据库字段长度 字符长度
    varchar(99) 12个中文字符、14个ascii字符
    varchar(198) 23个中文字符、25个ascii字符
    varchar(252) 29个中文字符、31个ascii字符

1 :利用 TypeHandler 支持数据加解密转换

INSERT VALUE、UPDATE ENTITY、SELECT RESULT
只对数据库和程序之间的数据转换,查询条件不会调用。

  • 整体加密类型处理器

    使用示例:
    1. MyBatis-Plus 注解(自动生产 ResultMap ,存在场景不生效)
        @TableField(typeHandler = OverallCryptoTypeHandler.class)
    2. 自定义 ResultMap 配置
        <result column="phone" property="phone" typeHandler="cn.eastx.practice.demo.crypto.config.mp.OverallCryptoTypeHandler" />
    
    整体加密类型处理器 OverallCryptoTypeHandler.java
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        /*
            对非null参数值进行加密,需要通过实体类处理方可,支持 INSERT/UPDATE ENTITY
            当前处理 INSERT ENTITY,UPDATE ENTITY 会先通过拦截器处理
            因为拦截器修改元数据将导致实体类属性值产生变更,所以实体类还是由 TypeHandler 来进行处理
        */
        ps.setString(i, CryptoDataUtil.overallEncrypt(parameter));
    }
    
    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        // 对可为null的结果进行解密
        return CryptoDataUtil.decrypt(rs.getString(columnName));
    }
    
  • 模糊加密类型处理器
    注意:根据4位英文字符(半角),2个中文字符(全角)为一个检索条件,如果字段值较少查询可能存在问题

        使用示例:
        1. MyBatis-Plus 注解(自动生产 ResultMap ,存在场景不生效)
            @TableField(typeHandler = FuzzyCryptoTypeHandler.class)
        2. 自定义 ResultMap 配置
            <result column="phone" property="phone" typeHandler="cn.eastx.practice.demo.crypto.config.mp.FuzzyCryptoTypeHandler" />
    
    模糊加密类型处理器 FuzzyCryptoTypeHandler.java
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {
        /*
            对非null参数值进行加密,需要通过实体类处理方可,支持 INSERT/UPDATE ENTITY
            当前处理 INSERT ENTITY,UPDATE ENTITY 会先通过拦截器处理
            因为拦截器修改元数据将导致实体类属性值产生变更,所以实体类还是由 TypeHandler 来进行处理
        */
        ps.setString(i, CryptoDataUtil.fuzzyEncrypt(parameter));
    }
    
    @Override
    public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
        // 对可为null的结果进行解密
        return CryptoDataUtil.decrypt(rs.getString(columnName));
    }
    

2 :利用字段注解配合 MyBatis 拦截器对条件进行拦截处理

注:目前仅支持简单查询处理,复杂查询可能存在问题。

  • 自定义字段注解 CryptoCond.java

    • 使用 replacedColumn() 替换 SQL 查询条件中的字段名
    • 使用 encryption() 对 SQL 中条件值、参数值进行加密,支持两种方式(整体匹配、模糊匹配)
    • 使用示例:User.java
  • 自定义 MyBatis 拦截器

    • 实现 Interceptor 接口,重写拦截器拦截 SQL 逻辑
    • 拦截器执行在 TypeHandler 之前,注意避免冲突
    自定义 MyBatis 拦截器 CryptoCondInterceptor.java
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        MappedStatement mappedStatement =
                (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        // 支持处理 SELECT、UPDATE、DELETE
        boolean canHandler = Stream.of(SqlCommandType.SELECT, SqlCommandType.UPDATE,
                        SqlCommandType.DELETE)
                .anyMatch(item -> item.equals(mappedStatement.getSqlCommandType()));
        if (canHandler && !getIntercept()) {
            clearIntercept();
            return invocation.proceed();
        }
    
        clearIntercept();
        // 判断是否有参数需要处理
        BoundSql boundSql = statementHandler.getBoundSql();
        if (Objects.isNull(boundSql.getParameterObject())) {
            return invocation.proceed();
        }
    
        // 获取自定义注解,通过 MapperID 获取到 Mapper 对应的实体类,获取实体类所有注解字段与注解对应 Map
        Map<String, CryptoCond> condMap = mapEntityFieldCond(mappedStatement.getId());
        if (CollectionUtil.isNotEmpty(condMap)) {
            replaceHandle(mappedStatement.getConfiguration(), condMap, boundSql);
        }
        return invocation.proceed();
    }
        
    // 替换数据处理
    private void replaceHandle(Configuration configuration, Map<String, CryptoCond> condMap,
                            BoundSql boundSql) {
        String sql = boundSql.getSql();
        List<SqlCondOperation> operationList = SqlUtil.listSqlCondOperation(sql);
        if (CollectionUtil.isEmpty(operationList)) {
            return;
        }
        MetaObject paramMetaObject = configuration.newMetaObject(boundSql.getParameterObject());
        List<ParameterMapping> mappings = boundSql.getParameterMappings();
        int condParamStart = SqlUtil.getSqlCondParamStartIdx(sql);
        int mappingStartIdx = 0;
        for (SqlCondOperation operation : operationList) {
            String columnName = operation.getColumnName();
            String condStr = operation.getOriginCond();
            int condNum = SqlUtil.countPreparePlaceholder(condStr);
            CryptoCond ann = condMap.get(operation.getColumnName());
            if (Objects.nonNull(ann)) {
                // 替换查询条件参数中的列名
                if (StrUtil.isNotBlank(ann.replacedColumn())
                        && condParamStart < operation.getOriginCondStartIdx()) {
                    sql = sql.replace(condStr,
                            condStr.replace(columnName, ann.replacedColumn()));
                }
                // 替换属性值为加密值
                if (condNum == 0) {
                    // 存在非预编译语句条件,直接替换 SQL 条件值
                    String propVal = String.valueOf(paramMetaObject.getValue(columnName));
                    String useVal = getCryptoUseVal(ann, propVal);
                    sql = sql.replace(condStr, condStr.replace(propVal, useVal));
                } else {
                    // 预编译语句条件通过替换条件值处理
                    for (int i = 0; i < condNum; i++) {
                        String propName = mappings.get(mappingStartIdx + i).getProperty();
                        if (!propName.startsWith("et.")) {
                            // 非实体类属性进行值替换,实体类属性通过 TypeHandler 处理
                            String propVal = String.valueOf(paramMetaObject.getValue(propName));
                            paramMetaObject.setValue(propName, getCryptoUseVal(ann, propVal));
                        }
                    }
                }
            }
            mappingStartIdx += condNum;
        }
        ReflectUtil.setFieldValue(boundSql, "sql", sql);
    }
    

3 :相关汇总

测试

  • 执行 /resources/db/schema.sql 创建数据库( java-practice-demos )、示例表( crypto-user )
  • 用户表 Service 层 IUserService.java
  • 用户表 Service 层 测试 UserServiceTest.java
    测试结果

使用问题

  • TypeHandler 不起效

    • 在 xml 中自定义 ResultMap ,示例:UserMapper.xml
    点击查看代码 UserMapper.xml
    <!-- 使用自定义SQL时,对于加密处理需要使用ResultMap作为返回对象,否则对解析成实际数据会存在问题 -->
    <resultMap id="BaseResultMap" type="cn.eastx.practice.demo.crypto.pojo.po.User">
        <result column="id" property="id" />
        <result column="name" property="name" />
        <result column="password" property="password" />
        <result column="salt" property="salt" />
        <result column="phone" property="phone" typeHandler="cn.eastx.practice.demo.crypto.config.mp.OverallCryptoTypeHandler" />
        <result column="email" property="email" typeHandler="cn.eastx.practice.demo.crypto.config.mp.FuzzyCryptoTypeHandler" />
        <result column="create_time" property="createTime" />
        <result column="update_time" property="updateTime" />
    </resultMap>
    
    • 在实体类上增加 @TableName(autoResultMap = true) ,自动构建 ResultMap
    • https://gitee.com/baomidou/mybatis-plus/issues/I103ZO
  • 加密后解密数据乱码

    • 可能是密钥存在问题,建议重新生成
  • CryptoCondInterceptor 不起效

    • 设置 CryptoCondInterceptor.setIntercept(false)
    • 在实体类上相应字段设置 @CryptoCond ,示例:User.java
    用户实体类 User.java
    @Data
    @TableName(value = "crypto_user", autoResultMap = true)
    public class User {
        /**
        * 用户表主键ID
        */
        private Long id;
        /**
        * 用户名
        */
        private String name;
        /**
        * 加密后的密码,MD5加盐
        */
        private String password;
        /**
        * 加密密码使用的盐
        */
        private String salt;
        /**
        * 手机号码,整体加密
        */
        @TableField(typeHandler = OverallCryptoTypeHandler.class)
        @CryptoCond(encryption = CryptoCond.EncryptionEnum.DEFAULT_OVERALL)
        private String phone;
        /**
        * 邮箱,模糊加密
        */
        @TableField(typeHandler = FuzzyCryptoTypeHandler.class)
        @CryptoCond(encryption = CryptoCond.EncryptionEnum.DEFAULT_FUZZY)
        private String email;
        /**
        * 创建时间
        */
        @TableField(fill = INSERT)
        private LocalDateTime createTime;
        /**
        * 更新时间
        */
        @TableField(fill = INSERT_UPDATE)
        private LocalDateTime updateTime;
    }
    
    • Mapper 实现 BaseMapper 并指定实体类,示例:public interface UserMapper extends BaseMapper<User>
    • 将拦截器加入 Spring IOC 管理
  • MySQL 异常

    • 索引长度
      MySQL 默认索引长度最大长度是767bytes
      Specified key was too long; max key length is 3072
      bytes

参考

  • mybatis(mybatis-plus)使用sql拦截器和自定义注解获取sql和参数

其他

demo 地址:https://github.com/EastX/java-practice-demos/tree/main/demo-crypto

推荐阅读:

  • 数据库隐私字段加密以及加密后的数据如何进行模糊查询? – 业余草
hmoban主题是根据ripro二开的主题,极致后台体验,无插件,集成会员系统
自学咖网 » 数据加密 – 数据库隐私字段组件