副标题:一个符号的差异,决定你的系统是否安全!🎯
🎬 开场: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永远安全,注入攻击无处可藏! 🛡️✨