别让 like 变成 “后门“!MyBatis 防 SQL 注入实战:5 种方案彻底筑牢数据安全防线
本文详细解析了在MyBatis中使用like语句时防止SQL注入的5种安全方案:1. 参数占位符+CONCAT函数:在SQL层面安全拼接模糊查询条件;2. Java手动拼接%符号:将处理逻辑前置到代码层;3. MyBatis bind标签:在XML中完成安全拼接;4. MyBatis-Plus QueryWrapper:利用框架提供的安全查询方法;5. 输入校验:基础防御措施。文章强调应优先使用参
- 本文总字数:约 7800 字 预计阅读时间:25 分钟
作为一名 Java 开发者,你是否曾在使用 MyBatis 的 like 语句时如履薄冰?当用户输入带有特殊字符的查询条件,简单的字符串拼接可能瞬间变成黑客入侵的 "后门",导致 SQL 注入攻击,造成数据泄露、篡改甚至服务器被控制等严重后果。
根据 OWASP(开放式 Web 应用安全项目)的统计,SQL 注入连续多年位居 Web 应用安全风险 TOP 10 前列,而 like 语句正是 SQL 注入的重灾区。本文将从原理到实践,全方位解析在 MyBatis 中使用 like 语句时如何有效防止 SQL 注入,通过 5 种实战方案 + 底层原理剖析,让你既知其然,更知其所以然,彻底杜绝这类安全隐患。
一、SQL 注入与 like 语句:为什么这里是重灾区?
在深入解决方案之前,我们必须先理解:为什么 like 语句特别容易引发 SQL 注入问题?
SQL 注入的本质是 "将用户输入的数据当作代码执行"。当我们使用 like 进行模糊查询时,通常需要在查询条件前后添加百分号(%)作为通配符,这种拼接操作恰恰给了攻击者可乘之机。
1.1 一个危险的错误示例
假设我们要实现一个根据用户名模糊查询的功能,很多初学者可能会这样写
<select id="findByUsernameLike" parameterType="String" resultType="User">
SELECT id, username, email, create_time
FROM t_user
WHERE username LIKE '%${username}%'
</select>
public interface UserMapper {
List<User> findByUsernameLike(String username);
}
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> searchUsers(String username) {
log.info("搜索用户,用户名关键词:{}", username);
return userMapper.findByUsernameLike(username);
}
}
这段代码看似简单直接,但隐藏着巨大的安全隐患。当正常用户输入 "张" 时,生成的 SQL 是:
SELECT id, username, email, create_time FROM t_user WHERE username LIKE '%张%'
一切正常。但如果有恶意用户输入的是:张%' OR '1'='1,那么生成的 SQL 就变成了:
SELECT id, username, email, create_time FROM t_user WHERE username LIKE '%张%' OR '1'='1%'
这个 SQL 会查询出表中所有记录,因为'1'='1永远为真!更危险的是,如果输入%' OR 1=1; DROP TABLE t_user;--,可能会直接删除整个表(取决于数据库用户权限)。
1.2 根本原因:$ 与 #的区别
MyBatis 中${}和#{}的区别是导致这个问题的核心:
${}是字符串替换,直接将参数拼接到 SQL 中,相当于 JDBC 中的 Statement#{}是参数占位符,会将参数作为一个整体,使用 PreparedStatement 的参数绑定
使用${}时,MyBatis 不会对参数进行任何处理,直接替换到 SQL 中,这就导致了 SQL 注入的风险。而#{}会将参数用引号包裹,并且对特殊字符进行转义,从而防止注入。
因此,在任何情况下,都应优先使用#{}而非${},尤其是处理用户输入时。
二、方案一:MyBatis 参数占位符 + CONCAT 函数动态拼接
第一种安全的方案是使用 MyBatis 的参数占位符#{}结合数据库的 CONCAT 函数来动态拼接模糊查询条件。
2.1 实现方式
UserMapper.xml
<select id="findByUsernameLikeWithConcat" parameterType="String" resultType="User">
SELECT id, username, email, create_time
FROM t_user
WHERE username LIKE CONCAT('%', #{username}, '%')
</select>
UserMapper.java
public interface UserMapper {
// 使用CONCAT函数拼接%的模糊查询
List<User> findByUsernameLikeWithConcat(String username);
}
UserService.java
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> searchUsersWithConcat(String username) {
// 入参校验
if (!StringUtils.hasText(username)) {
log.warn("搜索关键词为空,返回空列表");
return Collections.emptyList();
}
log.info("使用CONCAT方式搜索用户,关键词:{}", username);
return userMapper.findByUsernameLikeWithConcat(username);
}
}
2.2 原理分析
这种方式的核心是利用 MySQL 的 CONCAT 函数在 SQL 层面完成字符串拼接,将%符号和用户输入的参数拼接成完整的查询条件。
当用户输入 "张" 时,生成的 SQL 是:
SELECT id, username, email, create_time FROM t_user WHERE username LIKE CONCAT('%', '张', '%')
预处理后实际执行的 SQL 相当于:
SELECT id, username, email, create_time FROM t_user WHERE username LIKE ?
其中?被替换为%张%,但这是作为一个完整的参数值处理的。
即使用户输入恶意内容,比如张%' OR '1'='1,最终的查询条件会变成%张%' OR '1'='1%,MyBatis 会将整个字符串作为一个查询条件,而不是解析为 SQL 语句的一部分,从而有效防止了注入。
2.3 优缺点分析
优点:
- 完全防止 SQL 注入,因为使用了参数绑定
- 实现简单,只需要修改 SQL 语句
- 不依赖 Java 代码处理,逻辑集中在 SQL 层面
缺点:
- 依赖数据库的 CONCAT 函数,不同数据库的函数可能不同(如 Oracle 使用
||连接符) - 如果需要在不同数据库间迁移,可能需要修改 SQL
2.4 跨数据库兼容处理
如果你的应用需要支持多种数据库,可以使用 MyBatis 的数据库厂商标识(databaseIdProvider)来处理:
mybatis-config.xml
<databaseIdProvider type="DB_VENDOR">
<property name="MySQL" value="mysql"/>
<property name="Oracle" value="oracle"/>
<property name="SQL Server" value="sqlserver"/>
</databaseIdProvider>
UserMapper.xml
<select id="findByUsernameLikeWithConcat" parameterType="String" resultType="User">
<!-- MySQL实现 -->
<if test="_databaseId == 'mysql'">
SELECT id, username, email, create_time
FROM t_user
WHERE username LIKE CONCAT('%', #{username}, '%')
</if>
<!-- Oracle实现 -->
<if test="_databaseId == 'oracle'">
SELECT id, username, email, create_time
FROM t_user
WHERE username LIKE '%' || #{username} || '%'
</if>
<!-- SQL Server实现 -->
<if test="_databaseId == 'sqlserver'">
SELECT id, username, email, create_time
FROM t_user
WHERE username LIKE '%' + #{username} + '%'
</if>
</select>
这种方式可以保证在不同数据库环境下都能安全地使用 like 语句进行模糊查询。
三、方案二:手动拼接 % 并使用参数占位符
第二种方案是在 Java 代码中手动拼接%符号,然后在 MyBatis 映射文件中使用#{}参数占位符。
3.1 实现方式
UserMapper.xm
<select id="findByUsernameLikeWithManualConcat" parameterType="String" resultType="User">
SELECT id, username, email, create_time
FROM t_user
WHERE username LIKE #{username}
</select>
UserMapper.java
public interface UserMapper {
// 手动拼接%的模糊查询
List<User> findByUsernameLikeWithManualConcat(String username);
}
UserService.java
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> searchUsersWithManualConcat(String username) {
// 入参校验与处理
if (!StringUtils.hasText(username)) {
log.warn("搜索关键词为空,返回空列表");
return Collections.emptyList();
}
// 手动拼接%通配符
String likePattern = "%" + username + "%";
log.info("使用手动拼接方式搜索用户,处理后的关键词:{}", likePattern);
return userMapper.findByUsernameLikeWithManualConcat(likePattern);
}
}
3.2 原理分析
这种方式将%符号的拼接逻辑从 SQL 转移到了 Java 代码中,在调用 Mapper 方法之前就已经将用户输入的关键词包装成%关键词%的形式。
当用户输入 "张" 时,Java 代码会将其处理为 "% 张 %",然后 MyBatis 生成的 SQL 是:
SELECT id, username, email, create_time FROM t_user WHERE username LIKE ?
其中?被安全地替换为 "% 张 %"。
对于恶意输入张%' OR '1'='1,Java 代码会将其处理为%张%' OR '1'='1%,MyBatis 会将整个字符串作为查询条件,而不是解析为 SQL 的一部分,因此不会产生注入风险。
3.3 优缺点分析
优点:
- 完全防止 SQL 注入,使用参数绑定
- 不依赖数据库特定函数,具有良好的跨数据库兼容性
- 拼接逻辑在 Java 代码中,便于统一处理和维护
缺点:
- 需要在 Java 代码中处理,增加了代码量
- 如果有多处类似查询,需要重复编写拼接逻辑
- 可能会忘记拼接,导致查询结果不符合预期
3.4 优化:使用工具类统一处理
为了避免重复代码,可以创建一个工具类专门处理模糊查询的参数拼接:
LikeQueryUtils.java
/**
* 模糊查询工具类,处理like语句的参数拼接
*/
public class LikeQueryUtils {
/**
* 将关键词包装为前后都带%的形式,用于全模糊查询
*
* @param keyword 原始关键词
* @return 包装后的关键词,如:%keyword%
*/
public static String wrapWithPercent(String keyword) {
if (!StringUtils.hasText(keyword)) {
return "";
}
return "%" + keyword + "%";
}
/**
* 将关键词包装为前带%的形式,用于后缀模糊查询
*
* @param keyword 原始关键词
* @return 包装后的关键词,如:%keyword
*/
public static String wrapWithPrefixPercent(String keyword) {
if (!StringUtils.hasText(keyword)) {
return "";
}
return "%" + keyword;
}
/**
* 将关键词包装为后带%的形式,用于前缀模糊查询
*
* @param keyword 原始关键词
* @return 包装后的关键词,如:keyword%
*/
public static String wrapWithSuffixPercent(String keyword) {
if (!StringUtils.hasText(keyword)) {
return "";
}
return keyword + "%";
}
}
然后在 Service 中使用:
public List<User> searchUsersWithManualConcat(String username) {
if (!StringUtils.hasText(username)) {
log.warn("搜索关键词为空,返回空列表");
return Collections.emptyList();
}
// 使用工具类处理
String likePattern = LikeQueryUtils.wrapWithPercent(username);
log.info("使用工具类处理的关键词:{}", likePattern);
return userMapper.findByUsernameLikeWithManualConcat(likePattern);
}
这种方式既保证了代码的复用性,又统一了处理逻辑,减少了出错的可能性。
四、方案三:使用 MyBatis 的 bind 标签
第三种方案是使用 MyBatis 提供的<bind>标签,在 SQL 映射文件中定义一个变量并完成字符串拼接,然后在查询中使用这个变量。
4.1 实现方式
UserMapper.xml
<select id="findByUsernameLikeWithBind" parameterType="String" resultType="User">
<!-- 使用bind标签定义变量 -->
<bind name="likePattern" value="'%' + username + '%'" />
SELECT id, username, email, create_time
FROM t_user
WHERE username LIKE #{likePattern}
</select>
如果参数是一个对象,例如 UserQuery:
UserQuery.java
/**
* 用户查询条件类
*/
@Data
public class UserQuery {
// 用户名关键词
private String username;
// 邮箱关键词
private String email;
// 分页参数
private Integer pageNum;
private Integer pageSize;
}
对应的 Mapper.xml:
<select id="findByCondition" parameterType="UserQuery" resultType="User">
<!-- 对用户名进行模糊查询处理 -->
<if test="username != null and username != ''">
<bind name="usernameLike" value="'%' + username + '%'" />
</if>
<!-- 对邮箱进行模糊查询处理 -->
<if test="email != null and email != ''">
<bind name="emailLike" value="'%' + email + '%'" />
</if>
SELECT id, username, email, create_time
FROM t_user
WHERE 1=1
<if test="username != null and username != ''">
AND username LIKE #{usernameLike}
</if>
<if test="email != null and email != ''">
AND email LIKE #{emailLike}
</if>
</select>
UserMapper.java
public interface UserMapper {
// 使用bind标签的模糊查询
List<User> findByUsernameLikeWithBind(String username);
// 多条件查询,包含bind标签使用
List<User> findByCondition(UserQuery query);
}
UserService.java
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> searchUsersWithBind(String username) {
if (!StringUtils.hasText(username)) {
log.warn("搜索关键词为空,返回空列表");
return Collections.emptyList();
}
log.info("使用bind标签方式搜索用户,关键词:{}", username);
return userMapper.findByUsernameLikeWithBind(username);
}
public List<User> searchUsersByCondition(UserQuery query) {
if (Objects.isNull(query)) {
log.warn("查询条件为空,返回空列表");
return Collections.emptyList();
}
log.info("多条件搜索用户,查询参数:{}", query);
return userMapper.findByCondition(query);
}
}
4.2 原理分析
MyBatis 的<bind>标签允许我们在 OGNL 表达式中创建一个变量,并将其绑定到当前的上下文。在这个例子中,我们创建了一个名为likePattern的变量,其值是'%' + username + '%',其中username是传入的参数。
然后,在 SQL 语句中使用#{likePattern}来引用这个变量,MyBatis 会将其作为参数绑定,而不是直接拼接,从而防止 SQL 注入。
这种方式的本质是在 MyBatis 框架内部完成了字符串拼接,然后仍然使用参数绑定的方式设置到 SQL 中,兼具了拼接的灵活性和参数绑定的安全性。
4.3 优缺点分析
优点:
- 完全防止 SQL 注入,使用参数绑定
- 不依赖数据库特定函数,具有良好的跨数据库兼容性
- 拼接逻辑在 SQL 映射文件中,与 SQL 查询放在一起,便于理解
- 支持复杂的条件判断和多参数处理
缺点:
- 增加了 XML 文件的复杂度
- 对于复杂的拼接逻辑,OGNL 表达式可能不够灵活
4.4 高级用法:结合 OGNL 表达式处理特殊情况
<bind>标签支持 OGNL 表达式,可以处理更复杂的场景,例如:
<select id="findByUsernameLikeWithBindAdvanced" parameterType="String" resultType="User">
<!-- 处理空字符串,避免生成'%%'的情况 -->
<bind name="likePattern" value="username != null and username != '' ? '%' + username + '%' : null" />
SELECT id, username, email, create_time
FROM t_user
<where>
<if test="likePattern != null">
username LIKE #{likePattern}
</if>
<if test="likePattern == null">
1=1 -- 当没有查询条件时的默认条件
</if>
</where>
</select>
这个例子中,我们使用 OGNL 表达式判断参数是否为空,如果为空则将likePattern设为 null,然后在 SQL 中通过<if>标签判断是否添加查询条件,避免了参数为空时生成username LIKE '%%'这样的无意义查询。
五、方案四:使用 MyBatis-Plus 的 QueryWrapper
如果你在项目中使用 MyBatis-Plus(简称 MP),那么可以利用其提供的 QueryWrapper 来优雅地实现安全的模糊查询。
5.1 实现方式
首先确保项目中引入了 MyBatis-Plus 的依赖(以 Maven 为例):
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
User.java(实体类)
@Data
@TableName("t_user")
public class User {
@TableId(type = IdType.AUTO)
private Long id;
private String username;
private String email;
@TableField(value = "create_time")
private LocalDateTime createTime;
}
UserMapper.java(继承 BaseMapper)
public interface UserMapper extends BaseMapper<User> {
// MP的BaseMapper已经提供了基本的CRUD方法
}
UserService.java
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> searchUsersWithMP(String username) {
if (!StringUtils.hasText(username)) {
log.warn("搜索关键词为空,返回空列表");
return Collections.emptyList();
}
log.info("使用MyBatis-Plus搜索用户,关键词:{}", username);
// 创建查询条件构造器
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 使用like方法,MP会自动处理参数绑定
queryWrapper.like("username", username);
// 执行查询
return userMapper.selectList(queryWrapper);
}
// 多条件复杂查询示例
public List<User> searchUsersWithMPAdvanced(UserQuery query) {
if (Objects.isNull(query)) {
log.warn("查询条件为空,返回空列表");
return Collections.emptyList();
}
log.info("使用MyBatis-Plus高级搜索用户,查询参数:{}", query);
// 创建查询条件构造器
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
// 用户名模糊查询
if (StringUtils.hasText(query.getUsername())) {
queryWrapper.like("username", query.getUsername());
}
// 邮箱模糊查询
if (StringUtils.hasText(query.getEmail())) {
queryWrapper.like("email", query.getEmail());
}
// 分页处理(需要配合分页插件)
Page<User> page = new Page<>(query.getPageNum(), query.getPageSize());
// 执行分页查询
Page<User> resultPage = userMapper.selectPage(page, queryWrapper);
return resultPage.getRecords();
}
}
5.2 原理分析
MyBatis-Plus 的 QueryWrapper 在处理 like 方法时,内部会自动使用参数绑定的方式生成 SQL,而不是字符串拼接。
当调用queryWrapper.like("username", username)时,MP 会生成类似以下的 SQL 片段:
username LIKE ?
并将参数值处理为%username%,通过 PreparedStatement 的 setString 方法设置参数,从而避免 SQL 注入。
MP 还提供了不同的模糊查询方法:
like(R column, Object val): 等同于LIKE '%val%'likeLeft(R column, Object val): 等同于LIKE '%val'likeRight(R column, Object val): 等同于LIKE 'val%'
这些方法都会安全地处理参数,防止 SQL 注入。
5.3 优缺点分析
优点:
- 完全防止 SQL 注入,底层使用参数绑定
- 不需要编写 XML 映射文件,通过 Java 代码构建查询条件
- 提供了丰富的查询方法,满足各种复杂查询需求
- 支持链式调用,代码简洁易读
- 自动处理数据库差异,具有良好的跨数据库兼容性
缺点:
- 需要引入 MyBatis-Plus 依赖,适合新项目或可以引入新依赖的项目
- 对于极其复杂的 SQL,可能仍然需要编写 XML 或使用原生 SQL
5.4 分页查询配置
为了使分页查询生效,需要配置 MyBatis-Plus 的分页插件:
MyBatisPlusConfig.java
@Configuration
@MapperScan("com.example.mapper")
public class MyBatisPlusConfig {
/**
* 配置分页插件
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
// 可以添加其他插件,如乐观锁插件、防全表更新插件等
return interceptor;
}
}
这个配置会自动拦截分页查询,并生成分页 SQL,如 MySQL 中的LIMIT语句。
六、方案五:输入校验 —— 从源头阻断恶意输入
无论使用哪种技术方案,输入校验都是不可或缺的一环。通过对用户输入进行严格校验,可以从源头阻断大部分恶意输入,大大提高系统的安全性。
6.1 实现方式
InputValidator.java(输入校验工具类)
/**
* 输入校验工具类,用于验证用户输入的合法性
*/
public class InputValidator {
// 定义合法的模糊查询关键词正则表达式
// 只允许字母、数字、汉字、下划线、空格、小数点和部分常用标点
private static final Pattern VALID_KEYWORD_PATTERN =
Pattern.compile("^[a-zA-Z0-9\\u4e00-\\u9fa5_\\s\\.,,。??!!;;::()()]{1,100}$");
/**
* 验证模糊查询关键词是否合法
*
* @param keyword 待验证的关键词
* @return 如果合法返回true,否则返回false
*/
public static boolean isValidKeyword(String keyword) {
if (!StringUtils.hasText(keyword)) {
return false;
}
return VALID_KEYWORD_PATTERN.matcher(keyword).matches();
}
/**
* 清理关键词中的特殊字符
*
* @param keyword 原始关键词
* @return 清理后的关键词
*/
public static String sanitizeKeyword(String keyword) {
if (!StringUtils.hasText(keyword)) {
return "";
}
// 移除所有不合法的字符
return keyword.replaceAll("[^a-zA-Z0-9\\u4e00-\\u9fa5_\\s\\.,,。??!!;;::()()]", "");
}
}
UserService.java(加入输入校验)
@Slf4j
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> searchUsersWithValidation(String username) {
// 1. 空值校验
if (!StringUtils.hasText(username)) {
log.warn("搜索关键词为空,返回空列表");
return Collections.emptyList();
}
// 2. 合法性校验
if (!InputValidator.isValidKeyword(username)) {
log.warn("搜索关键词包含非法字符,原始输入:{}", username);
// 方案一:直接拒绝查询
// log.warn("拒绝非法查询,关键词:{}", username);
// return Collections.emptyList();
// 方案二:清理非法字符后继续查询
String sanitizedKeyword = InputValidator.sanitizeKeyword(username);
log.info("已清理非法字符,处理后的关键词:{}", sanitizedKeyword);
username = sanitizedKeyword;
// 如果清理后为空,返回空列表
if (!StringUtils.hasText(username)) {
log.warn("清理后关键词为空,返回空列表");
return Collections.emptyList();
}
}
log.info("经过校验的关键词搜索用户:{}", username);
// 3. 执行查询(以MyBatis-Plus为例)
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.like("username", username);
return userMapper.selectList(queryWrapper);
}
}
6.2 原理分析
输入校验的核心思想是 "不信任任何用户输入",通过以下两种方式提高安全性:
- 合法性校验:使用正则表达式等方式检查输入是否符合预期格式,拒绝明显的恶意输入。
- 输入清理:移除或转义输入中的特殊字符,即使通过了合法性校验,也对输入进行净化处理。
在模糊查询场景中,我们主要关注可能用于 SQL 注入的特殊字符,如单引号(')、双引号(")、分号(;)、注释符(--、#)等。通过正则表达式,我们只允许输入字母、数字、汉字和部分安全的标点符号。
6.3 优缺点分析
优点:
- 从源头阻断恶意输入,是防御 SQL 注入的第一道防线
- 可以结合业务场景定制校验规则,灵活性高
- 减轻后续处理逻辑的安全压力
- 不仅能防止 SQL 注入,还能防止 XSS 等其他安全问题
缺点:
- 可能误判合法输入,需要谨慎设计校验规则
- 不能完全替代其他防注入措施,需要与其他方案配合使用
- 复杂的校验规则可能影响性能
6.4 高级校验:使用 Spring Validation
对于复杂的参数校验,可以使用 Spring 提供的 Validation 框架:
UserQuery.java(使用 Validation 注解)
@Data
public class UserQuery {
// 用户名关键词,长度1-100,只允许特定字符
@Pattern(regexp = "^[a-zA-Z0-9\\u4e00-\\u9fa5_\\s\\.,,。??!!;;::()()]{1,100}$",
message = "用户名关键词包含非法字符或长度不符")
private String username;
// 邮箱关键词,符合邮箱格式的模糊查询
@Pattern(regexp = "^[a-zA-Z0-9_\\-\\.@]{1,100}$",
message = "邮箱关键词包含非法字符")
private String email;
// 页码,最小值为1
@Min(value = 1, message = "页码不能小于1")
private Integer pageNum = 1;
// 每页条数,1-100之间
@Min(value = 1, message = "每页条数不能小于1")
@Max(value = 100, message = "每页条数不能大于100")
private Integer pageSize = 10;
}
UserController.java(使用 @Valid 注解触发校验)
@Slf4j
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/search")
public ResponseEntity<List<User>> searchUsers(@Valid UserQuery query,
BindingResult bindingResult) {
// 检查校验结果
if (bindingResult.hasErrors()) {
// 获取错误信息
String errorMsg = bindingResult.getFieldErrors().stream()
.map(FieldError::getDefaultMessage)
.collect(Collectors.joining(";"));
log.warn("查询参数校验失败:{}", errorMsg);
return ResponseEntity.badRequest().body(Collections.emptyList());
}
List<User> users = userService.searchUsersByCondition(query);
return ResponseEntity.ok(users);
}
}
这种方式将校验规则与实体类绑定,通过注解的方式声明校验条件,代码更简洁,且校验逻辑与业务逻辑分离,便于维护。
七、扩展:SQL 注入的深层防御策略
除了上述针对 like 语句的特定方案,我们还应该在系统层面采取一些深层防御策略,构建多层次的安全防线。
7.1 使用最小权限原则配置数据库用户
数据库用户的权限应该遵循 "最小权限原则",即只授予应用程序运行所必需的权限,不授予不必要的权限(如 DROP、ALTER 等)。
例如,对于一个只读查询的应用,可以只授予 SELECT 权限:
-- 创建应用专用用户
CREATE USER 'app_user'@'%' IDENTIFIED BY 'strong_password';
-- 只授予查询权限
GRANT SELECT ON mydb.* TO 'app_user'@'%';
-- 刷新权限
FLUSH PRIVILEGES;
这样即使发生 SQL 注入,攻击者也无法执行删除表、修改数据等破坏性操作,最大限度地降低损失。
7.2 使用数据库连接池的参数过滤
一些数据库连接池或 JDBC 驱动提供了参数过滤功能,可以配置对特定字符进行转义或过滤。
例如,在 HikariCP 连接池中,可以配置连接属性:
spring:
datasource:
hikari:
jdbc-url: jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=utf8&allowMultiQueries=false
username: app_user
password: strong_password
driver-class-name: com.mysql.cj.jdbc.Driver
其中allowMultiQueries=false禁止执行多个 SQL 语句,防止攻击者通过分号分隔多个 SQL 的注入方式。
7.3 日志记录与监控
记录详细的 SQL 执行日志和用户操作日志,便于及时发现异常查询和潜在的注入攻击:
@Slf4j
@Aspect
@Component
public class SqlLoggingAspect {
@Around("execution(* com.example.mapper..*(..))")
public Object logSqlExecution(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
try {
// 执行目标方法
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
// 记录正常执行的SQL信息
log.info("SQL执行成功,方法:{},耗时:{}ms",
joinPoint.getSignature().toShortString(),
endTime - startTime);
return result;
} catch (Exception e) {
// 记录执行异常的SQL信息
log.error("SQL执行异常,方法:{},参数:{},异常:{}",
joinPoint.getSignature().toShortString(),
Arrays.toString(joinPoint.getArgs()),
e.getMessage(), e);
throw e;
}
}
}
通过监控这些日志,可以及时发现包含异常字符的 SQL 查询,为安全审计和攻击溯源提供依据。
7.4 定期安全审计与渗透测试
定期对系统进行安全审计和渗透测试,模拟黑客攻击的方式来发现潜在的安全漏洞。可以使用专业的安全工具(如 OWASP ZAP、Burp Suite 等)进行自动化测试,同时结合人工代码审查,确保所有 like 语句和其他 SQL 操作都采取了足够的安全措施。
八、总结:如何选择合适的方案?
通过本文的介绍,我们了解了 5 种在 MyBatis 中使用 like 语句时防止 SQL 注入的方案,每种方案都有其适用场景:
- 参数占位符 + CONCAT 函数:适用于单数据库环境,SQL 逻辑与 Java 代码分离的场景。
- 手动拼接 % 并使用参数占位符:适用于跨数据库环境,希望在 Java 代码中统一处理拼接逻辑的场景。
- MyBatis 的 bind 标签:适用于复杂的条件查询,希望在 XML 中集中管理 SQL 逻辑的场景。
- MyBatis-Plus 的 QueryWrapper:适用于使用 MyBatis-Plus 框架,希望通过 Java 代码构建查询条件的场景。
- 输入校验:这不是一个独立的方案,而是所有方案的基础,必须与其他方案结合使用。
在实际项目中,建议采取 "多层防御" 策略:
- 首先对所有用户输入进行严格的校验和清理
- 然后根据项目框架和团队习惯选择合适的 like 查询方案
- 同时配置数据库最小权限和连接池参数
- 最后通过日志监控和定期审计确保安全措施的有效性
记住安全是一个持续的过程,没有一劳永逸的解决方案。随着攻击手段的不断进化,我们的防御策略也需要不断更新和完善。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐


所有评论(0)