SQL 注入是 Web 安全頭號殺手,核心成因是「數據與代碼未分離」。本文構建三層防禦體系:參數化查詢(根治) + 全局請求過濾(攔截) + 字段級轉義(補丁),並給出生產級代碼示例。
原理:SQL 模板預編譯,參數僅作為數據傳入,永不改變查詢邏輯結構。
// ✅ 安全:參數化查詢
String sql = "SELECT * FROM users WHERE username = ? AND password = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, username);
ps.setString(2, password);
ResultSet rs = ps.executeQuery();
}
// ❌ 危險:字符串拼接
String sql = "SELECT * FROM users WHERE username = '" + username + "'";
強制規範:項目中禁用 Statement 拼接 SQL,所有動態查詢統一用 PreparedStatement 或 ORM 框架(MyBatis/JPA/Hibernate)的參數綁定。
@WebFilter("/*")
public class SqlInjectionFilter implements Filter {
// 常見 SQL 關鍵字與特殊符號(根據業務擴展)
private static final String INJECTION_PATTERN =
"(?i)(\\b(union|select|insert|update|delete|drop|alter|create|exec|declare|truncate)\\b|[';\\-+*/%])";
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
Map params = request.getParameterMap();
for (String[] values : params.values()) {
for (String value : values) {
if (value != null && value.matches(".*" + INJECTION_PATTERN + ".*")) {
log.warn("SQL注入嘗試攔截: IP={}, 參數值={}", request.getRemoteAddr(), value);
// 可選:直接阻斷、重定向錯誤頁、記錄審計日志
((HttpServletResponse) resp).sendError(HttpServletResponse.SC_BAD_REQUEST, "非法請求參數");
return;
}
}
}
chain.doFilter(req, resp);
}
}
優化建議:
Pattern,避免重複編譯開銷。
public class SqlUtils {
// 單引號轉義 + 註釋符過濾,僅作補丁,不可替代 PreparedStatement
public static String escapeForLegacy(String input) {
if (input == null) return null;
return input.replace("'", "''") // 標準 SQL 轉義
.replaceAll("(?i)(--|/\\*|\\*/)", ""); // 移除註釋符
}
}
適用場景:
IN 子句、ORDER BY 等無法參數化的片段(需極度謹慎)。#{param}、JPA ?1、Hibernate 命名參數,均等價於 PreparedStatement。防 SQL 注入的核心只有一條:數據永遠不參與 SQL 解析。PreparedStatement 是銀彈,Filter 是盾牌,轉義只是創可貼。架構層面統一參數化,才是治本之道。