Java JDBC PreparedStatement
Parametrisierte SQL-Abfragen sicher in Java mit PreparedStatement ausführen und SQL-Injection verhindern.
Ein PreparedStatement ist ein SQL-Template mit ?-Platzhaltern für die Werte. Die Werte werden separat per Index gesetzt, und der Treiber sendet das Template und die Daten über getrennte Kanäle — so kann ein Wert niemals als SQL interpretiert werden. Das ist die wichtigste Gewohnheit in JDBC: Sie macht Injection strukturell unmöglich und erlaubt der Datenbank, den Query-Plan wiederzuverwenden. Bevorzugen Sie es gegenüber einem einfachen Statement für praktisch alles.
Dieses Kapitel behandelt das Erstellen, Binden und Ausführen eines PreparedStatement, warum es SQL-Injection verhindert, wie NULL- und typisierte Werte gebunden werden und wann die Wiederverwendung eines Statements sich lohnt.
Erstellen, binden, ausführen
String sql = "INSERT INTO users (name, age) VALUES (?, ?)";
try (Connection conn = DriverManager.getConnection(url, user, pw);
PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, name); // bind by 1-based index
ps.setInt(2, age);
int rows = ps.executeUpdate();
}Die Platzhalter werden ab 1 nummeriert, nicht ab 0 — eine häufige Quelle für Off-by-one-Fehler. Jedes setXxx entspricht dem Typ der Spalte: setString, setInt, setBigDecimal, setTimestamp und so weiter.
Warum es Injection verhindert
Bei einem Statement ist der Wert Teil des SQL-Textes, sodass ein Anführungszeichen im Wert das Literal beenden und neue Befehle einschleusen kann. Bei einem PreparedStatement ist das SQL fest und wird vor dem Binden eines Wertes geparst; der Wert wird dann als typisierter Parameter übertragen. Es gibt keinen String, aus dem das Anführungszeichen eines Angreifers ausbrechen könnte — der gefährliche Wert aus dem Statement-Kapitel wird schlicht zu einem wörtlichen Namen.
Nach dem Ausführen einer Abfrage lesen Sie ihre Zeilen mit einem ResultSet, genau wie bei einem Statement.
NULL und spezielle Typen binden
Sie können kein Java-null an setInt übergeben (es erwartet einen primitiven Typ), und setString(i, null) ist für manche Typen mehrdeutig. Die explizite Form ist setNull(index, sqlType), wobei der Spaltentyp aus java.sql.Types angegeben wird:
ps.setNull(3, java.sql.Types.VARCHAR);Ein PreparedStatement wiederverwenden
Ein PreparedStatement ist darauf ausgelegt, viele Male mit unterschiedlichen Werten ausgeführt zu werden — setzen, ausführen, zurücksetzen, wiederholen. Die Datenbank parst und plant das SQL einmal und verwendet es wieder, weshalb PreparedStatements in einer Schleife auch schneller sind als das wiederholte Erstellen eines Statement-Strings. Für Massen-Inserts kombinieren Sie dies mit Batch-Verarbeitung.
Ein ausgearbeitetes Beispiel: Anatomie eines Templates und seiner Bindungen
Dieses Programm behandelt das SQL-Template als Daten: Es zählt die Platzhalter, durchläuft die Werte, die daran gebunden werden (einschließlich des schädlichen Strings, der das Statement kaputt gemacht hat), und zeigt den setNull-Typcode — all die beweglichen Teile der Parameterbindung, ohne eine Live-Datenbank.
Was man aus dem Lauf mitnehmen sollte:
- Das Template hat drei
?-Platzhalter, und das Programm zählt sie — diese Anzahl gibt genau an, wie vielesetXxx-Aufrufe gemacht werden müssen. Eine Abweichung (Parameter 4 einer Abfrage mit 3 Platzhaltern binden) löst bei der Ausführung einen Fehler aus. - Bindungen sind 1-basiert: Parameter 1 ist das erste
?. Die Schleife gibtbind 1,bind 2,bind 3aus, um dies zu verdeutlichen — der häufigste Anfängerfehler ist das Starten bei 0. - Der erste Wert ist derselbe
x'; DROP TABLE users;---String, der im letzten Kapitel dasStatementkompromittiert hat. Hier ist er nur ein Datenwert, der an Parameter 1 gebunden wird; der Treiber speichert ihn wörtlich als Namen. Die Injection wird durch Konstruktion neutralisiert, nicht durch Escaping. - Das
nullbei Parameter 3 ist der Grund, warumsetNull(index, Types.VARCHAR)existiert. JDBC benötigt den SQL-Typ, um der Datenbank mitzuteilen, welche Art von NULL es ist — Sie benennen ihn mit einerjava.sql.Types-Konstante. - Jeder Wert hat einen impliziten Typ —
String,int, NULL-of-VARCHAR — weshalb es einsetXxxpro Typ gibt statt eines einzigen stringly-typed Setters. Den Setter dem Spaltentyp anzupassen ist die Disziplin, die PreparedStatements sowohl sicher als auch korrekt hält.