Spring-Web - 数据库 字段加密 解密

  在工作中,为了保证数据安全,需要对数据库字段进行加解密,之前工作中就遇到了这种情况,因为线上数据库有很多的人都有权限,运维,账务,运营(通过后台系统查看),出口太多了,但有用户向我们平台举报,说有骚扰电话 ,能精准的知道用户姓名,手机号, 身份证号 ,以及一些用户在我们平台的账户数据,没有办法,数据被泄漏了,肯定不是阿里云数据库不安全导致的, 而是内部人员将数据给泄漏出去,因为出口太多,人员也太多,不知道怎么查询,公司决定将数据库中所有的用户的姓名,手机号, 身份证三要素加密,从数据库中无法看到三要素的名文,从而缩小数据导出人群的范围,因为这样做可能只有开发人员才能导出了。 其他运营,运维都只能看到加密的数据,如果没有密钥的话,是肯定解密不了数据,而后台做了一个功能,能将密文的excel导入,并转化为明文,但每一次解密的条数有限制,而且每次谁使用了导出明文功能,将消息发送钉钉群,这样一般人基本上就拿不到明文数据了,从而保证了数据的安全性。

  理想确实是好的,但是这却累死开发人员了,项目中几十张表中的字段都需要加密解密, 相当于每一张表的插入,查询出的数据,查询条件都需要加密,这工作量非常的大,同时为后期的维护带来无穷尽的麻烦 , 每一次插入,查询条件,查询得出的结果都要想到这个字段是否是加密字段,如果是,则需要做相关的转化,几十张表都要记下来,太麻烦了, 同时新加的表也要考虑这些事情,而原本整洁的代码可能会因为加上这些加密解密变得臃肿不堪,有没有一种好的方案来解决这个问题呢?如开发人员只需要配置哪些表的哪些字段需要加密解密,而在代码中不需要做任何修改,保证保存到数据库中的是密文,取出的是名文,而查询也没有影响,按原来的操作即可, 插件会根据需要将相应的查询条件进行加密,如果这能实现那该多好啊,对开发人员没有任何工作量,而扩展性和维护性那就勿庸置疑了,如果想实现这样一个功能,该怎样做呢?

  既然有了理想,接下来就是怎么去实现这样一款插件的功能了,再看插件如何开发之前,我们先来看一下插件的效果 。

  1. 创建表

CREATE TABLE lt_user_phone (
id bigint(20) NOT NULL AUTO_INCREMENT COMMENT ‘主键,自增id’,
is_delete tinyint(4) NOT NULL DEFAULT ‘0’ COMMENT ‘是否删除状态,1:删除,0:有效’,
gmt_create datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ‘创建时间’,
gmt_modified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT ‘最后修改时间’,
user_name_en varchar(64) NOT NULL DEFAULT ‘’,
real_name_en varchar(64) NOT NULL DEFAULT ‘’,
unique_code varchar(32) NOT NULL DEFAULT ‘’,
cnl_code varchar(64) DEFAULT ‘’,
type tinyint(4) NOT NULL DEFAULT ‘1’,
PRIMARY KEY (id),
KEY Index_uniqueCode (unique_code) USING BTREE,
KEY index_user_name_en (user_name_en) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT=‘用户表’;

CREATE TABLE lt_stage_borrow (
id bigint(11) unsigned NOT NULL AUTO_INCREMENT COMMENT ‘自增主键’,
is_delete bigint(4) NOT NULL DEFAULT ‘0’ COMMENT ‘是否删除’,
gmt_create datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
gmt_modified datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
unique_code varchar(20) DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_borrowcach_uid (unique_code)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COMMENT=‘分期借款表’;

  【注意】lt_user_phone 和 lt_stage_borrow 这两个表名为了和公司的用户表,记录表有所区别,所以这两张表名是我随意取的,字段为了和之前的有所区别,因此字段名也是随便弄的,方便和公司的业务表区分开,所以大家看起来可能觉得费解,如lt_stage_borrow 表存一个user_id 不就可以了不?还用unique_code来关联有什么意义。就是为了和公司相关的信息抹除,所以大家只关注技术即可,不需要关注字段含义。

  1. 配置字段,有些简单了,有兴趣可以写在配置文件中
@Slf4j
@Configuration
public class MybatisPluginConfig {

    //初始化变量
    @PostConstruct
    public void postConstruct() {
        log.info("开始初始化表数据");
        EncryptTableConfig.tableConfig.put("lt_user_phone", new PPTuple(Arrays.asList(new String[]{"real_name_en", "user_name_en", "id_number_en"}),
                Arrays.asList(new String[]{})));
    }
}

  配置文件的目的就是配置lt_user_phone表的real_name_en,user_name_en,id_number_en字段需要加密解密字段

  1. 配置加密解密的bean
@Bean(name = "sqlSessionFactory")
public SqlSessionFactory initSqlSessionFactory( PaginationInterceptor paginationInterceptor) throws Exception {
    MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
    sessionFactory.setDataSource(initDataSource());
    sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver()
            .getResources(env.getProperty("mybatis-plus.mapper-locations")));
    sessionFactory.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
    Interceptor[] interceptor = {paginationInterceptor,new DataScopeInterceptor(),new DataPrepareEncryptScopeInterceptor(),
            new DataEncryptScopeInterceptor(),new RestoreDataScopeInterceptor(),new QueryDecryptScopeInterceptor(),new MapF2FInterceptor()};
    sessionFactory.setPlugins(interceptor);
    return sessionFactory.getObject();
}

  DataPrepareEncryptScopeInterceptor,DataEncryptScopeInterceptor,RestoreDataScopeInterceptor,QueryDecryptScopeInterceptor 加密解密相关的插件,为什么这么配置,后面再来分析

  1. 先来看插入效果
// http://localhost:8502/api/test1
@RequestMapping("/test1")
public String test1() throws Exception {
    UserPhone userPhone = new UserPhone();
    userPhone.setUserNameEn("18488888888");
    userPhone.setRealNameEn("张三");
    userPhone.setIdNumberEn("483898329832983");
    userPhone.setUserNo("89329832");
    userPhone.setCnlCode("chenn");
    userPhone.setChannelCode("3232");
    userPhone.setType(1);
    userPhoneDao.insertUserPhone(userPhone);
    return "sucess";
}

在这里插入图片描述
  在代码中,我没有对任何字段加密,但打印的sql中却体现了加密效果。再来看看数据库保存的数据怎样。

在这里插入图片描述
  相应的字段被加密保存到数据库中。

  1. 再来看另外一个效果,那查询结果会解密吗?
// http://localhost:8502/api/test2
@RequestMapping("/test2")
public String test2() throws Exception {
    UserPhone userPhone = userPhoneDao.selectUserPhoneByUserNameEn("18488888888");
    System.out.println(JSON.toJSONString(userPhone));
    return "sucess";
}

  结果输出
在这里插入图片描述  从图中可以得知,在代码中输入的查询条件为18488888888,但sql打印出的查询条件已经被加密,在数据库中数据是被加密的,但是通过JSON.toJSONString(userPhone) JSON 打印,数据是被解密的。是不是感觉几招下来,开发功底尽显。不吹牛逼了,继续来看问题。

  从上面两个小例子中,当插入数据时,能对数据库字段加密,当查询数据时,能对符合加密条件的字段加密,对不需要加密的字段则不会加密,大家不要有一种错觉,【我对所有的查询条件都加密,显然不是】,当然后面还会分析更新,批量更新,分页查询等遇到的问题及解决方案。

  如果我们能精准的知道哪些查询条件字段需要被加密,当然还包括子查询 ,这必须涉及到一个问题,就是SQL解析,我们在mybatis插件中能得到如下SQL : SELECT * FROM lt_user_phone WHERE is_delete = 0 AND user_name_en = ? LIMIT 1
  其实我们只需要知道SQL中每个所代表的是哪个字段,并且这个字段属于哪张表,即可与我们配置信息(配置了哪张表的哪些字段需要被加密解密)中的字段和表比对,如果与配置表中配置的表名和字段名相匹配,则表示这个字段是需要被加密或解密的,从而对mybatis中传入数据进行加密即可。 既然有了想法,那么接下来看如何做。
  我们需要准备一个SQL解析包,能解析出每条SQL中?是哪个字段的条件并且属于哪张表?说是这么说,但怎么实现呢? SQL又有子查询,又有关联查询,又有子查询套子查询,我的天呐,不可能实现的嘛,这明显是写一个SQL解析器嘛。
  当然,感谢互联网时代,感谢阿里,他提供了一个druid的包,使得项目得到进展 , 当然我个人是非常喜欢和崇拜阿里的,他提供了太多的开发框架,如 Druid ,Dubbo,Rocketmq,Arthas,Nacos,Sentinel等,我觉得这些框架都值得去研究,回归正题。
  因为Druid包本身提供了对SQL的解析功能,只需要将这个包改成我们想要的为止,也忘记了曾经3天没日没夜的修改,最后比较完善,包路径为https://github.com/quyixiao/sql-parser.git
  既然如此,我们来看包的执行效果。

public class TestSql2 {

    public static void printSql(String sql) {
        // 新建 MySQL Parser
        SQLStatementParser parser = new MySqlStatementParser(sql);
        SQLExprParser exprParser = parser.getExprParser();
        Lexer lexer = exprParser.getLexer();
        SQLObjectImpl.lexer = lexer;

        // 使用Parser解析生成AST,这里SQLStatement就是AST
        SQLStatement statement = parser.parseStatement();
        // 使用visitor来访问AST
        MySqlSchemaStatVisitor visitor = new MySqlSchemaStatVisitor();

        statement.accept(visitor);
        // 从visitor中拿出你所关注的信息
        Collection<TableStat.Column> list = visitor.getColumns();

        List<Cell> header = new ArrayList<Cell>() {{
            add(new Cell("tableName"));
            add(new Cell("column"));
            add(new Cell("select"));
            add(new Cell("where"));
            add(new Cell("join"));
            add(new Cell("groupBy"));
            add(new Cell("having"));
            add(new Cell("fullName"));
            add(new Cell("dataType"));
            add(new Cell("pos"));
            add(new Cell("test"));
        }};
        List<List<Cell>> body = new ArrayList<List<Cell>>();
        for (TableStat.Column column : list) {
            List<Cell> cells = new ArrayList<Cell>();
            cells.add(new Cell(column.getTable()));
            cells.add(new Cell(column.getName()));
            cells.add(new Cell(column.isSelect() + ""));
            cells.add(new Cell(column.isWhere() + ""));
            cells.add(new Cell(column.isJoin() + ""));
            cells.add(new Cell(column.isGroupBy() + ""));
            cells.add(new Cell(column.isHaving() + ""));
            cells.add(new Cell(column.getFullName() + ""));
            cells.add(new Cell(column.getDataType() + ""));
            cells.add(new Cell(column.getPos() + ""));
            if (column.getPos() > 10) {
                cells.add(new Cell(sql.substring(column.getPos(), column.getPos() + column.getName().length())));
            } else {
                cells.add(new Cell(""));
            }
            body.add(cells);
        }
        String a = new ConsoleTable.ConsoleTableBuilder().addHeaders(header).addRows(body).build().toString();
        System.out.println(a);
    }

    @org.junit.Test
    public void test() {
        String sql = "select * from lt_user_phone up  left join lt_stage_borrow sb  on up.unique_code = sb.unique_code where up.user_name = ? and up.real_name = ? and sb.status = ? ";
        System.out.println(sql);
        printSql(sql);
    }

}

  sql为select * from lt_user_phone up left join lt_stage_borrow sb on up.unique_code = sb.unique_code where up.user_name = ? and up.real_name = ? and sb.status = ? ,SQL的意思很简单吧, 通过lt_user_phone这张表的user_name和real_name关联查询
lt_stage_borrow表,并且lt_stage_borrow.status为查询条件,意思就是通过lt_user_phone的user_name和real_name字段及lt_stage_borrow表的status字段查询表的所有数据 。

在这里插入图片描述
  从执行结果来看,是不是拿到了? 对应的表名和字段名,可能大家觉得这个是比较简单的sql ,我们再来看更加复杂的sql,执行效果 。

在这里插入图片描述
  我们再次写了一条复杂的sql , 其中传入了5个查询条件,毫无疑问,结果是正确的,当年我在测试这个包的时候,测试了几百条SQL,最终将发现的问题都修复了,而且现在已经在1000万用户以上的平台线上运行了2年左右,所以这款插件的准确性还是不用担心的,目前没有发现什么问题, 放心使用即可。
  关于sql-parser插件的源码感兴趣可以自行去研究,当然感兴趣建议去研究Druid源码,里面涉及到SQL 的解析,当然不要想得有什么捷径,和其他语言解析器一样,都是一个字符一个字符的读取,最终构建成一颗语法树,当然呐,Druid插件的源码解析也并非一朝一夕阳能说清楚,也不是一篇博客能说明白,先放着,以后有机会再来写相关的博客 。

  1. 查询条件加密开始修改,我们写一个DataQueryEncryptScopeInterceptor mybatis拦截器。
@Slf4j
@Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})})
public class DataQueryEncryptScopeInterceptor extends SqlParserHandler implements Interceptor {

    public DataQueryEncryptScopeInterceptor() {

    }

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        // 先判断是不是SELECT操作
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        if (SqlCommandType.SELECT.equals(mappedStatement.getSqlCommandType())) {           //非select语句
            this.sqlParser(metaObject);
            ParameterHandler parameterHandler = (ParameterHandler) metaObject.getValue("delegate.parameterHandler");
            String mapperdId = PSqlParseUtil.getMapperId(mappedStatement);
            Configuration configuration = mappedStatement.getConfiguration();
            encrySqlParamData(configuration, parameterHandler, mapperdId, boundSql);
            Object result = invocation.proceed();
            return result;
        } else {
            return invocation.proceed();
        }
    }

    @Override
    public Object plugin(Object target) {
        if (target instanceof StatementHandler) {
            //使用MyBatis提供的Plugin类生成代理对象
            return Plugin.wrap(target, this);
        }
        return target;
    }
  
    @Override
    public void setProperties(Properties properties) {

    }
}

  其他代码都是mybatis 的常规操作,而encrySqlParamData代码就是我们查询条件加密的方法 。

public void encrySqlParamData(Configuration configuration, ParameterHandler parameterHandler, String mapperdId, BoundSql boundSql) {
    PTuple2<Boolean, Map<Integer, String>> data = null;
    String sql = boundSql.getSql();
    // 不能每条sql都进行解析,对于相同的SQL
    // 只做一次解析,将解析的结果以 mapperdId + md5(sql) 组成key
    // 存储到缓存中
    String key = mapperdId + PMD5Util.encode(sql);
    try {
        PPTuple pluginTuple = enterMap.get(key);
        if (pluginTuple == null) {
            // data 为元组
            // 第一个对象标识SQL中有没有需要被加密解密的字段
            // 第二个对象为一个map , key 为 加密字段出现的位置,value 为字段名称
            // 如select * from user where status = ? and user_name_en = ?
            // 而status 不需要被加密解密 ,user_name_en 需要被加密解密
            // 则map 中存储的为 key  = 1 , value = user_name_en
            data = getChangeColumn(configuration, mapperdId, sql, key).getData();
        } else {
            data = pluginTuple.getData();
        }
        // 这里使用了元组,虽然Java 使用元组不太好用
        // 那还是用一下吧, first存储的是这条SQL有没有字段需要被加密解密
        // 如果first 为true,表示SQL中有字段需要被加密,如果为false
        // SQL 中没有任何字段需要被加密解密
        if (!data.getFirst()) {
            return;
        }
        
        Map<Integer, String> paramMaps = data.getSecond();
        Object parameterObject = boundSql.getParameterObject();
        StringBuffer sb = new StringBuffer();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();

        if (parameterMappings.size() > 0 && parameterObject != null) {
            // 如果parameterObject 是字符串,并且
            // 并且 parameterMappings 只有一个参数
            if (parameterMappings != null && parameterMappings.size() == 1
                    && parameterObject != null && parameterObject instanceof String) {
                // 通过反射修改 parameterObject 属性值
                // 为加密后的值
                Object encode = encode(parameterObject + "");
                setFieldValue(parameterHandler, "parameterObject", encode);
                sb.append("(").append(parameterObject).append(" -> ").append(encode).append("),");
            } else {
                MetaObject metaObject = configuration.newMetaObject(parameterObject);
                if (parameterObject instanceof DefaultSqlSession.StrictMap) {       //表示一个集合
                    Set<String> keySet = ((DefaultSqlSession.StrictMap<?>) parameterObject).keySet();
                    // 对于查询条件是一个list 或 collection时
                    // 需要将list或collection对象的值给替换掉,这个和mybatis源码相关
                    if(keySet.contains("list")){
                        List<Object> list = (List<Object>) metaObject.getValue("list");
                        if (list != null && list.size() > 0) {
                            Object object = list.get(0);
                            if(object instanceof  String){
                                setValueOrSetAdditionalParameter(paramMaps,parameterMappings,boundSql,metaObject ,sb );
                            }else{
                                List<Object> collection = (List<Object>) metaObject.getValue("collection");
                                int i = 0 ;
                                for (Map.Entry<Integer, String> paramMap : paramMaps.entrySet()) {
                                    MetaObject meta1 = configuration.newMetaObject( list.get(i));
                                    MetaObject meta2 = configuration.newMetaObject( collection.get(i));
                                    String propertyName = PSqlParseUtil.field2JavaCode(paramMap.getValue());
                                    Object temp = meta1.getValue(propertyName);
                                    Object value = encode(temp);
                                    meta1.setValue(propertyName, value);
                                    meta2.setValue(propertyName, value);
                                    i ++;
                                    sb.append("(").append(propertyName).append(" : ").append(temp).append(" -> ").append(value).append("),");
                                }

                            }
                        }
                    }else{
                        // 如果请求参数是一个数组
                        Object[] array = (Object[]) metaObject.getValue("array");
                        if (array != null && array.length > 0) {
                            Object object = array[0];
                            if(object instanceof  String){
                                setValueOrSetAdditionalParameter(paramMaps,parameterMappings,boundSql,metaObject ,sb );
                            }else{
                                int i = 0 ;
                                for (Map.Entry<Integer, String> paramMap : paramMaps.entrySet()) {
                                    MetaObject meta1 = configuration.newMetaObject(array[i]);
                                    String propertyName = PSqlParseUtil.field2JavaCode(paramMap.getValue());
                                    Object temp = meta1.getValue(propertyName);
                                    Object value = encode(temp);
                                    meta1.setValue(propertyName, value);
                                    i ++;
                                    sb.append("(").append(propertyName).append(" : ").append(temp).append(" -> ").append(value).append("),");
                                }
                            }
                        }
                    }
                } else {
                    setValueOrSetAdditionalParameter(paramMaps,parameterMappings,boundSql,metaObject ,sb );
                }
            }
        }
        String a = sb.toString();
    } catch (Exception e) {
        try {
            PDingDingUtils.sendText("异常编号 =" + Logger.inheritableThreadLocalNo.get() + "\n key =" + key + "\n mapperId =" + mapperdId + "\n sql = " + boundSql.getSql() + "\n 异常堆栈 = " + PExceptionUtils.dealException(e));
            PPTuple pluginTuple = enterMap.get(key);
            log.error("sql insert/update/select/delete 加密异常 exception sql = " + sql + ", key = " + key + " ,mappid = " + mapperdId + " ,data =   " + JSON.toJSONString(data) + ", pluginTuple = " + JSON.toJSONString(pluginTuple), e);
        } catch (Exception ex) {
            log.error("异常xxx", e);
        }
    }
}

public void setValueOrSetAdditionalParameter(Map<Integer, String> paramMaps, List<ParameterMapping> parameterMappings,
                                             BoundSql boundSql, MetaObject metaObject, StringBuffer sb) {
	// paramMaps中存储的是SQL中第几个问号需要被加密的字段
    for (Map.Entry<Integer, String> paramMap : paramMaps.entrySet()) {
        int index = paramMap.getKey();
        ParameterMapping parameterMapping = parameterMappings.get(index);
        String propertyName = parameterMapping.getProperty();
        PTuple2<Boolean, Object> indetValue = getIndexObject(boundSql, metaObject, propertyName, index).getData();
        Object encode = encode(indetValue.getSecond());
        metaObject.setValue(propertyName, encode);
        sb.append("(").append(propertyName).append(" : ").append(indetValue.getSecond()).append(" -> ").append(encode).append("),");
        if (indetValue.getFirst()) {
            boundSql.setAdditionalParameter(propertyName, encode);
        }
    }
}

  因为上面的代码涉及到很多mybatis源码相关的知识,在之前的mybatis源码系中 深入浅出MyBatis技术原理与实战-学习-源码解析-MyBatis 运行原理(四) 详细的分析了mybatis 运行原理相关的知识,感兴趣可以去读一下, 这里就不再深入分析mybatis源码了,为了方便大家理解代码,我们通过例子的方式来分析。
  所有的业务逻辑都在上面这些代码中了, 但是理解起来还是很抽象的,paramMaps 到底存储的是什么东西呢? 我们来看一个例子。

Controller
// http://localhost:8502/api/test7
@RequestMapping("/test7")
public String test7() throws Exception {
    String [] arrays = new String[]{"18411111111","18422222222","18433333333","18444444444"};
    List<UserPhone> userPhone = userPhoneDao.selectUserPhoneByArrayUserNameEnsAndRealName(arrays,"张三") ;
    System.out.println(JSON.toJSONString(userPhone));

    return "sucess";
}

Mapper
List<UserPhone> selectUserPhoneByArrayUserNameEnsAndRealName(@Param("arrays") String[] arrays,@Param("realNameEn") String realNameEn);
Mapper.xml
<select id="selectUserPhoneByArrayUserNameEnsAndRealName" resultType="com.data.en.decoder.entity.user.UserPhone">
    select *   from lt_user_phone where  user_name_en in
    <foreach collection="arrays" separator="," open="(" close=")" item="item">
        #{item}
    </foreach>
    and real_name_en = #{realNameEn}
</select>

  相信大家对上面写法再熟悉不过了
  boundSql.getSql()为 select * from lt_user_phone where user_name_en in (?,?,?,?) and real_name_en = ? ,有5个问号
在这里插入图片描述

  getChangeColumn()方法返回的map为{0:“user_name_en”,1:“user_name_en”,2:“user_name_en”,3:“user_name_en”,4:“real_name_en”}
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
  上面有没有发现一个特点,对于普通对象,直接通过反射修改其值即可,但是对于 boundSql.hasAdditionalParameter(propertyName) 为true时,则需要调用boundSql.setAdditionalParameter(propertyName, encode); 来修改他的值,也就是对于 ParameterMapping的property=为__frch_item_0,__frch_item_1,__frch_item_2,__frch_item_3时,则需要修改__frch_item_0的值,不然不生效,为什么这么做呢?我们看一下mybatis的源码 。

  DefaultParameterHandler 参数处理器中的setParameters方法中。

 public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
        for (int i = 0; i < parameterMappings.size(); i++) {
            ParameterMapping parameterMapping = parameterMappings.get(i);
            if (parameterMapping.getMode() != ParameterMode.OUT) {
                Object value;
                String propertyName = parameterMapping.getProperty();
                if (boundSql.hasAdditionalParameter(propertyName)) { // issue #448 ask first for additional params
                    value = boundSql.getAdditionalParameter(propertyName);
                } else if (parameterObject == null) {
                    value = null;
                } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {
                    value = parameterObject;
                } else {
                    MetaObject metaObject = configuration.newMetaObject(parameterObject);
                    value = metaObject.getValue(propertyName);
                }
                TypeHandler typeHandler = parameterMapping.getTypeHandler();
                JdbcType jdbcType = parameterMapping.getJdbcType();
                if (value == null && jdbcType == null) {
                    jdbcType = configuration.getJdbcTypeForNull();
                }
                try {
                    typeHandler.setParameter(ps, i + 1, value, jdbcType);
                } catch (TypeException e) {
                    throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
                } catch (SQLException e) {
                    throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
                }
            }
        }
    }
}

  因此boundSql.hasAdditionalParameter(propertyName)为true时,需要调用boundSql.setAdditionalParameter(propertyName, encode); 查询条件才起作用。 只有在最后才从 metaObject.getValue(propertyName); 获取属性值,可能大家觉得这有点断章取义,但是没有办法 ,mybatis源码就是这样写的 。大家可能感兴趣paramMaps的获取很巧妙嘛,完全迎合了mybatis源码,那又是怎样实现的呢?

public PPTuple getChangeColumn(Configuration configuration, String mapperdId, String sql, String key) {
    PPTuple pluginTuple = enterMap.get(key);
    if (pluginTuple != null) {
        return pluginTuple;
    }
    log.info(" 没有从缓存中获取数据,直接生成数据,key= " + key + ", sql = " + sql);
    // 如果sql = ? ,则直接返回 
    // 在mapper.xml 中 直接 通过 ${sql} 来执行sql ,对于这种情况
    // 不做加密解密处理
    if ("?".equals(sql.trim())) {
        pluginTuple = new PPTuple(false, null);
        enterMap.put(key, pluginTuple);
        log.info("sql是问号 : " + mapperdId + "sql  : " + sql + ",key = " + key + ",pluginTuple = " + JSON.toJSONString(pluginTuple));
        return pluginTuple;
    }

    Map<Integer, String> linkedHashMap = new LinkedHashMap<>();
    
    PTuple2<Collection<TableStat.Column>, List<String>> data = PSqlParseUtil.getParserInfo(sql).getData();
    Collection<TableStat.Column> columns = data.getFirst();
    // 当sql 中的问号和 sql-parser框架解析出来的问号个数不相等
    // 并且sql 中不包含in 时,打印提示信息,其实也没有什么用,用来找问题方便 
    if (columns != null) {
        int count = PStringUtils.getStringCountKey(sql, "?");
        String message = " 解析sql得到的个?数为 " + columns.size() + ",sql中的?个数为 " + count;
        if (count != columns.size() && PStringUtils.getStringCountKey(sql.toLowerCase(), "in") <= 0) {
            log.info("message = " + message + " ,mappId = " + mapperdId + ", sql = " + sql + ",key=" + key);
        }
    }
    
    List<String> tableNames = data.getSecond();
    Map<String, PPTuple> tableConfig = EncryptTableConfig.tableConfig;
    int i = 0;
    boolean canCache = true;
    for (TableStat.Column column : columns) {
        String tableName = PSqlParseUtil.getRealName(column.getTable());
        String columnName = PSqlParseUtil.getRealName(column.getName());
        // 如sql 为 select *   from lt_user_phone where  user_name_en in (?,?,?,?) and real_name_en = ?
        // 解析出表名 和 列名为
        // lt_user_phone user_name_en
		// lt_user_phone user_name_en
		// lt_user_phone user_name_en
		// lt_user_phone user_name_en
		// lt_user_phone real_name_en
		// 如果表名和列名为我们所配置需要加密的表字段 
		// 则将结果加入到linkedHashMap中
        if (tableConfig.containsKey(tableName)) {
            if (PSqlParseUtil.notEqQuestionMark(columnName)) {       //
                PTuple2<List<String>, List<String>> columnNameData = tableConfig.get(tableName).getData();
                List<String> columnNames = columnNameData.getFirst();
                if (columnNames.contains(columnName)) {
                    linkedHashMap.put(i, columnName);
                }
            }
        // 这里涉及到另外一种考虑 
        // select * from lt_user_phone up left join lt_borrow b on up.id = b.user_id   where up.user_name_en = ? and status = ? 
        // 当lt_user_phone表没有status字段时 ,但lt_borrow 表有status字段时 
        // 这样写sql 是不会有问题,解析status是就不知道属于哪张表了 
        // 需要通过sql每一张表的所有字段,判断其是否有status字段,如果有
        // 则这个字段属于这张表 
        } else if (PSqlParseUtil.isUnkown(tableName) && PSqlParseUtil.notEqQuestionMark(columnName)) { //如果表名为UNKOWN,但column 不为空时
            int flag = 0;
            for (String realTableName : tableNames) {
                if (tableConfig.containsKey(realTableName)) {
                    PTuple2<List<String>, List<String>> tableInfos = getTableInfo(configuration, realTableName).getData();
                    List<String> tableColumns = tableInfos.getSecond();
                    PTuple2<List<String>, List<String>> columnNameData = tableConfig.get(realTableName).getData();
                    List<String> columnNames = columnNameData.getFirst();
                    for (String tableColumn : columnNames) {
                        if (tableColumns.contains(tableColumn) && tableColumn.equals(columnName)) {
                            flag++;
                        }
                    }
                }
            }
            if (flag == 1) {
                linkedHashMap.put(i, columnName);
            } else {
                canCache = false;
            }
        }
        i++;
    }
    boolean isEncrypt = linkedHashMap.size() > 0;
    pluginTuple = new PPTuple(linkedHashMap.size() > 0, linkedHashMap);
    if (canCache) {
        enterMap.put(key, pluginTuple);
            log.info(" 加密信息获取 mapperdId : " + mapperdId + " , isEncrypt : " + isEncrypt + ",columns :" + JSON.toJSONString(pluginTuple) + ",key=" + key);
    } else {
        log.info(" 不加入缓存加密数据 mapperdId : " + mapperdId + " , isEncrypt : " + isEncrypt + ",columns :" + JSON.toJSONString(pluginTuple) + ",key=" + key);
    }
    return pluginTuple;
}

  对于第二种情况,当lt_user_phone 没有status字段,但lt_borrow有status字段,sql : select * from lt_user_phone up left join lt_borrow b on up.id = b.user_id where up.user_name_en = ? and status = ? ,这样写执行sql是没有问题的,但是在解析时会出现status不知道属于哪张表。
在这里插入图片描述
  因此对于这种情况需要动态的遍历所有的表,拿到每张表的字段,再进行比对,如果匹配上了,这个字段肯定属于这张表 。 因此写了下面的sql进行查询表中的所有字段
SELECT COLUMN_NAME columnName, DATA_TYPE dataType, COLUMN_COMMENT columnComment,COLUMN_KEY columnKey FROM INFORMATION_SCHEMA.COLUMNS WHERE table_name = ‘" + tableName + "’,当然这条sql最好是加上数据库名,这样会更加准确。
  为什么需要对parameterObject为DefaultSqlSession.StrictMap做单独的处理呢?看一下mybatis源码 。
在这里插入图片描述
  这段代码的意思是什么呢? 我们在写查询条件时,如果传入的是一个List对象或一个数组,我们在Mapper层方法参数可以不加@Param注解,而在Mapper.xml 的 foreach循环中 collection 可以写collection或list,如果是数组的话,可以写成 collection=“array”,先来看collection="list"的例子。

Controller
// http://localhost:8502/api/test4
@RequestMapping("/test4")
public String test4() throws Exception {
    List<UserPhone> userPhones = new ArrayList<>();
    UserPhone userPhone = new UserPhone();
    userPhone.setUserNameEn("18488888888");
    userPhones.add(userPhone);


    UserPhone userPhone1 = new UserPhone();
    userPhone1.setUserNameEn("18488888889");
    userPhones.add(userPhone1);


    List<UserPhone> userPhoneList = userPhoneDao.selectUserPhoneByUserNameEnEntitys(userPhones);
    System.out.println(JSON.toJSONString(userPhoneList));
    return "sucess";
}
Mapper
List<UserPhone> selectUserPhoneByUserNameEnEntitys(List<UserPhone> list);
Mapper.xml
<select id="selectUserPhoneByUserNameEnEntitys" resultType="com.data.en.decoder.entity.user.UserPhone">
    select *   from lt_user_phone where  user_name_en in
    <foreach collection="list" separator="," open="(" close=")" item="item">
        #{item.userNameEn}
    </foreach>
</select>
执行结果

在这里插入图片描述
  再来看一个例子,如果传入的是数组时,该怎样处理呢?

Controller
// http://localhost:8502/api/test6
@RequestMapping("/test6")
public String test6() throws Exception {
    UserPhone userPhone = new UserPhone();
    userPhone.setUserNameEn("18488888888");

    UserPhone userPhone1 = new UserPhone();
    userPhone1.setUserNameEn("18488888889");
    UserPhone [] arrays = new UserPhone[]{userPhone,userPhone1};

    List<UserPhone> userPhoneList = userPhoneDao.selectUserPhoneByUserNameArrayEnEntitys(arrays);
    System.out.println(JSON.toJSONString(userPhoneList));
    return "sucess";
}
Mapper
List<UserPhone> selectUserPhoneByUserNameArrayEnEntitys(UserPhone[] arraysxxx);
Mapper.xml
<select id="selectUserPhoneByUserNameArrayEnEntitys" resultType="com.data.en.decoder.entity.user.UserPhone">
    select *   from lt_user_phone where  user_name_en in
    <foreach collection="array" separator="," open="(" close=")" item="item">
        #{item.userNameEn}
    </foreach>
</select>
结果如下

在这里插入图片描述

  我们再来看另外一种情况,大家注意,这里打印的并不是查询出结果的值,而是请求参数的值

// http://localhost:8502/api/test8
@RequestMapping("/test8")
public String test8() throws Exception {
    UserPhone userPhone = new UserPhone();
    userPhone.setUserNameEn("18488888888");
    UserPhone data = userPhoneDao.selectUserPhoneByUsernNameObject(userPhone);
    System.out.println(JSON.toJSONString(userPhone));
    return "success";
}
Mapper
UserPhone selectUserPhoneByUsernNameObject(UserPhone userPhone);
Mapper.xml
<select id="selectUserPhoneByUsernNameObject" resultType="com.data.en.decoder.entity.user.UserPhone">
    select * from lt_user_phone where  user_name_en = #{userNameEn} limit 1
</select>

  看下面结果,如果使用userPhone作为查询条件,调用Mapper 方法之后,再在代码中使用,发现竟然userNameEn被加密了
在这里插入图片描述

  这显然不是我们想要的,我们的目的就是保证在代码中,一定是明文,而如果新来的开发小伙伴不小心使用了加密后的数据,带来不必要的麻烦 ,怎么办呢? 是怎样加密的,那就怎样解密回来不就好了不?这就是RestoreQueryDataScopeInterceptor实现类的由来

RestoreQueryDataScopeInterceptor
@Slf4j
@Intercepts({@Signature(type = StatementHandler.class, method = "query", args = {Statement.class, ResultHandler.class})})
public class RestoreQueryDataScopeInterceptor extends SqlParserHandler implements Interceptor {

    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = (StatementHandler) PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        this.sqlParser(metaObject);
        // 先判断是不是SELECT操作
        BoundSql boundSql = (BoundSql) metaObject.getValue("delegate.boundSql");
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement");
        String mapperdId = PSqlParseUtil.getMapperId(mappedStatement);
        Configuration configuration = mappedStatement.getConfiguration();
        Object result = invocation.proceed();
        try {
            decodeData(configuration, mapperdId, boundSql);
        } catch (Exception e) {
            log.error("查询解析失败", e);
        }
        return result;
    }
	... 
}

  发现没有,加密和解密插件的区别就是时机问题。

public interface StatementHandler {

  Statement prepare(Connection connection, Integer transactionTimeout)
      throws SQLException;

  void parameterize(Statement statement)
      throws SQLException;

  void batch(Statement statement)
      throws SQLException;

  int update(Statement statement)
      throws SQLException;

  <E> List<E> query(Statement statement, ResultHandler resultHandler)
      throws SQLException;

  <E> Cursor<E> queryCursor(Statement statement)
      throws SQLException;

  BoundSql getBoundSql();

  ParameterHandler getParameterHandler();
}

  我们在prepare,parameterize,方法调用时,对方法参数进行加密处理,在query方法时将加密参数解密回来,因为此时Statement对象已经准备好了, 将加密参数修改回来对查询结果无影响,因此我们选择在query拦截器中将之前加密的参数解密回来 ,看看解密方法如何实现。

public void decodeData(Configuration configuration, String mapperdId, BoundSql boundSql) {
    PTuple2<Boolean, Map<Integer, String>> data = null;
    String sql = boundSql.getSql();
    String key = mapperdId + PMD5Util.encode(sql);
    try {
        PPTuple pluginTuple = enterMap.get(key);
        if (pluginTuple == null) {
            data = getChangeColumn(configuration, mapperdId, sql, key).getData();
        } else {
            data = pluginTuple.getData();
        }
        if (!data.getFirst()) {
            return;
        }
        Map<Integer, String> paramMaps = data.getSecond();
        Object parameterObject = boundSql.getParameterObject();
        List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
        if (parameterMappings.size() > 0 && parameterObject != null) {
            MetaObject metaObject = configuration.newMetaObject(parameterObject);
            if (parameterObject instanceof DefaultSqlSession.StrictMap) {       //表示一个集合
                Set<String> keySet = ((DefaultSqlSession.StrictMap<?>) parameterObject).keySet();
                if(keySet.contains("list")){
                    List<Object> list = (List<Object>) metaObject.getValue("list");
                    if (list != null && list.size() > 0) {
                        Object object = list.get(0);
                        if (!(object instanceof String)) {
                            List<Object> collection = (List<Object>) metaObject.getValue("collection");
                            int i = 0 ;
                            for (Map.Entry<Integer, String> paramMap : paramMaps.entrySet()) {
                                MetaObject meta1 = configuration.newMetaObject(list.get(i));
                                MetaObject meta2 = configuration.newMetaObject(collection.get(i));
                                String propertyName = PSqlParseUtil.field2JavaCode(paramMap.getValue());
                                Object temp = meta1.getValue(propertyName);
                                Object value = decode(temp);
                                meta1.setValue(propertyName, value);
                                meta2.setValue(propertyName, value);
                                i ++;
                            }
                        }
                    }
                } else {

                    Object[] array = (Object[]) metaObject.getValue("array");
                    if (array != null && array.length > 0) {
                        Object object = array[0];
                        if (!(object instanceof String)) {
                            int i = 0 ;
                            for (Map.Entry<Integer, String> paramMap : paramMaps.entrySet()) {
                                MetaObject meta1 = configuration.newMetaObject(array[i]);
                                String propertyName = PSqlParseUtil.field2JavaCode(paramMap.getValue());
                                Object temp = meta1.getValue(propertyName);
                                Object value = decode(temp);
                                meta1.setValue(propertyName, value);
                                i ++;
                            }
                        }
                    }

                }
            } else {
                for (Map.Entry<Integer, String> paramMap : paramMaps.entrySet()) {
                    int index = paramMap.getKey();
                    ParameterMapping parameterMapping = parameterMappings.get(index);
                    String propertyName = parameterMapping.getProperty();
                    PTuple2<Boolean, Object> indetValue = getIndexObject(boundSql, metaObject, propertyName, index).getData();
                    Object decode = decode(indetValue.getSecond());
                    if (decode != null) {
                        metaObject.setValue(propertyName, decode);
                    }
                    if (indetValue.getFirst()) {
                        boundSql.setAdditionalParameter(propertyName, decode);
                    }
                }
            }
        }
    } catch (Exception e) {
        try {
            log.error("showSql exception " + JSON.toJSONString(data) + ",key = " + key + ",sql = " + sql + ",mapperId" + mapperdId, e);
        } catch (Exception ex) {
            log.error("decodeData exception", e);
        }
    }
}

  聪明的读者肯定会发现,解密方法和加密方法的实现原理一样,只是将原来的encode()方法改成decode()方法即可。
在这里插入图片描述

  接下来,我们继续来看更新方法的实现。先将实现好的功能恢复回去 。

在这里插入图片描述
在这里插入图片描述
  恢复成之前的版本,看只有DataQueryEncryptScopeInterceptor拦截器时会有什么问题。
在这里插入图片描述
  从执行结果上来看,显然更新成功了, 是不是DataQueryEncryptScopeInterceptor 拦截器就可以了呢?为什么还要写一个DataQueryEncryptScopeInterceptor 来拦截更新条件的参数呢?

  那批量更新有问题吗?我们继续看例子。

// http://localhost:8502/api/test10
@RequestMapping("/test10")
public String test10() throws Exception {
    UserPhone userPhone1 = userPhoneDao.selectUserPhoneById(1l);
    UserPhone userPhone2 = userPhoneDao.selectUserPhoneById(2l);
    userPhone1.setUserNameEn("111111111111");
    userPhone2.setUserNameEn("222222222222");

    List<UserPhone> userPhones = new ArrayList<>();
    userPhones.add(userPhone1);
    userPhones.add(userPhone2);

    userPhoneService.updateBatchById(userPhones);

    UserPhone userPhone11 = userPhoneDao.selectUserPhoneById(1l);
    UserPhone userPhone22 = userPhoneDao.selectUserPhoneById(2l);
    System.out.println(JSON.toJSONString(userPhone11));
    System.out.println(JSON.toJSONString(userPhone22));
    return "success";

}

  上面的例子也很简单,查询id = 1 和 2 的userPhone ,修改其user_name_en的值,进行批量更新
在这里插入图片描述
  从上图结果来看,第一条更新语句进行了加密解密,但第二条更新语句,要加密的字段并没有加密成功, 为什么呢?难道是两条数据导致的吗?如果批量更新3条数据,会怎样呢?
在这里插入图片描述
  现在发现规率了,如果批量更新,只有第一条数据会被加密,从第二条开始,后面的数据都没有被加密,那是什么原因导致这样的问题呢?
  又发挥我在代码中寻寻觅觅的长处了,找到了批量处理代码 。

public int doUpdate(MappedStatement ms, Object parameterObject) throws SQLException {
    final Configuration configuration = ms.getConfiguration();
    final StatementHandler handler = configuration.newStatementHandler(this, ms, parameterObject, RowBounds.DEFAULT, null, null);
    final BoundSql boundSql = handler.getBoundSql();
    final String sql = boundSql.getSql();
    final Statement stmt;
    // 如果SQL一样,mybatis为了优化性能,就会走下面代码 
    if (sql.equals(currentSql) && ms.equals(currentStatement)) {
        int last = statementList.size() - 1;
        stmt = statementList.get(last);
        applyTransactionTimeout(stmt);
        handler.parameterize(stmt);//fix Issues 322
        BatchResult batchResult = batchResultList.get(last);
        batchResult.addParameterObject(parameterObject);
    } else {
        Connection connection = getConnection(ms.getStatementLog());
        stmt = handler.prepare(connection, transaction.getTimeout());
        handler.parameterize(stmt);    //fix Issues 322
        currentSql = sql;
        currentStatement = ms;
        statementList.add(stmt);
        batchResultList.add(new BatchResult(ms, sql, parameterObject));
    }
    // handler.parameterize(stmt);
    handler.batch(stmt);
    return BATCH_UPDATE_RETURN_VALUE;
}

  从上述代码来看,mybatis为了优化性能,在更新处理时,如果两条SQL一样,则只会调用一次handler.prepare()方法,但无论什么情况parameterize()总是被调用,我们只要写一个对parameterize方法拦截的拦截器即可。

在这里插入图片描述

  批量更新进入doUpdate()方法
在这里插入图片描述
  批量更新数据都已经加密了
在这里插入图片描述

  那只需要DataUpdateEncryptScopeInterceptor即可,还需要DataQueryEncryptScopeInterceptor拦截器做什么呢?我们继续来看问题。
  我们写一个分页查询

// http://localhost:8502/api/test11
@RequestMapping("/test11")
public String test11() throws Exception {
    List<String> usernames = new ArrayList<>();
    usernames.add("111111111111");
    usernames.add("222222222222");
    usernames.add("333333333333");
    PageUtils pageUtils = PageUtils.startPage(1, 10).doSelect(new ISelect() {
        @Override
        public List doSelect(IPage page) {
            return userPhoneDao.selectUserPhoneByPageUserNameEns(usernames);
        }
    });
    System.out.println(JSON.toJSONString(pageUtils));
    return "success";
}

  PageUtils 为一个分页工具类,我不喜欢mybatis-plus用来做分页,太死板了,又不喜欢Github PageHelper那种,直接设置分页参数,再写一行查询语句 ,就帮你实现了分页,感觉两者之间没有太大关系,因此我自己写了一个分页工具类,做了一层包装,感觉这样更加贴切 。我们重点讲分页问题。
在这里插入图片描述
  很明显,查询条件是做了加密的,但是遗憾的是总页数和总条数却为0 。

在这里插入图片描述

  显然这样是不符合业务的,前端分页怎么能没有总页数呢?但出现这个问题的原因是什么呢?我们进入分页拦截器。

在这里插入图片描述

  发现问题没有,并不是分页拦截器没有用,而是查询时,查询条件并没有被加密 ,不知道细心的读者有没有发现导致问题产生的原因 。
在这里插入图片描述
  发现没有,分页插件是对prepare拦截,而我们的加密解密插件是对parameterize方法拦截,从执行流程来看,prepare()方法先于parameterize()方法执行,因此为了解决这个问题,需要将查询和更新插件分开,保证查询加密插件比分页插件先执行即可。因此DataQueryEncryptScopeInterceptor和DataUpdateEncryptScopeInterceptor一个都不能少, DataQueryEncryptScopeInterceptor主要对prepare方法拦截 ,只加密查询参数 ,而DataUpdateEncryptScopeInterceptor主要对parameterize方法拦截 , 只加密更新参数,修改后再来看分页结果 。

在这里插入图片描述
  分页查询总条数时,查询条件已经加密 。
在这里插入图片描述
  再来看查询结果

在这里插入图片描述
  总条数和总页数已经有数值了。

  对于查询和更新已经分析完了,那么查询结果怎样解密呢?

@Slf4j
@Intercepts({@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})})
public class QueryDecryptScopeInterceptor implements Interceptor {

    public static Map<String, PPTuple> outMap = new ConcurrentHashMap<>(256);

    /**
     * 代替拦截对象的方法内容
     * 责任链对象
     */
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        DefaultResultSetHandler defaultResultSetHandler = (DefaultResultSetHandler) PluginUtils.realTarget(invocation.getTarget());
        //MappedStatement维护了一条<select|update|delete|insert>节点的封装
        MetaObject metaObject = SystemMetaObject.forObject(defaultResultSetHandler);
        // 先判断是不是SELECT操作
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("mappedStatement");

        //获取当前resutType的类型
        Object object = realTarget(invocation.getArgs()[0]);
        com.mysql.jdbc.Field[] fields = null;
        //BoundSql对象是处理SQL语句用的
        MetaObject metaObjectResult = SystemMetaObject.forObject(object);
        if (object != null) {
            if (object instanceof JDBC42PreparedStatement) {
                fields = (com.mysql.jdbc.Field[]) metaObjectResult.getValue("results.fields");
            } else if ("DruidPooledPreparedStatement".equals(object.getClass().getSimpleName())) {
                Object obx = metaObjectResult.getValue("stmt");
                if (obx != null && "PreparedStatementProxyImpl".equals(obx.getClass().getSimpleName())) {
                    fields = (com.mysql.jdbc.Field[]) metaObjectResult.getValue("stmt.statement.results.fields");
                } else {
                    fields = (com.mysql.jdbc.Field[]) metaObjectResult.getValue("stmt.results.fields");
                }
            } else {
                log.info("查询没有捕捉到的类:" + object.getClass().getSimpleName());
            }
        }
        Object result = invocation.proceed();
        String mapperdId = PSqlParseUtil.getMapperId(mappedStatement);
        BoundSql boundSql = (BoundSql) metaObject.getValue("boundSql");
        try {
            if (result != null) {
                PTuple2<Boolean, List<String>> data = getChangeColumn(fields, mapperdId, boundSql.getSql()).getData();
                if (data.getFirst()) {
                    if (result instanceof Collection) {
                        forceChageList((Collection) result, data.getSecond());
                    } else if (result.getClass().isArray()) {     //如果是数组类型
                        forceChageList(Arrays.asList((Object[]) result), data.getSecond());
                    } else {
                        forceChange(result, data.getSecond());
                    }
                }
            }
        } catch (Exception e) {
            log.error("sql select 解密数据异常 exception ", e);
            PDingDingUtils.sendText("异常编号 =" + Logger.inheritableThreadLocalNo.get()
                    + "\n mapperId =" + mapperdId
                    + "\n sql = " + boundSql.getSql()
                    + "\n 异常堆栈 = " + PExceptionUtils.dealException(e));
        }
        return result;
    }

}

  上面代码看上去一大堆,其实只要看懂JDBC就明白了,其实解密很简单。

在这里插入图片描述
  无论是什么框架,总能拿到查询结果的表名和列名,这个和mybatis无关,jdbc的返回结果中也有这些数据 。
  看一个例子

@Test
public void jdbcQuery() {
    Connection conn = null;
    PreparedStatement pstemt = null;
    try {
        //注册加载jdbc驱动
        Class.forName("com.mysql.jdbc.Driver");
        //打开连接
        conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/lz_test?characterEncoding=utf-8", "ldd_biz", "123456");
        //创建执行对象
        String sql = "select *  from lz_test_user where id = ?";
        pstemt = conn.prepareStatement(sql);
        pstemt.setObject(1,"14");
        //执行sql语句
        ResultSet rs = pstemt.executeQuery();
        System.out.println("---------");
        //展开结果集
        while (rs.next()) {
            System.out.println(rs.getString("username"));
        }
        rs.close();
        conn.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}

在这里插入图片描述  因此我们都不需要sql-parser框架解析sql ,只需要将表名和列名与我们配置 需要加密的 字段相比对,如果是,则进行解密即可。原理很简单,就不深入分析。
  当然有人会想,你这是单表查询,如果级联查询,子查询,这些也能从jdbc中看到查询出结果对应的表和列字段吗?当然可以。

   修改sql select * from lt_stage_borrow sb left join lt_user_phone up on sb.unique_code = up.unique_code ,查看结果。

在这里插入图片描述

MapF2FInterceptor

  我相信细心的读者还发现一个问题,就是我还写了一个MapF2FInterceptor,这个插件有什么用呢?同样这个插件的源码很简单,不分析 ,只看应用场景,这个应用场景也是对之前的一篇博客的补充吧。 之前哪篇博客呢?2.mybatis实用小插件 ,主要对mybatis 的一些写法的优化,讲一种应用场景。

// http://localhost:8502/api/test12
@RequestMapping("/test12")
public String test12() throws Exception {
    List<StageBorrow> borrows = stageBorrowDao.selectStageBorrowByAll();
    // 此时此刻 需要拿到所有用户 的用户名
    List<String> uniqueCodes = borrows.stream().map(StageBorrow::getUniqueCode).collect(Collectors.toList());
    List<UserPhone> userPhones = userPhoneDao.selectUserPhoneByUniqueCodes(uniqueCodes);
    Map<String,UserPhone> mapUserPhone =  userPhones.stream().collect(Collectors.toMap(UserPhone::getUniqueCode , Function.identity()));
    for(StageBorrow stageBorrow :borrows){
        UserPhone userPhone = mapUserPhone.get(stageBorrow.getUniqueCode());
        System.out.println("=========" + userPhone.getRealNameEn());
    }

    System.out.println("==================还有一种写法=========================");
    for(StageBorrow stageBorrow :borrows){
        // 这种法写,性能太差了
        UserPhone userPhone = userPhoneDao.selectUserPhoneByUniqueCode(stageBorrow.getUniqueCode());
        System.out.println("=========" + userPhone.getRealNameEn());
    }
    return "success";
}

上面这个例子,想通过借款表查找所有借款用户的真实姓名,通过uniqueCode关联,上面提供了两种写法,
第一种

  1. 先通过lamb表达式获取uniqueCode列表
  2. 通过uniqueCode查询
  3. 将查询的结果转化为Map

  这种方式我不太喜欢,至少写了3行代码,同时我也不喜欢lamb表达式,因为没有python语法简洁,因此我基本不用。第二种写法,就不用说了,性能太差,java 就没有一种好的写法吗?很遗憾的告诉你,到少我没有发现,既然没有,那就自己想办法实现吧。

 // http://localhost:8502/api/test13
@RequestMapping("/test13")
public String test13() throws Exception {
    List<StageBorrow> borrows = stageBorrowDao.selectStageBorrowByAll();
    // 此时此刻 需要拿到所有用户 的用户名
    Map<String,UserPhone> mapUserPhone =  userPhoneDao.selectUserPhoneMapByUniqueCode(borrows);
    for(StageBorrow stageBorrow :borrows){
        UserPhone userPhone = mapUserPhone.get(stageBorrow.getUniqueCode());
        System.out.println("=========" + userPhone.getRealNameEn());
    }
    return "success";
}

   先来看看效果
在这里插入图片描述
  Mapper.xml的实现如下

@Bean2Map(key = "uniqueCode",value = "this")
Map<String, UserPhone> selectUserPhoneMapByUniqueCode(@IN  @Row("uniqueCode") List<StageBorrow> uniqueCode);

  Bean2Map注解有两个参数,key返回对象中的哪个字段封装为key , value 如果为"this" ,表示对象自身,如果为其他的字符串,则对对象中的某个属性, 方法参数中有两个注解,@IN注解可以传一个参数,参数对应的是下面加粗的条件 ,如果不传,则取方法名转化为下划线 unique_code ,
select * from lt_user_phone where unique_code in ( ? ,? ,?)
uniqueCode转化为下划线后为unique_code, @Row()注解也需要传一个参数,如这个参数表示是取StageBorrow对象的哪个属性作为in 的条件 ,本例中取StageBorrow的 uniqueCode属性作为in 的条件 。所以真正的sql为

SELECT * FROM lt_user_phone WHERE is_delete = 0 AND unique_code IN ( ‘1111’ , ‘2222’ , ‘3333’ , ‘1111’ )

  接下来,我们继续看,如果返回的Map 的value是一个具体的值,而不是一个对象,那怎么办呢?

在这里插入图片描述

  看Mapper中的写法

@Bean2Map(key = "uniqueCode",value = "realNameEn")
MapString, String> selectUserPhoneMapByUniqueCodeRealName(@IN("uniqueCode")  @Row("uniqueCode")ListStageBorrow> borrows);

  以UserPhone对象的uniqueCode为key , realNameEn属性为value的Map ,同时@IN()注解的参数为加粗代码
select * from lt_user_phone where unique_code in ( ? ,? ,?),而 @Row(“uniqueCode”) 注解里的参数为in (?,?,?)里的条件 。
  我们再回到test13方法。
在这里插入图片描述
  应该加上空判断,如果userPhone为空,是不是导致userPhone.getRealNameEn()报空指针异常,其实你也不用担心 ,底层框架已经实现。我们修改lt_stage_borrow的unique_code的值 。
在这里插入图片描述

在这里插入图片描述
  访问http://localhost:8502/api/test13 ,发现没有报空指针异常
在这里插入图片描述

@Bean2Map(key = "uniqueCode",value = "this",fillNull = false)
MapString, UserPhone> selectUserPhoneMapByUniqueCode(@IN  @Row("uniqueCode") ListStageBorrow> uniqueCode);

  在@Bean2Map中设置fillNull参数为false。
在这里插入图片描述

  当然你不用担心,fillNull的默认值为true,如果你不想让插件帮你填充空值时,可以设置fillNull = false

  hibernate虽然我不喜欢用,但是他有些功能我觉得还是很好的。比如级联拉取。

在这里插入图片描述

  这个是什么意思呢?先来看看Mapper

Mapper
ListStageBorrowDto> selectStageBorrowByAllDto();
StageBorrowDto
@Data
public class StageBorrowDto extends StageBorrow{
    @Pull(self = "uniqueCode", target = "uniqueCode", sort = "desc",limit =  1 )
    private List<UserPhone> userPhoneList;
}

  Pull注解的self表示StageBorrow的字段, target 表示UserPhone对应的字段,相当于写一条SQL
select * from lt_user_phone where unique_code = #{uniqueCode} order by id desc limit 1

  其实selectStageBorrowByAllDto()的意图就是,在查询StageBorrow 时将与之关联的UserPhone对象拉取出来 。当然,拉取的UserPhone也可以是一个对象,并不一定是一个list。
在这里插入图片描述

Controller
// http://localhost:8502/api/test16
@RequestMapping("/test16")
public String test16() throws Exception {
    ListStageBorrowDto2> borrows = stageBorrowDao.selectStageBorrowByAllDto2();
    for(StageBorrowDto2 stageBorrowDto : borrows){
        UserPhone userPhone = stageBorrowDto.getUserPhone();
        System.out.println(JSON.toJSONString(userPhone));
    }
    return "success";
}
StageBorrowDto2
@Data
public class StageBorrowDto2 extends StageBorrow{

    @Pull(self = "uniqueCode", target = "uniqueCode", sort = "desc",limit =  1 )
    private UserPhone userPhone;


}

Mapper

List<StageBorrowDto2> selectStageBorrowByAllDto2();

  当然此时此刻还有人会想,如果我想在lt_user_phone表拉取时,加一个条件怎么办呢?那我们也可以来终级大招。 修改StageBorrowDto3实体

StageBorrowDto3
@Data

public class StageBorrowDto3 extends StageBorrow{

    @Pull(self = "uniqueCode", target = "uniqueCode", where = " type = 1 and cnl_code = 'chenn' ", sort = "desc",limit =  1 )
    private UserPhone userPhone;

}

在这里插入图片描述

  当然,有人想,我也分不清self ,target 也容易弄混,能不能都写到where条件里,那当然可以。

@Data
public class StageBorrowDto4 extends StageBorrow{
    @Pull(where = " unique_code = #{uniqueCode} and  type = 1 and cnl_code = 'chenn' ", sort = "desc",limit =  1 )
    private UserPhone userPhone;
}

在这里插入图片描述
  当然,有人还 会想,如果UserPhone对象内部还想拉取其他对象,能不能实现呢?当然可以啦。
  依然还是先创建两个实体,StageBorrowDto5包含了UserPhoneDto 。 主要意思是什么呢?我想通过借款表查找到用户表,再将用户所有的登陆日志拉取出来 。

@Data
public class StageBorrowDto5 extends StageBorrow{

    @Pull(where = " unique_code = #{uniqueCode} and  type = 1 and cnl_code = 'chenn' ", sort = "desc",limit =  1 )
    private UserPhoneDto userPhone;
}

@Data
public class UserPhoneDto extends UserPhone {

    @Pull(where = " user_id = #{id}  ", sort = "desc")
    private List<UserLogin> userLoginList;
}

在这里插入图片描述  上面的pull功能依然有点小问题,如果是需要被加密解密字段就不能用pull进行拉取了,需要自己重新写Mapper,这一点需要注意 。

  文章到这里也告一段落了,整个项目的开发过程中,非常感谢涛哥,华坤,小谷,中正,铮俊,志轩 提供了问题及保贵的建议,才得以框架在生产上正常运行。

  同时感谢技术大佬爱姐毫无条件的支持与信任, 感谢测试团队黎甜,进惠,海燕,潘含,闪哥辛勤不辍的不断发现问题,如数据被加密两遍,分页问题,批量更新问题,通过条件查询过后,查询条件被加密问题等 ,最终这些问题都得以解决,这个框架是一个技术团队花了几个月的时间共同努力的结果。希望其中的解决方案对同行有所帮助 。

  我觉得,如果是一个构架师,应该根据开发的需要开发更加易用,出bug率少的工具给开发人员使用,让开发人员投身到具体的业务中去,而少写无用的东西,我也不是什么架构师,只是一个技术爱好者而已 。

  如果网友有什么好的建议及更好的方案提供,请给我留言吧,下一篇博客见。

本文相关项目的github地址为

https://github.com/quyixiao/lz_mybatis_plugin.git
https://github.com/quyixiao/github-lz-mybatis-en-plugins.git
https://github.com/quyixiao/spring-data-en-decoder
https://github.com/quyixiao/sql-parser.git

  • 6
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值