核心原则:永远不要直接拼接字符串构造 SQL,优先使用
PreparedStatement(预编译语句),参数使用占位符?;MyBatis 使用#{},禁止${}。
一、原生 JDBC PreparedStatement(推荐,防注入)
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class JdbcSafeDemo {
public static User getUserById(String username) {
String url = "jdbc:mysql://127.0.0.1:3306/testdb?useSSL=false";
String user = "root";
String password = "123456";
// ✅ 使用 ? 占位符,参数预编译,不会把参数当成SQL执行
String sql = "SELECT id, username, age FROM user WHERE username = ?";
try (Connection conn = DriverManager.getConnection(url, user, password);
PreparedStatement pstmt = conn.prepareStatement(sql)) {
// 设置参数,JDBC会自动转义特殊字符
pstmt.setString(1, username);
try (ResultSet rs = pstmt.executeQuery()) {
if (rs.next()) {
User u = new User();
u.setId(rs.getLong("id"));
u.setUsername(rs.getString("username"));
u.setAge(rs.getInt("age"));
return u;
}
}
} catch (SQLException e) {
e.printStackTrace();
}
return null;
}
static class User {
private Long id;
private String username;
private Integer age;
// getter setter
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
}
public static void main(String[] args) {
// 注入测试 payload: ' or '1'='1
String payload = "' or '1'='1";
User user = getUserById(payload);
// 不会查询到数据,因为整个payload会被当做【字符串值】,不会解析成SQL语法
System.out.println(user);
}
}
❌ 危险写法(千万不要写!字符串拼接 SQL,会 SQL 注入)
// 危险!!不要使用!
String sql = "SELECT * FROM user WHERE username = '" + username + "'";
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
二、MyBatis 安全写法(最常用)
✅ #{} 预编译(安全)
<!-- 安全:#{username} 使用PreparedStatement占位符 -->
<select id="selectUser" resultType="User">
SELECT id,username,age FROM user WHERE username = #{username}
</select>
❌ ${} 直接字符串替换(存在注入风险,除非特殊场景)
<!-- 危险!${username} 直接字符串拼接,会SQL注入 -->
<select id="selectUser" resultType="User">
SELECT id,username,age FROM user WHERE username = ${username}
</select>
补充:
${}仅适合数据库表名、排序字段这类不能用占位符的场景,使用时必须做白名单校验!
三、Spring JdbcTemplate 安全示例
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
public class JdbcTemplateDemo {
private final JdbcTemplate jdbcTemplate;
public JdbcTemplateDemo(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
public User findUser(String username) {
String sql = "SELECT id,username,age FROM user WHERE username = ?";
RowMapper<User> rowMapper = (rs, rowNum) -> {
User u = new User();
u.setId(rs.getLong("id"));
u.setUsername(rs.getString("username"));
u.setAge(rs.getInt("age"));
return u;
};
// 参数放在可变参数,底层还是PreparedStatement
return jdbcTemplate.queryForObject(sql, rowMapper, username);
}
}
四、额外防御手段(多层防护)
输入校验:前端 + 后端校验参数格式,用户名只允许字母数字下划线,拒绝单引号、
--、union等敏感字符
// 简单校验示例
public static boolean validUsername(String username) {
return username.matches("^[a-zA-Z0-9_]{1,32}$");
}
最小权限账号:数据库业务账号不要给
drop、alter等高权限禁止错误堆栈对外暴露,防止泄露表结构
ORM 框架(MyBatis-Plus、JPA)默认参数绑定都是安全的,不要自己拼接原生 SQL 字符串
五、当必须动态拼接字段 / 表名(${} 场景)白名单示例
场景:动态排序
order by ${sortField},不能用?占位符,必须白名单
// 白名单,只允许这几个字段排序
private static final Set<String> SORT_WHITE_LIST = Set.of("id", "username", "age");
public List<User> listUser(String sortField) {
// 不在白名单直接拒绝
if (!SORT_WHITE_LIST.contains(sortField)) {
throw new IllegalArgumentException("非法排序字段");
}
String sql = "SELECT * FROM user ORDER BY " + sortField;
// ...执行
}本文原创作者:易君召,详见:https://www.yijunzhao.cc/about,转载请注明出处。
原文链接
欢迎访问