MyBatis源码分析之防SQL注入

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012734441/article/details/86533694

MyBatis源码分析之防SQL注入

这一节来讲下MyBatis的防SQL注入,SQL注入大多数也会比较清楚,就是SQL参数对应的字段值时插入混合SQL,如 ** username = or 1= 1** 这种,如果有更恶劣的,带上drop database 这种都是有可能的,所以一般SQL都会进行一定防注入处理,MyBatis其实用法大都清楚,就是**#{paras}${paras}**两种用法,以前我就是会用,但是具体原理咱也没看过,在一次面试中,被别人问到过具体用法,注入原理,处理原理等等,当时就是根据自己的印象去回答了,但是对于MyBatis与数据库具体处理SQL注入却不是很熟悉的,这一节就来详细讲解下,记录一下。

#1. #{paras}与${paras}用法


对于**#{paras}KaTeX parse error: Expected 'EOF', got '#' at position 23: …**写两个用法吧,一个根据**#̲{paras}**来查询,一种…{paras}**来查询,如下:

@ResultMap("BaseResultMap")
@Select("select * from user where username = #{username}")
User getUserByParas1(@Param("username") String username);

@ResultMap("BaseResultMap")
@Select("select * from user where username = ${username}")
List<User> getUserByParas2(@Param("username") String username);

调用这两个方法的程序为:

User user = userMapper.getUserByParas1("xiaxuan");
List<User> users = userMapper.getUserByParas2(" 1 or 1 = 1");
System.out.println(user);
System.out.println(users);

运行结果为:
在这里插入图片描述

运行结果和想象中差不多,在conf.xml中添加打印sql的配置.

<settings>
  <!-- 打印查询语句 -->
  <setting name="logImpl" value="STDOUT_LOGGING" />
</settings>

再运行一次看看,如下图:

在这里插入图片描述

第一个sql是进行了占位符处理,第二个是直接拼出了sql语句。

上述就是演示#{paras}调用和和sql注入的过程,下面开始讲两者在实现过程中的原理。

2. 初始化源码分析


首先看看这两个方法是怎么加载的,这还是要回到注解方法的解析中,在前文中我们知在解析注解时,会将当前sql包装成SqlSource对象,然后在查询时使用,代码如下:

void parseStatement(Method method) {
    Class<?> parameterTypeClass = getParameterType(method);
    LanguageDriver languageDriver = getLanguageDriver(method);
    SqlSource sqlSource = getSqlSourceFromAnnotations(method, parameterTypeClass, languageDriver);
    .....
}

我们进getSqlSourceFromAnnotations方法中看看。

扫描二维码关注公众号,回复: 4963097 查看本文章
private SqlSource getSqlSourceFromAnnotations(Method method, Class<?> parameterType, LanguageDriver languageDriver) {
    try {
      Class<? extends Annotation> sqlAnnotationType = getSqlAnnotationType(method);
      Class<? extends Annotation> sqlProviderAnnotationType = getSqlProviderAnnotationType(method);
      if (sqlAnnotationType != null) {
        if (sqlProviderAnnotationType != null) {
          throw new BindingException("You cannot supply both a static SQL and SqlProvider to method named " + method.getName());
        }
        Annotation sqlAnnotation = method.getAnnotation(sqlAnnotationType);
        final String[] strings = (String[]) sqlAnnotation.getClass().getMethod("value").invoke(sqlAnnotation);
        return buildSqlSourceFromStrings(strings, parameterType, languageDriver);
      } else if (sqlProviderAnnotationType != null) {
        Annotation sqlProviderAnnotation = method.getAnnotation(sqlProviderAnnotationType);
        return new ProviderSqlSource(assistant.getConfiguration(), sqlProviderAnnotation, type, method);
      }
      return null;
    } catch (Exception e) {
      throw new BuilderException("Could not find value method on SQL annotation.  Cause: " + e, e);
    }
  }

此处我们只分析带有@Select注解的方法,所有就是进入if条件中进行处理,这里再进入buildSqlSourceFromStrings方法。

private SqlSource buildSqlSourceFromStrings(String[] strings, Class<?> parameterTypeClass, LanguageDriver languageDriver) {
    final StringBuilder sql = new StringBuilder();
    for (String fragment : strings) {
      sql.append(fragment);
      sql.append(" ");
    }
    return languageDriver.createSqlSource(configuration, sql.toString().trim(), parameterTypeClass);
  }

最后通过languageDriver返回SqlSource,进入createSqlSource方法中。

@Override
  public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    SqlSource source = super.createSqlSource(configuration, script, parameterType);
    checkIsNotDynamic(source);
    return source;
  }

@Override
  public SqlSource createSqlSource(Configuration configuration, String script, Class<?> parameterType) {
    // issue #3
    if (script.startsWith("<script>")) {
      XPathParser parser = new XPathParser(script, false, configuration.getVariables(), new XMLMapperEntityResolver());
      return createSqlSource(configuration, parser.evalNode("/script"), parameterType);
    } else {
      // issue #127
      script = PropertyParser.parse(script, configuration.getVariables());
      TextSqlNode textSqlNode = new TextSqlNode(script);
      if (textSqlNode.isDynamic()) {
        return new DynamicSqlSource(configuration, textSqlNode);
      } else {
        return new RawSqlSource(configuration, script, parameterType);
      }
    }
  }

在两个方法中做了最后的sql包装,在createSqlSource方法中,第一种是处理带有**

public boolean isDynamic() {
    DynamicCheckerTokenParser checker = new DynamicCheckerTokenParser();
    GenericTokenParser parser = createParser(checker);
    parser.parse(text);
    return checker.isDynamic();
  }

这里实例化了一个DynamicCheckerTokenParser对象,但是进这个实例化方法中可以看到并没有做什么事情,主要是createParser中,转到createParser方法。

private GenericTokenParser createParser(TokenHandler handler) {
    return new GenericTokenParser("${", "}", handler);
  }

然后下一步对是否有"${", "}"进行判断,parser.parse(text);,最后返回是否返回是否是动态sql,在这毫无疑问的是我们前面写的方法中,第一个是返回的RawSqlSource对象,第二个返回的是DynamicSqlSource对象,这两个对象这里不做分析,等会再执行时再来看是如何对sql进行处理的。

3. mapper执行源码分析


上面分析完了在配置时的sql过程,现在分析在select语句执行时对SqlSource的解析,在前面文章分析中,我们知所有的查询基本是调用的SelectList方法,然后在最后调用doQuery方法之前会获得BoundSql对象,我们的源码分析就是从这开始。

 @Override
  public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
    BoundSql boundSql = ms.getBoundSql(parameter);
    CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
    return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 }

这里可以对上面两个代码调试一下代码,BoundSql对象如下图:

方法1:

在这里插入图片描述

这里#{paras}已经替换成了占位符,然后对应参数映射在parameterObject对象中,这里就先不展示第二个方法执行到这的BoundSql对象了,先把这个分析清楚吧,从上面图与源码中我们知最终的处理方法在,ms.getBoundSql中,进入到getBoundSql方法。

public BoundSql getBoundSql(Object parameterObject) {
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings == null || parameterMappings.isEmpty()) {
      boundSql = new BoundSql(configuration, boundSql.getSql(), parameterMap.getParameterMappings(), parameterObject);
    }

    // check for nested result maps in parameter mappings (issue #30)
    for (ParameterMapping pm : boundSql.getParameterMappings()) {
      String rmId = pm.getResultMapId();
      if (rmId != null) {
        ResultMap rm = configuration.getResultMap(rmId);
        if (rm != null) {
          hasNestedResultMaps |= rm.hasNestedResultMaps();
        }
      }
    }

    return boundSql;
  }

进入到sqlSource.getBoundSql(parameterObject);方法中,在前文中我们只分析了DynamicSqlSource,但是并没有对RawSqlSource进行分析,在这顺便说下RawSqlSource,直接看RawSqlSource的构造方法。

public RawSqlSource(Configuration configuration, String sql, Class<?> parameterType) {
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> clazz = parameterType == null ? Object.class : parameterType;
    sqlSource = sqlSourceParser.parse(sql, clazz, new HashMap<String, Object>());
  }

对sql进行了进一步的解析,进入到parse方法中。

public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {
    ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler(configuration, parameterType, additionalParameters);
    GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);
    String sql = parser.parse(originalSql);
    return new StaticSqlSource(configuration, sql, handler.getParameterMappings());
  }

此处判断sql中是否含有"#{""}",parser.parse(originalSql)中解析源码的过程比较复杂,没有太多可以说的,但是得说下占位符的替换,在替换参数是最终会调用到SqlSource中的handlerToken方法。

@Override
    public String handleToken(String content) {
      parameterMappings.add(buildParameterMapping(content));
      return "?";
    }

此处进行了参数的替换,以及对字段的映射。

然后最终返回StaticSqlSource对象,所以此处RawSqlSource最后还是返回StaticSqlSource对象,然后在StaticSqlSource对象中执行getBoundSql方法,可以进StaticSqlSource的getBoundSql中看看。

@Override
  public BoundSql getBoundSql(Object parameterObject) {
    return new BoundSql(configuration, sql, parameterMappings, parameterObject);
  }

最终对替换参数后的sql、参数、参数映射以及对应值,构造BoundSql对象返回。

此处几乎分析的还是加载配置时的代码,还是回到query执行处,在获取到boundSql对象后,继续执行query方法,最终调用PreparedStatement对象执行execute方法,这里传入的sql是需要进行预编译的sql,同时传入参数,因此此处参数不会和sql一起执行,纯粹是sql对应查询字段值。

再来分析动态sql,再次回到query方法,如下图:

在这里插入图片描述

此处已经是拼好的sql,但是我们看ms中的SqlSource对象。

在这里插入图片描述

此时还没有对sql进行拼接,所以在获取BoundSql时,对参数就进行了替换,在上文中我们知此时的SqlSource对象为DynamicSqlSource对象,直接进入getBoundSql方法中。

public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    rootSqlNode.apply(context);
    SqlSourceBuilder sqlSourceParser = new SqlSourceBuilder(configuration);
    Class<?> parameterType = parameterObject == null ? Object.class : parameterObject.getClass();
    SqlSource sqlSource = sqlSourceParser.parse(context.getSql(), parameterType, context.getBindings());
    BoundSql boundSql = sqlSource.getBoundSql(parameterObject);
    for (Map.Entry<String, Object> entry : context.getBindings().entrySet()) {
      boundSql.setAdditionalParameter(entry.getKey(), entry.getValue());
    }
    return boundSql;
  }

此处构建DynamicContext上下文,最终处理还是在apply方法中。

  @Override
  public boolean apply(DynamicContext context) {
    GenericTokenParser parser = createParser(new BindingTokenParser(context, injectionFilter));
    context.appendSql(parser.parse(text));
    return true;
  }

在apply方法中,对text进行处理,核心代码在parser.parse(text)中,这里处理的逻辑就不讲了,进去后可以看到对参数的替换最终组装成当前的sql,从而实现了sql 的注入。

在后面的query方法就不再进行分析了,基本在组装出sql后一切都明了了。

猜你喜欢

转载自blog.csdn.net/u012734441/article/details/86533694