Mybatis Generator(MBG)安全加固插件介绍与使用

为什么要使用Mybatis Generator安全加固插件

首先,Mybatis generator(MBG)是一个可以自动生成实体类,mapper文件,dao接口文件的插件,可以大幅提高我们工作效率,并且他提供的很多动态sql语句编写,可以通过example类,criteria来实现。不过MBG存在一个sql注入的问题,在使用order by语句的时候,他利用的是$来进行sql语句的注入,也就是说在插件识别到oderbyClauses不为空的时候,他允许用户在后面输入任何符合sql语法规范的sql语句。

    <if test="orderByClause != null">
          order by ${orderByClause}
    </if>
复制代码

这是MBG自动生成的一个mapper文件,可以看到这里存在因$导致的sql注入问题,有人会问这里为什么不能用#{},用#会自动在参数两端加引号,这又不符合sql的语法规范,具体的sql注入问题可以参考这篇博客:MyBatis框架中常见的SQL注入。 因此,当我们的云服务提供这样一个插件给用户的时候,是不安全的。就需要有一个新的插件来加固MBG的安全性。

配置Mybatis Generator安全加固插件

默认已经配置好了Mybatis和Mybatis Generator插件;
可以看这个博客关于Mybatis Generator插件配置方式

MBG安全加固插件的github链接: Mybatis-generator-nodollar-master

1. 下载源码,打成jar包
2. 用maven install把jar包添加到本地的maven仓库里或者安全部门编译插件加入公司私服

我说一下本地方式,因为网上很多方式说的不对,maven install应该先找到项目所在地址,而不是jar包所在目录,命令行cd到达和pom.xml的平级目录后,输入:mvn install :install-file -DgroupId=com.venscor.codesec -DartifactId=mybatis-generator-nodollar-plugin -Dversion=1.0-SNAPSHOT -Dpackaging=jar -Dfile=${jar包所在目录}\mybatis-generator-nodollar-master.jar
会如下图显示:

Image 1.jpg

3. 引入依赖
<dependency>
    <groupId>com.venscor.codesec</groupId>
    <artifactId>mybatis-generator-nodollar-plugin</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>
复制代码
4. 修改MybatisGeneratConfig.xml

只需要改一行代码:
<context id="MysqlTables" targetRuntime="org.mybatis.generator.codegen.mybatis3.SafeIntrospectedTableMyBatis3Impl">

接着我们就可以运行Mybatis Generator,来自动生成代码了!有人遇到dao层里的Mapper接口方法很少,那是因为在MybatisGeneratorConfig.xml里没有设置,很多方法都默认为false,要在自己手动设置为true才行。

【注意】 版本号要一致,Mybatis generator安全加固插件的MybatisGenerator插件版本是1.4.0,比如你项目pom引入的MBG版本是1.3.5,那就会出现TargetRuntime Invalid的报错。

Mybatis Generator安全加固插件源码分析

因为MBG自动生成orderby主要集中在ProviderSelectByExampleWithoutBLOBsMethodGenerator.java这个类里,所以我们来看一下这个类里对orderby进行了怎样的修改。

List<IntrospectedColumn> columns = introspectedTable.getAllColumns();

        method.addBodyLine(""); //$NON-NLS-1$
        if (columns != null && columns.size() > 0) {
            method.addBodyLine("List<String> columnNameList = new ArrayList<>();");
            for (IntrospectedColumn column : columns) {
                if (column.getActualColumnName() != null) {
                    method.addBodyLine("columnNameList.add(\"" + column.getActualColumnName() + "\");");
                }
            }
        }

        method.addBodyLine(""); //$NON-NLS-1$
        method.addBodyLine("if (example.getOrderByClauses() != null && example.getOrderByClauses().size() > 0) {");
        method.addBodyLine("for (Map.Entry<String, String> entry : example.getOrderByClauses().entrySet()) {");
        method.addBodyLine("String columnName = entry.getKey();");
        method.addBodyLine("String descOrAsc = entry.getValue();");
        method.addBodyLine("if (columnNameList.contains(columnName)) {");
        method.addBodyLine("if (descOrAsc.equals(\"desc\")) {");
        method.addBodyLine("sql.ORDER_BY(columnName + \" desc\");");
        method.addBodyLine("} else {");
        method.addBodyLine("sql.ORDER_BY(columnName);");
        method.addBodyLine("}");
        method.addBodyLine("}");
        method.addBodyLine("}");
        method.addBodyLine("}");


        method.addBodyLine(""); //$NON-NLS-1$


//        method.addBodyLine("if (example != null && example.getOrderByClause() != null) {"); //$NON-NLS-1$
//        method.addBodyLine(String.format("%sORDER_BY(example.getOrderByClause());", builderPrefix)); //$NON-NLS-1$
//        method.addBodyLine("}"); //$NON-NLS-1$

        method.addBodyLine(""); //$NON-NLS-1$
        if (useLegacyBuilder) {
            method.addBodyLine("return SQL();"); //$NON-NLS-1$
        } else {
            method.addBodyLine("return sql.toString();"); //$NON-NLS-1$
        }

复制代码

被注释掉的三行是原本MBG对orderby的处理,也就是最原始的String字符串拼接方式。而MBG安全加固插件是先用一个columnNameList(ArrayList)来存储通过introspectedTable获取的所有的字段名,然后判断这些字段是否被用户输入。而用户需要怎么输入呢?这个时候不是单纯的一个输入一个字符串了,而是需要输入一个Map,Map里面存储一个或多个Entry<String,String>,key值应当是表的字段名,而所对应的value可以用来判断升序或者降序,根据源码,降序必须要在value写明是“desc”,而升序是默认的,可以为null或者其他任意字符串。

而对于key值也会有一个判断,判断是否属于columnNameList,若属于就才会进行接下来的升序降序判断,但是我们知道sql语句会根据我们写的orderbyClause的顺序来依次排序。那对于map我们怎么操作呢?

因为源码里是对map里的元素进行for循环遍历,所以HashMap这样的无序集合一定无法满足我们的要求。有序的Map结构有LinkedHashMap和TreeMap。LinkedHashMap是比较好理解的,根据我们put操作的先后产生一个顺序,这也符合我们平时写sql的传统思路。至于TreeMap我觉得也会有一些使用场景,关于这个我没有测试,欢迎大家和我留言讨论。

对比

我建了一个user表,里面有id,name,pwd三个字段。用来测试两种orderby查询的方式。

MBG的orderby查询

    //orderby查询
    @Test
    public void test7(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        UserExample userExample = new UserExample();
        UserExample.Criteria criteria = userExample.createCriteria();
        userExample.setOrderByClause("`name` desc,`id`,pwd desc");
        List<User> userList = mapper.selectByExample(userExample);
        for (User user : userList) {
            System.out.println(user);
        }
    }
复制代码

MBG安全加固插件的orderby查询

    @Test
    public void test(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        UserExample example = new UserExample();
        UserExample.Criteria criteria = example.createCriteria();
        Map<String,String> map = new LinkedHashMap<>();
        map.put("name","desc");
        map.put("id","addd");
        map.put("pwd","desc");
        example.setOrderByClauses(map);
        List<User> userList = mapper.selectByExample(example);
        for (User user : userList) {
            System.out.println(user);
        }
    }
复制代码

一致的查询结果

User{id=6, name='zhang', pwd='12222'}
User{id=2, name='tom', pwd='123232'}
User{id=4, name='san', pwd='232'}
User{id=1, name='ryan', pwd='1'}
User{id=7, name='ryan', pwd='122'}
User{id=5, name='meng', pwd='123444'}
User{id=3, name='dick', pwd='21212'}
User{id=11, name='', pwd=''}
User{id=8, name='null', pwd='null'}
复制代码

当我们开始测试的时候,就会发现MBG安全加固插件解决$的sql注入问题其实核心原理就是做了一个白名单,在java层面做映射,设置了一个存储所有的字段名的List,仅允许用户传入字段名,保证传入的字段的合法性。 这种方式带来的坏处就是orderbyClause的写法被限制的很厉害。我们原本orderby后面可以写的if,case when语句都不能实现了。比如,原本的MBG插件可以写如下代码:

    @Test
    public void test(){
        SqlSession sqlSession = MybatisUtils.getSqlSession();
        UserMapper mapper = sqlSession.getMapper(UserMapper.class);
        UserExample userExample = new UserExample();
        userExample.setOrderByClause("case when id = 3 then 0 when id =2 then 1 end");
        List<User> userList = mapper.selectByExample(userExample);
        for (User user : userList) {
            System.out.println(user);
        }

    }
复制代码

而MBG安全加固插件自动生成的Mapper.xml文件却把很多东西写死了:

<select id="selectByExample" parameterType="com.rocket.model.UserExample" resultMap="BaseResultMap">
    <!--
      WARNING - @mbg.generated
      This element is automatically generated by MyBatis Generator, do not modify.
    -->
    select
    <if test="distinct">
      distinct
    </if>
    <!--'true' as QUERYID,-->
    <include refid="Base_Column_List" />
    from user
    <if test="_parameter != null">
      <include refid="Example_Where_Clause" />
    </if>
    <if test="orderByClauses != null and orderByClauses.size > 0">
      order by
      <foreach collection="orderByClauses" index="orderByName" item="ascOrDesc" separator=",">
        <if test="orderByName != null and orderByName == 'id'.toString()">
          id
          <if test="ascOrDesc !=null and ascOrDesc == 'desc'.toString()">
            desc
          </if>
        </if>
        <if test="orderByName != null and orderByName == 'name'.toString()">
          name
          <if test="ascOrDesc !=null and ascOrDesc == 'desc'.toString()">
            desc
          </if>
        </if>
        <if test="orderByName != null and orderByName == 'pwd'.toString()">
          pwd
          <if test="ascOrDesc !=null and ascOrDesc == 'desc'.toString()">
            desc
          </if>
        </if>
      </foreach>
    </if>
  </select>
  <select id="selectByPrimaryKey" parameterType="java.lang.Integer" resultMap="BaseResultMap">
复制代码

因此,MBG安全加固插件是用牺牲功能的方式换取了安全。不过由于我们云服务是给客户使用的,安全非常重要,在使用说明文档里我们可以注明这种方式的缺陷。并且用户如果有更多的需求,也可以修改对应的Mapper.xml来实现自己的需求。我觉得这个问题是可以解决的。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享