🛡️ MyBatis的#{}和${}:安全 vs 危险!

副标题:一个符号的差异,决定你的系统是否安全!🎯


🎬 开场:SQL注入的恐怖

真实案例

vbnet 复制代码
某公司用户登录被黑:

正常登录:
username: admin
password: 123456

SQL: SELECT * FROM users WHERE username = 'admin' AND password = '123456'

黑客登录:
username: admin' --
password: (任意)

SQL: SELECT * FROM users WHERE username = 'admin' -- ' AND password = '任意'
                                         ↑
                                      注释掉后面

结果:直接登录成功!💥

原因:使用了${username},导致SQL注入!

📚 #{}和${}的区别

基本概念

markdown 复制代码
#{}:预编译,安全 ✅
- 参数使用占位符 ?
- 参数值会被当作字符串处理
- 自动添加引号
- 防止SQL注入

${}:字符串替换,危险 ❌
- 直接拼接到SQL中
- 不会添加引号
- 存在SQL注入风险
- 不推荐使用

示例对比

xml 复制代码
<!-- 使用#{} -->
<select id="selectByName" resultType="User">
    SELECT * FROM users WHERE name = #{name}
</select>

<!-- 
执行过程:
1. SQL预编译:SELECT * FROM users WHERE name = ?
2. 设置参数:ps.setString(1, "张三")
3. 最终SQL:SELECT * FROM users WHERE name = '张三'

特点:
- 参数会自动加引号
- 防止SQL注入
- 推荐使用 ✅
-->

<!-- 使用${} -->
<select id="selectByName2" resultType="User">
    SELECT * FROM users WHERE name = '${name}'
</select>

<!--
执行过程:
1. 直接替换:SELECT * FROM users WHERE name = '张三'

特点:
- 直接字符串替换
- 需要手动加引号
- 存在SQL注入风险 ❌
-->

🎯 详细对比

#{}:预编译(推荐)

java 复制代码
/**
 * #{}:预编译方式
 */
@Mapper
public interface UserMapper {
    
    /**
     * 使用#{}查询
     */
    @Select("SELECT * FROM users WHERE name = #{name}")
    User selectByName(String name);
}

// 调用
User user = userMapper.selectByName("张三");

// MyBatis处理流程:
// 1. 生成预编译SQL
String sql = "SELECT * FROM users WHERE name = ?";

// 2. 创建PreparedStatement
PreparedStatement ps = connection.prepareStatement(sql);

// 3. 设置参数(自动添加引号和转义)
ps.setString(1, "张三");

// 4. 执行查询
ResultSet rs = ps.executeQuery();

// 最终SQL:
SELECT * FROM users WHERE name = '张三'

#{}的特点

markdown 复制代码
1. 预编译
   - SQL和参数分开
   - 数据库会缓存执行计划
   - 性能更好

2. 类型处理
   - 自动添加引号(字符串)
   - 自动类型转换
   - ps.setString()、ps.setInt()等

3. 安全
   - 参数会被转义
   - 防止SQL注入 ✅
   
4. 性能
   - 预编译可复用
   - 减少SQL解析时间

${}:字符串替换(危险)

java 复制代码
/**
 * ${}:字符串替换方式
 */
@Mapper
public interface UserMapper {
    
    /**
     * 使用${}查询(危险!)
     */
    @Select("SELECT * FROM users WHERE name = '${name}'")
    User selectByName2(String name);
}

// 调用
User user = userMapper.selectByName2("张三");

// MyBatis处理流程:
// 1. 直接字符串替换
String sql = "SELECT * FROM users WHERE name = '" + "张三" + "'";
// 结果:SELECT * FROM users WHERE name = '张三'

// 2. 创建Statement(不是PreparedStatement)
Statement stmt = connection.createStatement();

// 3. 直接执行
ResultSet rs = stmt.executeQuery(sql);

${}的特点

markdown 复制代码
1. 字符串替换
   - 直接拼接到SQL中
   - 不会预编译

2. 无类型处理
   - 不会自动添加引号
   - 需要手动处理
   
3. 不安全
   - 存在SQL注入风险 ❌
   - 不推荐使用

4. 性能
   - 每次都要解析SQL
   - 性能较差

🚨 SQL注入风险

注入攻击示例

java 复制代码
/**
 * 危险代码:使用${}
 */
@Mapper
public interface UserMapper {
    
    @Select("SELECT * FROM users WHERE name = '${name}'")
    User selectByName(String name);
}

// 正常调用
User user1 = userMapper.selectByName("张三");
// SQL: SELECT * FROM users WHERE name = '张三' ✅

// 恶意输入1:查询所有数据
User user2 = userMapper.selectByName("' OR '1'='1");
// SQL: SELECT * FROM users WHERE name = '' OR '1'='1' 
// 结果:返回所有用户!❌

// 恶意输入2:删除数据
User user3 = userMapper.selectByName("'; DELETE FROM users WHERE '1'='1");
// SQL: SELECT * FROM users WHERE name = ''; DELETE FROM users WHERE '1'='1' 
// 结果:所有用户被删除!💥

// 恶意输入3:union注入
User user4 = userMapper.selectByName("' UNION SELECT * FROM admin_users WHERE '1'='1");
// SQL: SELECT * FROM users WHERE name = '' UNION SELECT * FROM admin_users WHERE '1'='1'
// 结果:泄露管理员数据!💥

安全代码:使用#{}

java 复制代码
/**
 * 安全代码:使用#{}
 */
@Mapper
public interface UserMapper {
    
    @Select("SELECT * FROM users WHERE name = #{name}")
    User selectByName(String name);
}

// 恶意输入被转义
User user = userMapper.selectByName("' OR '1'='1");

// 预编译SQL:SELECT * FROM users WHERE name = ?
// 参数设置:ps.setString(1, "' OR '1'='1")
// 最终SQL:SELECT * FROM users WHERE name = '\' OR \'1\'=\'1\''
//                                            ↑   ↑    ↑
//                                          转义符

// 结果:查不到数据,安全 ✅

🎯 使用场景

#{}的使用场景(推荐)

xml 复制代码
<!-- 1. WHERE条件 ✅ -->
<select id="selectById" resultType="User">
    SELECT * FROM users WHERE id = #{id}
</select>

<!-- 2. IN查询(foreach) ✅ -->
<select id="selectByIds" resultType="User">
    SELECT * FROM users WHERE id IN
    <foreach collection="ids" item="id" open="(" separator="," close=")">
        #{id}
    </foreach>
</select>

<!-- 3. INSERT语句 ✅ -->
<insert id="insert">
    INSERT INTO users (name, age) VALUES (#{name}, #{age})
</insert>

<!-- 4. UPDATE语句 ✅ -->
<update id="update">
    UPDATE users SET name = #{name}, age = #{age} WHERE id = #{id}
</update>

<!-- 5. LIKE查询(推荐方式) ✅ -->
<select id="selectByNameLike" resultType="User">
    SELECT * FROM users WHERE name LIKE CONCAT('%', #{name}, '%')
</select>

<!-- 或使用JDBC的方式 -->
<select id="selectByNameLike2" resultType="User">
    SELECT * FROM users WHERE name LIKE #{pattern}
</select>

// 调用时拼接通配符
String pattern = "%" + name + "%";
List<User> users = userMapper.selectByNameLike2(pattern);

${}的使用场景(谨慎)

xml 复制代码
<!-- 1. 动态表名 -->
<select id="selectByTable" resultType="Map">
    SELECT * FROM ${tableName} WHERE id = #{id}
</select>

<!-- 
注意:
- tableName必须从可信来源获取(如配置文件)
- 不能直接使用用户输入
- 最好使用白名单验证
-->

// 安全的使用方式
public List<Map> selectByTable(String tableName, Long id) {
    // 白名单验证
    if (!Arrays.asList("users", "orders", "products").contains(tableName)) {
        throw new IllegalArgumentException("Invalid table name");
    }
    
    return userMapper.selectByTable(tableName, id);
}

<!-- 2. 动态列名 -->
<select id="selectByColumn" resultType="User">
    SELECT * FROM users ORDER BY ${column} ${direction}
</select>

// 白名单验证
public List<User> selectByColumn(String column, String direction) {
    // 验证列名
    if (!Arrays.asList("id", "name", "age").contains(column)) {
        throw new IllegalArgumentException("Invalid column");
    }
    
    // 验证排序方向
    if (!Arrays.asList("ASC", "DESC").contains(direction)) {
        throw new IllegalArgumentException("Invalid direction");
    }
    
    return userMapper.selectByColumn(column, direction);
}

<!-- 3. 动态SQL片段 -->
<sql id="columns">
    id, name, age
</sql>

<select id="selectAll" resultType="User">
    SELECT ${columns} FROM users
</select>

🛡️ 防御SQL注入

方法1:使用#{}(推荐)⭐⭐⭐⭐⭐

xml 复制代码
<!-- 永远优先使用#{} -->
<select id="selectByName" resultType="User">
    SELECT * FROM users WHERE name = #{name}
</select>

方法2:白名单验证 ⭐⭐⭐⭐

java 复制代码
/**
 * 使用白名单验证
 */
@Service
public class UserService {
    
    // 允许的表名
    private static final Set<String> VALID_TABLES = 
        Set.of("users", "orders", "products");
    
    // 允许的列名
    private static final Set<String> VALID_COLUMNS = 
        Set.of("id", "name", "age", "create_time");
    
    public List<Map> selectByTable(String tableName) {
        // 白名单验证
        if (!VALID_TABLES.contains(tableName)) {
            throw new IllegalArgumentException("Invalid table name: " + tableName);
        }
        
        return userMapper.selectByTable(tableName);
    }
    
    public List<User> orderBy(String column, String direction) {
        // 验证列名
        if (!VALID_COLUMNS.contains(column)) {
            throw new IllegalArgumentException("Invalid column: " + column);
        }
        
        // 验证排序方向
        if (!"ASC".equals(direction) && !"DESC".equals(direction)) {
            throw new IllegalArgumentException("Invalid direction: " + direction);
        }
        
        return userMapper.orderBy(column, direction);
    }
}

方法3:输入验证和转义 ⭐⭐⭐

java 复制代码
/**
 * 输入验证
 */
public class SqlInjectionValidator {
    
    /**
     * 验证是否包含SQL注入关键字
     */
    public static boolean isSqlInjection(String input) {
        if (input == null) {
            return false;
        }
        
        // SQL注入关键字
        String[] keywords = {
            "'", "\"", "--", "/*", "*/", ";",
            "or", "and", "union", "select", "insert", 
            "update", "delete", "drop", "create", "alter"
        };
        
        String lowerInput = input.toLowerCase();
        
        for (String keyword : keywords) {
            if (lowerInput.contains(keyword)) {
                return true;
            }
        }
        
        return false;
    }
    
    /**
     * 转义特殊字符
     */
    public static String escapeSQL(String input) {
        if (input == null) {
            return null;
        }
        
        return input
            .replace("\\", "\\\\")  // \ -> \\
            .replace("'", "\\'")    // ' -> \'
            .replace("\"", "\\\"")  // " -> \"
            .replace("\n", "\\n")   // 换行
            .replace("\r", "\\r");  // 回车
    }
}

方法4:ORM框架(推荐)⭐⭐⭐⭐⭐

java 复制代码
/**
 * 使用MyBatis的安全方式
 */

// ❌ 错误:拼接SQL
String sql = "SELECT * FROM users WHERE name = '" + name + "'";

// ✅ 正确:使用#{}
@Select("SELECT * FROM users WHERE name = #{name}")
User selectByName(String name);

// ✅ 正确:使用Criteria API(MyBatis-Plus)
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getName, name);
List<User> users = userMapper.selectList(wrapper);

📊 性能对比

预编译 vs 字符串拼接

java 复制代码
/**
 * 性能测试
 */
@Test
public void performanceTest() {
    long startTime, endTime;
    int times = 10000;
    
    // 测试#{}(预编译)
    startTime = System.currentTimeMillis();
    for (int i = 0; i < times; i++) {
        userMapper.selectByName("张三");  // 使用#{}
    }
    endTime = System.currentTimeMillis();
    System.out.println("#{}耗时:" + (endTime - startTime) + "ms");
    
    // 测试${}(字符串替换)
    startTime = System.currentTimeMillis();
    for (int i = 0; i < times; i++) {
        userMapper.selectByName2("张三");  // 使用${}
    }
    endTime = System.currentTimeMillis();
    System.out.println("${}耗时:" + (endTime - startTime) + "ms");
}

/**
 * 测试结果:
 * #{}耗时:1200ms  ✅ 更快
 * ${}耗时:1800ms  ❌ 更慢
 * 
 * 原因:
 * - #{}使用预编译,SQL可以复用
 * - ${}每次都要解析SQL
 */

🎉 总结

核心区别

维度 #{} ${}
处理方式 预编译(PreparedStatement) 字符串替换(Statement)
安全性 安全,防SQL注入 ✅ 不安全,有SQL注入风险 ❌
性能 快(预编译可复用) 慢(每次解析SQL)
类型处理 自动添加引号和转义 需要手动处理
使用场景 参数值 表名、列名(谨慎使用)
推荐度 ⭐⭐⭐⭐⭐ ⭐(仅特殊场景)

使用建议

markdown 复制代码
1. 默认使用#{}
   - 所有参数值都用#{}
   - 安全、性能好

2. 特殊场景使用${}
   - 动态表名
   - 动态列名
   - 必须加白名单验证!

3. 防御措施
   - 输入验证
   - 白名单
   - 永远不要信任用户输入

4. 代码审查
   - 检查所有${}的使用
   - 确保有安全措施

记忆口诀

sql 复制代码
井号和美元符号,
MyBatis参数两方式。

井号预编译安全,
问号占位类型转换。
自动添加引号转义,
SQL注入无处藏。
性能更好可复用,
推荐首选是井号!

美元字符串替换,
直接拼接很危险。
不会添加引号转义,
SQL注入有风险。
每次解析性能差,
千万小心别乱用。

使用场景要分清,
参数值用井号,
表名列名用美元,
但要加上白名单验证。

永远不要信用户输入,
验证转义不能少。
安全第一记心上,
井号美元要分清!

总结一句话记牢:
能用井号不用美元号!

愿你的SQL永远安全,注入攻击无处可藏! 🛡️✨

相关推荐
uhakadotcom5 小时前
ChatGPT Atlas的使用笔记
后端·面试·github
得物技术5 小时前
从一次启动失败深入剖析:Spring循环依赖的真相|得物技术
java·后端
程序猿DD5 小时前
Jackson 序列化的隐性成本
java·后端
用户68545375977695 小时前
⚡ Spring Boot自动配置:约定优于配置的魔法!
后端
aicoding_sh5 小时前
为 Claude Code CLI 提供美观且高度可定制的状态行,具有powerline support, themes, and more.
后端·github
码农小站5 小时前
从零搭建vsftpd服务器:避坑指南+实战解决方案
后端
掘金一周5 小时前
一个前端工程师的年度作品:从零开发媲美商业级应用的后台管理系统 | 掘金一周 10.23
前端·人工智能·后端
凤山老林5 小时前
SpringBoot 如何实现零拷贝:深度解析零拷贝技术
java·linux·开发语言·arm开发·spring boot·后端