Mybatis中#{}和${}源码解析

目录

前言

一、初步解析

1.getBoundSql

2.parse

3.handleToken

二、parameterMappings解析

1.setParameters

2.防止sql注入

总结


前言

       使用#能防止sql注入,这是一个共识,但是为什么#能够做到,却很难说的清。本文主要分析#{}和${}的解析过程,从源码层次上解释#防止sql注入的原因。


一、初步解析

1.getBoundSql

        在CachingExecutor的query方法中,mybatis在从缓存或者数据库中获取数据之前,需要调用getBoundSql方法,将解析后的sql作为缓存中key的一部分,之所以在查找之前解析,也是为了缓存key的生成。在这个方法中,实现了将${}中的值替换为真实参数,将#{}中的值替换为?占位符,并将参数写入参数映射中。

  public BoundSql getBoundSql(Object parameterObject) {
    DynamicContext context = new DynamicContext(configuration, parameterObject);
    //TextSqlNode,只是对$符完成了解析
    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);
    //设置additionalParameter
    context.getBindings().forEach(boundSql::setAdditionalParameter);
    return boundSql;
  }

2.parse

        实际对sql进行处理的是GenericTokenParser的parse方法,在该方法中,保留{}外面的字符不变,对于${},直接查找对应的参数值,返回string进行 拼接;对于#{},返回?占位符。

  public String parse(String text) {
    if (text == null || text.isEmpty()) {
      return "";
    }
    // search open token
    int start = text.indexOf(openToken);
    if (start == -1) {
      return text;
    }
    char[] src = text.toCharArray();
    int offset = 0;
    final StringBuilder builder = new StringBuilder();
    StringBuilder expression = null;
    while (start > -1) {
      if (start > 0 && src[start - 1] == '\\') {
        // this open token is escaped. remove the backslash and continue.
        builder.append(src, offset, start - offset - 1).append(openToken);
        offset = start + openToken.length();
      } else {
        // found open token. let's search close token.
        if (expression == null) {
          expression = new StringBuilder();
        } else {
          expression.setLength(0);
        }
        //builder中添加#或$之前的字段
        builder.append(src, offset, start - offset);
        offset = start + openToken.length();
        int end = text.indexOf(closeToken, offset);
        while (end > -1) {
          if (end > offset && src[end - 1] == '\\') {
            // this close token is escaped. remove the backslash and continue. expression中添加{}中的字段
            expression.append(src, offset, end - offset - 1).append(closeToken);
            offset = end + closeToken.length();
            end = text.indexOf(closeToken, offset);
          } else {
            expression.append(src, offset, end - offset);
            break;
          }
        }
        if (end == -1) {
          // close token was not found.
          builder.append(src, start, src.length - start);
          offset = src.length;
        } else {
          //sqlSourceBuild在#{}情况下生成?,将参数加入到parameterMappings。
          //BindingTokenParser 在${}情况下直接返回对应值,字符串相加,varchar字段需要手动添加单引号
          //builder添加转化后的值,#{}对应?占位符,S{}对应真正的值
          builder.append(handler.handleToken(expression.toString()));
          offset = end + closeToken.length();
        }
      }
      start = text.indexOf(openToken, offset);
    }
    if (offset < src.length) {
      //添加{}后面的内容
      builder.append(src, offset, src.length - offset);
    }
    return builder.toString();
  }

3.handleToken

           parse方法中通过handleToken方法返回不同的值实现对$和#的不同解析。对于$,返回真实参数;对于#,生成参数映射器,返回?。

    public String handleToken(String content) {
      Object parameter = context.getBindings().get("_parameter");
      if (parameter == null) {
        context.getBindings().put("value", null);
      } else if (SimpleTypeRegistry.isSimpleType(parameter.getClass())) {
        context.getBindings().put("value", parameter);
      }
      //查找{}中的内容对应的真实参数,并变为string返回
      Object value = OgnlCache.getValue(content, context.getBindings());
      String srtValue = value == null ? "" : String.valueOf(value); // issue #274 return "" instead of "null"
      checkInjection(srtValue);
      return srtValue;
    }
    public String handleToken(String content) {
      //将参数映射添加到parameterMappings中,返回?占位符
      parameterMappings.add(buildParameterMapping(content));
      return "?";
    }

二、parameterMappings解析

1.setParameters

          前面在对#{}进行解析的时候,会返回占位符?,并将参数添加到parameterMappings中,在SimpleExecutor的doQuery方法中,回对parameterMappings进行解析。

  public <E> List<E> doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) throws SQLException {
    Statement stmt = null;
    try {
      Configuration configuration = ms.getConfiguration();
      // 创建语句处理器
      StatementHandler handler = configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql);
      //语句预处理,
      stmt = prepareStatement(handler, ms.getStatementLog());
      return handler.query(stmt, resultHandler);
    } finally {
      closeStatement(stmt);
    }
  }

        在DefaultParameterHandler的setParameters方法中,会遍历parameterMappings,找到参数对应值,对预编译语句进行处理。

 public void setParameters(PreparedStatement ps) {
    ErrorContext.instance().activity("setting parameters").object(mappedStatement.getParameterMap().getId());
    List<ParameterMapping> parameterMappings = boundSql.getParameterMappings();
    if (parameterMappings != null) {
      //通过@param和实体object传递参数都是在此处解析的,parameterMappings对应xml中的参数
      for (int i = 0; i < parameterMappings.size(); i++) {
        ParameterMapping parameterMapping = parameterMappings.get(i);
        if (parameterMapping.getMode() != ParameterMode.OUT) {
          Object value;
          String propertyName = parameterMapping.getProperty();
          //当boundSql含有额外参数时,mapper参数为list时
          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 {
            //传递参数使用@param时,parameterObject对应ParamMap(基于@param参数和默认key生成),使用map.get(prop.getName())取值,保证xml中变量名与@param中的名一致,或者使用arg0,arg1,param0
            //传递参数使用实体object时,parameterObject对应的时实体object,使用BeanWrapper基于属性反射获取,保证xml变量名与属性名一致
            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 {
            //对预编译sql中的binding赋值,对参数值进行处理
            typeHandler.setParameter(ps, i + 1, value, jdbcType);
          } catch (TypeException | SQLException e) {
            throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);
          }
        }
      }
    }
  }

2.防止sql注入

       setParameter方法最终会调用mysql-connection中的ClientPreparedStatement类的setString方法,如果参数值中有敏感字符,如单引号,双引号,回车等,需要进行相应处理,避免sql注入。

  public void setString(int parameterIndex, String x) {
        if (x == null) {
            setNull(parameterIndex);
        } else {
            int stringLength = x.length();
            String parameterAsString = x;
            boolean needsQuoted = true;
            //当参数字符串中敏感字符时进行处理
            if (this.isLoadDataQuery || isEscapeNeededForString(x, stringLength)) {
                needsQuoted = false; // saves an allocation later

                StringBuilder buf = new StringBuilder((int) (x.length() * 1.1));

                buf.append('\'');

                //
                // Note: buf.append(char) is _faster_ than appending in blocks, because the block append requires a System.arraycopy().... go figure...
                //

                for (int i = 0; i < stringLength; ++i) {
                    char c = x.charAt(i);

                    switch (c) {
                        case 0: /* Must be escaped for 'mysql' */
                            buf.append('\\');
                            buf.append('0');
                            break;
                        case '\n': /* Must be escaped for logs */
                            buf.append('\\');
                            buf.append('n');
                            break;
                        case '\r':
                            buf.append('\\');
                            buf.append('r');
                            break;
                        case '\\':
                            buf.append('\\');
                            buf.append('\\');
                            break;
                        case '\'':
                            buf.append('\'');
                            buf.append('\'');
                            break;
                        case '"': /* Better safe than sorry */
                            if (this.session.getServerSession().useAnsiQuotedIdentifiers()) {
                                buf.append('\\');
                            }
                            buf.append('"');
                            break;
                        case '\032': /* This gives problems on Win32 */
                            buf.append('\\');
                            buf.append('Z');
                            break;
                        case '\u00a5':
                        case '\u20a9':
                            // escape characters interpreted as backslash by mysql
                            if (this.charsetEncoder != null) {
                                CharBuffer cbuf = CharBuffer.allocate(1);
                                ByteBuffer bbuf = ByteBuffer.allocate(1);
                                cbuf.put(c);
                                cbuf.position(0);
                                this.charsetEncoder.encode(cbuf, bbuf, true);
                                if (bbuf.get(0) == '\\') {
                                    buf.append('\\');
                                }
                            }
                            buf.append(c);
                            break;

                        default:
                            buf.append(c);
                    }
                }

                buf.append('\'');

                parameterAsString = buf.toString();
            }

            byte[] parameterAsBytes = this.isLoadDataQuery ? StringUtils.getBytes(parameterAsString)
                    : (needsQuoted ? StringUtils.getBytesWrapped(parameterAsString, '\'', '\'', this.charEncoding)
                            : StringUtils.getBytes(parameterAsString, this.charEncoding));

            setValue(parameterIndex, parameterAsBytes, MysqlType.VARCHAR);
        }
    }

       对于select  * from user where user_name=#{name}和’${name}‘,当输入参数为123456' or '1=1时,会有不同的结果。

       对于${}情况,mybatis直接将参数值与sql语句拼接,形成第一种情况,

       对于#{},会使用预编译,经setString转化后变为'123456'' or ''1=1',能有效避免sql注入。

select  * from user where user_name= '123456' or '1=1'

select  * from user where user_name= '123456'' or ''1=1'

总结

       本文针对${}和#{}的解析过程进行分析,重点分析了#防止sql注入的原因,在工作开发中还是尽量使用#,避免sql注入。但是在order by后面的字段加单引号会导致排序失效,此时还是应该使用$符,有必要可以手动进行格式化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值