本文目录导读:

在Java中,使用预编译SQL(PreparedStatement)是防止SQL注入最有效的方法,下面是完整的防注入案例和原理说明:
核心原理
预编译SQL的防注入原理:参数与SQL语句分离编译,参数值被当作纯数据处理,不会改变SQL语义。
正确案例(防注入)
1 基本PreparedStatement使用
public class SafeQueryExample {
// 正确:使用PreparedStatement
public User getUserById(Long id) {
String sql = "SELECT * FROM users WHERE id = ?";
try (Connection conn = dataSource.getConnection();
PreparedStatement pstmt = conn.prepareStatement(sql)) {
// 参数绑定(自动处理转义)
pstmt.setLong(1, id);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
return mapUser(rs);
}
}
} catch (SQLException e) {
log.error("查询用户失败", e);
}
return null;
}
// 多个参数示例
public List<User> searchUsers(String username, String email) {
String sql = "SELECT * FROM users WHERE username = ? AND email = ?";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, username);
pstmt.setString(2, email);
try (ResultSet rs = pstmt.executeQuery()) {
List<User> users = new ArrayList<>();
while (rs.next()) {
users.add(mapUser(rs));
}
return users;
}
}
}
}
2 MyBatis框架示例
<!-- Mapper XML文件:使用#{}实现预编译 -->
<select id="getUser" resultType="User">
SELECT * FROM users
WHERE username = #{username}
AND password = #{password}
<!-- #{xxx} 会生成 PreparedStatement 的 ? 占位符 -->
</select>
// MyBatis Mapper接口
@Mapper
public interface UserMapper {
// 安全:使用#{}
@Select("SELECT * FROM users WHERE username = #{username}")
User findByUsername(@Param("username") String username);
// 安全:动态SQL也使用#{}
@Select("<script>" +
"SELECT * FROM users WHERE 1=1" +
"<if test='username != null'> AND username = #{username}</if>" +
"<if test='email != null'> AND email = #{email}</if>" +
"</script>")
List<User> findByCondition(UserQuery query);
}
3 JPA/Hibernate示例
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
// 安全:JPQL使用参数绑定
@Query("SELECT u FROM User u WHERE u.username = :username")
User findByUsername(@Param("username") String username);
// 安全:Native SQL也支持参数绑定
@Query(value = "SELECT * FROM users WHERE username = :username",
nativeQuery = true)
User findByUsernameNative(@Param("username") String username);
}
// EntityManager方式
public User findUserByCondition(String username) {
String jpql = "SELECT u FROM User u WHERE u.username = :username";
return entityManager.createQuery(jpql, User.class)
.setParameter("username", username)
.getSingleResult();
}
错误案例(易注入)
// ❌ 错误1:使用字符串拼接SQL
public User findUserByUsername(String username) {
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
// 如果username = "' OR '1'='1",将查询所有用户!
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
}
// ❌ 错误2:MyBatis中使用${}
@Select("SELECT * FROM users WHERE username = ${username}")
// 使用${}不会预编译,会直接替换,存在注入风险
User findUserUnsafe(@Param("username") String username);
// ❌ 错误3:JPA中原生SQL拼接
@Query(value = "SELECT * FROM users WHERE username = '" + ":username" + "'",
nativeQuery = true)
// 错误的参数绑定方式
特殊场景处理
1 LIKE查询防注入
// 正确:手动处理通配符转义
public List<User> searchUsers(String keyword) {
String sql = "SELECT * FROM users WHERE username LIKE ? ESCAPE '/'";
// 转义特殊字符
String safeKeyword = keyword
.replace("!", "!!")
.replace("%", "!%")
.replace("_", "!_")
.replace("[", "![");
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
pstmt.setString(1, "%" + safeKeyword + "%");
// ...
}
}
2 IN子句处理
// 正确:动态生成占位符
public List<User> findUsersByIds(List<Long> ids) {
if (ids == null || ids.isEmpty()) {
return Collections.emptyList();
}
// 生成占位符: ?, ?, ?
String placeholders = ids.stream()
.map(id -> "?")
.collect(Collectors.joining(", "));
String sql = "SELECT * FROM users WHERE id IN (" + placeholders + ")";
try (PreparedStatement pstmt = conn.prepareStatement(sql)) {
for (int i = 0; i < ids.size(); i++) {
pstmt.setLong(i + 1, ids.get(i));
}
// ...
}
}
// MyBatis方式:使用collection
@Select("<script>" +
"SELECT * FROM users WHERE id IN " +
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>" +
"#{id}" +
"</foreach>" +
"</script>")
List<User> findByIds(@Param("ids") List<Long> ids);
3 动态排序(特殊处理)
// 排序字段不能使用预编译,需要白名单校验
public List<User> findUsers(String sortField, String sortOrder) {
// 白名单校验
Set<String> allowedFields = new HashSet<>(Arrays.asList(
"id", "username", "email", "create_time"
));
if (!allowedFields.contains(sortField)) {
throw new IllegalArgumentException("Invalid sort field");
}
// 校验排序方向
String order = "ASC".equalsIgnoreCase(sortOrder) ? "ASC" : "DESC";
String sql = "SELECT * FROM users ORDER BY " + sortField + " " + order;
// 这里字段名经过白名单校验,相对安全
try (Statement stmt = conn.createStatement()) {
ResultSet rs = stmt.executeQuery(sql);
// ...
}
}
最佳实践总结
1 原则
- 永远不要拼接SQL字符串,除非经过严格白名单校验
- 始终使用预编译API:PreparedStatement, #{}, :param
- 输入验证:在业务层进行参数合法性校验
- 最小权限:数据库用户只赋予必要的权限
2 检查清单
| 场景 | 正确做法 | 错误做法 |
|---|---|---|
| 基本查询 | WHERE id = ? |
WHERE id = '${id}' |
| MyBatis | ||
| LIKE查询 | 手动转义通配符 | 直接拼接 |
| 排序字段 | 白名单校验 | 直接拼接 |
| IN子句 | 动态?占位符/foreach | 字符串拼接 |
| 批量操作 | batch + 参数绑定 | 循环执行Statement |
防护验证示例
// 注入攻击测试
public void testInjection() {
String maliciousInput = "' OR '1'='1";
// 使用PreparedStatement
String safeSql = "SELECT * FROM users WHERE username = ?";
try (PreparedStatement pstmt = conn.prepareStatement(safeSql)) {
pstmt.setString(1, maliciousInput); // 当作普通字符串处理
ResultSet rs = pstmt.executeQuery();
// 不会返回所有用户,因为条件变成了:
// username = '' OR '1'='1' => 被转义为字符串
// 实际执行的SQL:SELECT * FROM users WHERE username = ''' OR ''1''=''1'
}
// 使用Statement拼接
String unsafeSql = "SELECT * FROM users WHERE username = '" + maliciousInput + "'";
// 实际执行的SQL:SELECT * FROM users WHERE username = '' OR '1'='1'
// 返回所有用户!存在注入漏洞
}
通过严格遵守这些规范,可以有效防止SQL注入攻击,保护数据安全。