Java预编译SQL案例怎么防注入

wen java案例 25

本文目录导读:

Java预编译SQL案例怎么防注入

  1. 核心原理
  2. 正确案例(防注入)
  3. 错误案例(易注入)
  4. 特殊场景处理
  5. 最佳实践总结
  6. 防护验证示例

在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 原则

  1. 永远不要拼接SQL字符串,除非经过严格白名单校验
  2. 始终使用预编译API:PreparedStatement, #{}, :param
  3. 输入验证:在业务层进行参数合法性校验
  4. 最小权限:数据库用户只赋予必要的权限

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注入攻击,保护数据安全。

抱歉,评论功能暂时关闭!