当前位置: 首页 > news >正文

手写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解析阶段
处理组件ParameterHandlerTextSqlNode
替换结果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;}}

总结与建议

通过源码级别的深度分析,我们可以清晰地看到#{}${}参数在实现机制上的根本差异:

  1. 安全性#{}通过预编译从根本上防止SQL注入,${}存在严重的安全风险

  2. 性能#{}可以利用预编译提升重复查询性能,${}无法享受这一优化

  3. 适用场景#{}适用于值参数,${}仅适用于动态SQL结构(表名、列名等)

最佳实践建议:

  • 默认情况下始终使用#{}参数

  • 仅在必须使用动态SQL结构时才考虑${}参数

  • 使用${}时必须进行严格的白名单验证

  • 避免将用户输入直接用于${}参数

理解这些底层机制不仅有助于我们编写更安全的代码,还能在遇到问题时快速定位根本原因,这是成为高级开发者的重要里程碑。

🥂(❁´◡`❁)您的点赞👍➕评论📝➕收藏⭐是作者创作的最大动力🤞

💖📕🎉🔥 支持我:点赞👍+收藏⭐️+留言📝欢迎留言讨论

🔥🔥🔥(源码 + 调试运行 + 问题答疑)

🔥🔥🔥  有兴趣可以联系我。文末有免费源码

💖学习知识需费心,
📕整理归纳更费神。
🎉源码免费人人喜,
🔥码农福利等你领!

💖常来我家多看看,
📕网址:扣棣编程
🎉感谢支持常陪伴,
🔥点赞关注别忘记!

💖山高路远坑又深,
📕大军纵横任驰奔,
🎉谁敢横刀立马行?
🔥唯有点赞+关注成!

往期文章推荐:

基于Springboot + vue实现的学生宿舍信息管理系统
免费获取宠物商城源码--SpringBoot+Vue宠物商城网站系统 
【2025小年源码免费送】

⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇点击此处获取源码⬇⬇⬇⬇⬇⬇⬇⬇⬇

http://www.dtcms.com/a/478200.html

相关文章:

  • 【Pytorch】数学运算
  • 金泉网站建设开发手机网站开发注意
  • 当Excel遇上大语言模型:ExcelAgentTemplate架构深度剖析与实战指南
  • 新农村建设在哪个网站申请vi设计包含的内容
  • 达梦数据库相关术语及管理操作
  • 百度网站推广公司济南网络优化推广
  • 【SpringBoot从初学者到专家的成长14】SpringBoot项目结构介绍
  • mongodb一个服务器部署多个节点
  • 基金网站制作工程承包公司
  • 成都企业网站建设价格搜索引擎收录
  • 第9章:两条道路的风景:技术与管理的真实世界(4)
  • 基于frenet坐标系的规划与避障
  • 从本地到云端:Fiora+cpolar打造真正的私密社交通讯站
  • Vue Router 导航守卫
  • 技术评测丨RPA主流平台稳定性、安全与信创适配能力对比
  • 简约淘宝网站模板免费下载建立 wiki 网站
  • 【Unity】uNet游戏服务端框架(三)心跳机制
  • 二叉树的深搜
  • C++设计模式之行为型模式:模板方法模式(Template Method)
  • 做3dh春丽网站叫什么重庆十大软件公司
  • 长沙电商网站开发php开发网站后台
  • QT6中Combo Box与Combo BoxFont 功能及用法
  • 软考网工知识点-1
  • win10下Qt应用程序使用FastDDS
  • 链表相关的知识以及算法题
  • 模板网站建站步骤微信公众号和小程序的区别
  • Shell 使用指南
  • 重庆网站seo服务没效果
  • 开源项目重构我们应该怎么做-以 SQL 血缘系统开源项目为例
  • Sora2:AIGC的技术革命与生态重构