Java JDBC 防止 SQL 注入的正确写法

SQL 注入长期位居 OWASP Top 10 榜首,是 Web 应用面临的最经典也最危险的攻击方式之一。在 Java 生态中,JDBC 是连接数据库的底层标准接口,很多开发者虽然知道要“防注入”,但在实际编码中仍然习惯性地使用字符串拼接来构建 SQL 语句,从而留下严重的安全隐患。本文将从攻击原理出发,系统讲解 Java JDBC 中防止 SQL 注入的正确写法,并指出常见误区。

SQL 注入是如何发生的

SQL 注入的本质是:用户输入的数据被当作 SQL 代码的一部分执行了。当开发者使用字符串拼接的方式构造 SQL 时,数据库无法区分“这是数据”还是“这是指令”,攻击者就可以通过精心构造的输入改变原有 SQL 的语义。

举个最简单的例子:

String sql = "SELECT * FROM users WHERE username = '" + username + "' AND password = '" + password + "'";
Statement stmt = connection.createStatement();
ResultSet rs = stmt.executeQuery(sql);

如果攻击者在密码输入框中填入 ' OR '1'='1,拼接后的 SQL 就变成:

SELECT * FROM users WHERE username = 'admin' AND password = '' OR '1'='1'

由于 '1'='1' 永远为真,攻击者无需正确密码即可登录。更危险的是,攻击者还可以利用 UNION SELECT 窃取其他表的数据,或用 ; DROP TABLE 直接破坏数据库。

正确写法一:PreparedStatement(首选方案)

PreparedStatement 是 JDBC 防止 SQL 注入的标准方案。它的核心原理是:SQL 语句的结构在发送给数据库时就已经确定,参数值通过占位符 ? 传入,数据库会将参数值当作纯数据来处理,而不会将其解析为 SQL 语法。

String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, username);
pstmt.setString(2, password);
ResultSet rs = pstmt.executeQuery();

即使攻击者输入 ' OR '1'='1,数据库也只会把它当作一个普通的字符串去匹配,无法改变 SQL 的结构。

使用 PreparedStatement 时需要注意几点:

  • 所有用户输入都必须通过占位符传入,不能有任何拼接。例如 "WHERE id = " + id 这种写法即使使用了 PreparedStatement 也依然存在注入风险。
  • 表名、列名、ORDER BY 字段等不能使用占位符。这些属于 SQL 结构的一部分,占位符只适用于值。如果确实需要动态指定,必须在代码层面做白名单校验。
  • LIKE 查询要正确处理通配符。可以这样写:
String sql = "SELECT * FROM articles WHERE title LIKE ?";
pstmt.setString(1, "%" + keyword + "%");

注意通配符 % 是加在参数值里的,而不是拼在 SQL 字符串里。

正确写法二:CallableStatement 调用存储过程

如果业务逻辑通过存储过程实现,应使用 CallableStatement 并同样通过参数传递:

CallableStatement cstmt = connection.prepareCall("{call check_login(?, ?)}");
cstmt.setString(1, username);
cstmt.setString(2, password);
ResultSet rs = cstmt.executeQuery();

前提是存储过程内部也使用参数化查询,而不是在存储过程里再拼接 SQL。

正确写法三:配合输入校验与最小权限

参数化查询是根本,但不能作为唯一防线。纵深防御同样重要:

  1. 输入校验:对用户输入做类型、长度、格式校验。例如 ID 必须是数字,邮箱必须符合格式。这能过滤掉大量异常输入。
  2. 白名单机制:对于排序字段、表名等无法参数化的部分,使用白名单枚举,例如 if (!allowedColumns.contains(sortColumn)) throw new IllegalArgumentException();
  3. 数据库最小权限:应用连接数据库的账号只授予必要的权限,禁止 DROPFILE 等危险操作,即使被注入也能限制损害范围。
  4. 错误信息处理:不要将数据库原始异常信息直接返回给前端,避免泄露表结构等敏感信息。

常见误区与错误写法

以下写法都不能有效防止 SQL 注入:

  • 使用 Statement 拼接字符串:这是最典型的错误,前文已说明。
  • 手动转义单引号:例如 input.replace("'", "''")。这种方式容易遗漏,且不同数据库转义规则不同,无法覆盖所有攻击向量。
  • 只在部分参数上使用 PreparedStatement:只要有一个参数是拼接的,整个查询就可能被注入。
  • 认为 ORM 框架绝对安全:MyBatis 中使用 ${} 同样是字符串拼接,只有 #{} 才是参数化。Hibernate 的 HQL 拼接也存在风险。

总结

Java JDBC 防止 SQL 注入的正确做法可以归纳为一句话:始终使用 PreparedStatement 的参数化查询,绝不拼接用户输入。在此基础之上,配合输入校验、白名单、最小权限和错误信息收敛,构建纵深防御体系。安全不是某一个函数能解决的问题,而是贯穿编码习惯的系统工程。养成参数化的编码习惯,才能从根本上杜绝 SQL 注入。

如果你还想了解其他相关话题,可以参考本站的《XSS 的常见方式和前端如何防御》《MySQL 防止 SQL 注入的几种写法》以及《Linux 服务器安全加固清单》,从不同层面完善你的 Web 安全防护体系。

未经允许不得转载:任鹏个人博客 » Java JDBC 防止 SQL 注入的正确写法

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏