手写MyBatis第106弹:#{}预编译安全机制 vs ${}字符串替换风险 - 源码级深度解析
MyBatis参数处理内核解析:#{}与${}的源码级差异揭秘
「MyBatis参数处理终极对决:#{}预编译安全机制 vs ${}字符串替换风险 - 源码级深度解析」
两种参数处理的本质差异
在MyBatis的使用中,
#{}
和${}
是两种截然不同的参数处理方式,它们不仅在用法上存在差异,在底层实现机制上更是天壤之别。通过源码层面的深入分析,我们可以清晰地看到这两种方式在安全性、性能和执行时机上的根本区别。
目录
MyBatis参数处理内核解析:#{}与${}的源码级差异揭秘
两种参数处理的本质差异
#{}参数:预编译的安全卫士
ParameterHandler的预编译处理机制
类型处理器的核心作用
参数映射的构建过程
${}参数:字符串替换的风险之源
TextSqlNode的字符串替换机制
OGNL表达式求值的风险
两种参数处理的执行时机对比
处理阶段的根本差异
执行流程的源码追踪
#{}参数的完整执行路径
${}参数的完整执行路径
安全风险的实证分析
SQL注入攻击场景还原
#{}参数的安全保障
性能影响的分析
预编译的性能优势
字符串替换的性能代价
适用场景的深度分析
${}参数的合理使用场景
动态表名和列名
数据库函数调用
安全使用${}参数的最佳实践
总结与建议
🥂(❁´◡`❁)您的点赞👍➕评论📝➕收藏⭐是作者创作的最大动力🤞
💖📕🎉🔥 支持我:点赞👍+收藏⭐️+留言📝欢迎留言讨论
🔥🔥🔥(源码 + 调试运行 + 问题答疑)
🔥🔥🔥 有兴趣可以联系我。文末有免费源码
免费获取源码。
更多内容敬请期待。如有需要可以联系作者免费送
更多源码定制,项目修改,项目二开可以联系作者
点击可以进行搜索(每人免费送一套代码):千套源码目录(点我)2025元旦源码免费送(点我)
我们常常在当下感到时间慢,觉得未来遥远,但一旦回头看,时间已经悄然流逝。对于未来,尽管如此,也应该保持一种从容的态度,相信未来仍有许多可能性等待着我们。
#{}参数:预编译的安全卫士
ParameterHandler的预编译处理机制
#{}
参数的处理发生在SQL执行阶段,由ParameterHandler
负责将其转换为JDBC的预编译参数。这种机制从根本上防止了SQL注入攻击,同时提供了类型安全的参数处理。
public class DefaultParameterHandler implements ParameterHandler {private final TypeHandlerRegistry typeHandlerRegistry;private final MappedStatement mappedStatement;private final Object parameterObject;private final BoundSql boundSql;private final Configuration configuration;@Overridepublic void setParameters(PreparedStatement ps) throws SQLException {// 获取参数映射列表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)) {// 处理动态SQL生成的额外参数value = boundSql.getAdditionalParameter(propertyName);} else if (parameterObject == null) {value = null;} else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) {// 基本类型参数直接使用value = parameterObject;} else {// 复杂对象参数,通过MetaObject反射获取属性值MetaObject metaObject = configuration.newMetaObject(parameterObject);value = metaObject.getValue(propertyName);}// 获取类型处理器TypeHandler typeHandler = parameterMapping.getTypeHandler();JdbcType jdbcType = parameterMapping.getJdbcType();// 处理null值if (value == null && jdbcType == null) {jdbcType = configuration.getJdbcTypeForNull();}try {// 关键步骤:使用类型处理器设置预编译参数typeHandler.setParameter(ps, i + 1, value, jdbcType);} catch (TypeException | SQLException e) {throw new TypeException("Could not set parameters for mapping: " + parameterMapping + ". Cause: " + e, e);}}}}}}
类型处理器的核心作用
类型处理器是#{}
参数安全性的关键保障,它负责Java类型与JDBC类型之间的安全转换:
public interface TypeHandler<T> {void setParameter(PreparedStatement ps, int i, T parameter, JdbcType jdbcType) throws SQLException;T getResult(ResultSet rs, String columnName) throws SQLException;T getResult(ResultSet rs, int columnIndex) throws SQLException;T getResult(CallableStatement cs, int columnIndex) throws SQLException;}
以StringTypeHandler为例的具体实现:
public class StringTypeHandler extends BaseTypeHandler<String> {@Overridepublic void setParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType) throws SQLException {if (parameter == null) {if (jdbcType == null) {// 设置NULL值ps.setNull(i, Types.VARCHAR);} else {// 使用指定的JDBC类型设置NULLps.setNull(i, jdbcType.TYPE_CODE);}} else {// 关键安全措施:使用PreparedStatement.setString方法// 这个方法会自动处理特殊字符,防止SQL注入ps.setString(i, parameter);}}@Overridepublic String getResult(ResultSet rs, String columnName) throws SQLException {return rs.getString(columnName);}}
参数映射的构建过程
#{}
参数在SQL解析阶段就被识别并构建为ParameterMapping
对象:
public class SqlSourceBuilder extends BaseBuilder {public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {ParameterMappingTokenHandler handler = new ParameterMappingTokenHandler();GenericTokenParser parser = new GenericTokenParser("#{", "}", handler);// 将#{}替换为?,同时构建ParameterMappingString sql = parser.parse(originalSql);return new StaticSqlSource(configuration, sql, handler.getParameterMappings());}private static class ParameterMappingTokenHandler implements TokenHandler {private List<ParameterMapping> parameterMappings = new ArrayList<>();@Overridepublic String handleToken(String content) {// 为每个#{}参数创建ParameterMappingparameterMappings.add(buildParameterMapping(content));return "?"; // 替换为JDBC占位符}private ParameterMapping buildParameterMapping(String content) {Map<String, String> propertiesMap = parseParameterMapping(content);String property = propertiesMap.get("property");String javaType = propertiesMap.get("javaType");String jdbcType = propertiesMap.get("jdbcType");// 构建ParameterMapping.BuilderParameterMapping.Builder builder = new ParameterMapping.Builder(configuration, property, resolveClass(javaType));// 设置JDBC类型if (jdbcType != null) {builder.jdbcType(JdbcType.valueOf(jdbcType));}// 设置类型处理器if (typeHandler != null) {builder.typeHandler(resolveTypeHandler(typeHandler));}return builder.build();}}}
${}参数:字符串替换的风险之源
TextSqlNode的字符串替换机制
${}
参数的处理发生在SQL解析阶段,由TextSqlNode
直接进行字符串替换,这种机制带来了SQL注入的安全风险。
public class TextSqlNode implements SqlNode {private final String text;public TextSqlNode(String text) {this.text = text;}public boolean isDynamic() {// 检查文本是否包含${}占位符return text.contains("${");}@Overridepublic boolean apply(DynamicContext context) {// 创建GenericTokenParser处理${}占位符GenericTokenParser parser = new GenericTokenParser("${", "}", new BindingTokenParser(context));// 执行字符串替换String parsedText = parser.parse(text);// 将替换后的文本追加到动态上下文context.appendSql(parsedText);return true;}private static class BindingTokenParser implements TokenHandler {private final DynamicContext context;public BindingTokenParser(DynamicContext context) {this.context = context;}@Overridepublic String handleToken(String content) {// 从绑定上下文中获取参数值Object value = OgnlCache.getValue(content, context.getBindings());// 直接转换为字符串 - 这是安全风险的根源!String strValue = value == null ? "" : String.valueOf(value);// 直接返回字符串值,没有任何安全处理return strValue;}}}
OGNL表达式求值的风险
${}
参数使用OGNL表达式从参数对象中提取值,这本身也带来了安全风险:
public class OgnlCache {private static final Map<String, Object> expressionCache = new ConcurrentHashMap<>();public static Object getValue(String expression, Object root) {try {// 创建OGNL上下文Map<String, Object> context = Ognl.createDefaultContext(root, new OgnlMemberAccess(), new OgnlClassResolver(), null);// 编译OGNL表达式Object node = expressionCache.get(expression);if (node == null) {node = Ognl.parseExpression(expression);expressionCache.put(expression, node);}// 执行表达式求值 - 可能执行任意代码!return Ognl.getValue(node, context, root);} catch (OgnlException e) {throw new BuilderException("Error evaluating expression '" + expression + "'. Cause: " + e, e);}}}
两种参数处理的执行时机对比
处理阶段的根本差异
特性 | #{} 参数 | ${} 参数 |
---|---|---|
处理阶段 | SQL执行阶段 | SQL解析阶段 |
处理组件 | ParameterHandler | TextSqlNode |
替换结果 | JDBC占位符(?) | 直接字符串替换 |
安全性 | 防止SQL注入 | 存在SQL注入风险 |
执行流程的源码追踪
#{}参数的完整执行路径
// 1. SQL解析阶段:构建ParameterMappingpublic class SqlSourceBuilder {public SqlSource parse(String originalSql, Class<?> parameterType, Map<String, Object> additionalParameters) {// #{}被替换为?,同时记录参数映射信息String sql = parser.parse(originalSql); // "SELECT * FROM user WHERE id = ?"return new StaticSqlSource(configuration, sql, parameterMappings);}}// 2. SQL执行阶段:参数绑定public class PreparedStatementHandler {public void parameterize(Statement statement) {// 委托给ParameterHandler设置参数parameterHandler.setParameters((PreparedStatement) statement);}}// 3. 数据库执行:预编译安全执行// 最终生成的JDBC调用:preparedStatement.setString(1, "safeValue");
${}参数的完整执行路径
// 1. 动态SQL解析阶段:字符串替换public class TextSqlNode {public boolean apply(DynamicContext context) {// ${}被直接替换为参数值String parsedText = parser.parse(text); // "SELECT * FROM user WHERE id = 123"context.appendSql(parsedText);return true;}}// 2. 生成最终SQLpublic class DynamicSqlSource {public BoundSql getBoundSql(Object parameterObject) {DynamicContext context = new DynamicContext(configuration, parameterObject);rootSqlNode.apply(context); // 应用所有SqlNode,包括TextSqlNodeString finalSql = context.getSql(); // 包含已替换的${}参数// ... 后续处理}}// 3. 数据库执行:直接执行拼接的SQL// 最终生成的SQL:SELECT * FROM user WHERE id = 123
安全风险的实证分析
SQL注入攻击场景还原
通过源码分析,我们可以清晰地看到${}
参数为何容易导致SQL注入:
// 攻击者输入的参数
String userInput = "1 OR 1=1; DROP TABLE users; --";// 使用${}参数的Mapper配置
// <select id="findUser">SELECT * FROM users WHERE id = ${id}</select>// TextSqlNode的处理结果
String finalSql = "SELECT * FROM users WHERE id = 1 OR 1=1; DROP TABLE users; --";// 直接执行这个SQL,导致数据泄露和表被删除!
#{}参数的安全保障
相比之下,#{}
参数提供了完整的安全防护:
// 同样的攻击者输入
String userInput = "1 OR 1=1; DROP TABLE users; --";// 使用#{}参数的Mapper配置
// <select id="findUser">SELECT * FROM users WHERE id = #{id}</select>// ParameterHandler的处理结果
PreparedStatement ps = connection.prepareStatement("SELECT * FROM users WHERE id = ?");
ps.setString(1, "1 OR 1=1; DROP TABLE users; --");// 最终执行的SQL:SELECT * FROM users WHERE id = '1 OR 1=1; DROP TABLE users; --'
// 攻击代码被当作普通的字符串值,无法执行!
性能影响的分析
预编译的性能优势
#{}
参数利用数据库的预编译功能,可以显著提升重复查询的性能:
// 第一次执行:数据库需要编译SQL
PreparedStatement ps = connection.prepareStatement("SELECT * FROM users WHERE id = ?");
ps.setString(1, "1001");
ResultSet rs = ps.executeQuery(); // 编译 + 执行// 后续执行:复用已编译的执行计划
ps.setString(1, "1002");
rs = ps.executeQuery(); // 仅执行,无需编译
ps.setString(1, "1003");
rs = ps.executeQuery(); // 仅执行,无需编译
字符串替换的性能代价
${}
参数每次都需要生成新的SQL语句,无法利用预编译的优势:
// 每次都是不同的SQL,需要重新编译
String sql1 = "SELECT * FROM users WHERE id = 1001";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql1); // 编译 + 执行String sql2 = "SELECT * FROM users WHERE id = 1002";
stmt = connection.createStatement();
rs = stmt.executeQuery(sql2); // 重新编译 + 执行String sql3 = "SELECT * FROM users WHERE id = 1003";
stmt = connection.createStatement();
rs = stmt.executeQuery(sql3); // 再次编译 + 执行
适用场景的深度分析
${}参数的合理使用场景
尽管存在安全风险,但在某些特定场景下${}
参数是必要的:
动态表名和列名
<!-- 多租户系统的分表查询 -->
<select id="findDataByTenant" parameterType="map">SELECT * FROM ${tablePrefix}_business_data WHERE tenant_id = #{tenantId}
</select><!-- 动态排序字段 -->
<select id="findWithDynamicSort" parameterType="map">SELECT * FROM products ORDER BY ${sortField} ${sortOrder}
</select>
数据库函数调用
<!-- 调用数据库特定函数 -->
<select id="findWithFunction" parameterType="map">SELECT * FROM records WHERE ${dateFunction}(create_time) = #{targetDate}
</select>
安全使用${}参数的最佳实践
当必须使用${}
参数时,应该采取严格的安全措施:
public class SafeSqlFilter {private static final Set<String> ALLOWED_COLUMNS = Set.of("id", "name", "create_time");private static final Set<String> ALLOWED_ORDERS = Set.of("ASC", "DESC");private static final Pattern SAFE_PATTERN = Pattern.compile("^[a-zA-Z_][a-zA-Z0-9_]*$");public static String validateColumnName(String columnName) {if (!ALLOWED_COLUMNS.contains(columnName)) {throw new SecurityException("Invalid column name: " + columnName);}if (!SAFE_PATTERN.matcher(columnName).matches()) {throw new SecurityException("Potential SQL injection detected: " + columnName);}return columnName;}public static String validateOrder(String order) {if (!ALLOWED_ORDERS.contains(order.toUpperCase())) {throw new SecurityException("Invalid order: " + order);}return order;}}
总结与建议
通过源码级别的深度分析,我们可以清晰地看到#{}
和${}
参数在实现机制上的根本差异:
-
安全性:
#{}
通过预编译从根本上防止SQL注入,${}
存在严重的安全风险 -
性能:
#{}
可以利用预编译提升重复查询性能,${}
无法享受这一优化 -
适用场景:
#{}
适用于值参数,${}
仅适用于动态SQL结构(表名、列名等)
最佳实践建议:
-
默认情况下始终使用
#{}
参数 -
仅在必须使用动态SQL结构时才考虑
${}
参数 -
使用
${}
时必须进行严格的白名单验证 -
避免将用户输入直接用于
${}
参数
理解这些底层机制不仅有助于我们编写更安全的代码,还能在遇到问题时快速定位根本原因,这是成为高级开发者的重要里程碑。
🥂(❁´◡`❁)您的点赞👍➕评论📝➕收藏⭐是作者创作的最大动力🤞
💖📕🎉🔥 支持我:点赞👍+收藏⭐️+留言📝欢迎留言讨论
🔥🔥🔥(源码 + 调试运行 + 问题答疑)
🔥🔥🔥 有兴趣可以联系我。文末有免费源码
💖学习知识需费心,
📕整理归纳更费神。
🎉源码免费人人喜,
🔥码农福利等你领!💖常来我家多看看,
📕网址:扣棣编程,
🎉感谢支持常陪伴,
🔥点赞关注别忘记!💖山高路远坑又深,
📕大军纵横任驰奔,
🎉谁敢横刀立马行?
🔥唯有点赞+关注成!
往期文章推荐:
基于Springboot + vue实现的学生宿舍信息管理系统
免费获取宠物商城源码--SpringBoot+Vue宠物商城网站系统
【2025小年源码免费送】