Thursday, December 27, 2012

Hochperformante und sichere SQL Zugriffe in Java

Beim Zugriff auf SQL Datenbanken wird immer gerne der Fehler gemacht, dass wenn ein SQL Statement mehrmals mit verschiedenen Parametern ausgeführt werden soll nicht die dafür vorgesehenen PreparedStatements sondern die SQL Befehle mit String Konkatinierung  zusammengebaut und mit execute() ausgeführt werden. Sogar in Java Lehrbüchern findet man immer wieder Beispiele wie das folgende, dass man aber in der Praxis auf keinen Fall so verwenden sollte.

public class StatementJDBC {
 private static Statement stmt;
 /**
  * @param args
  */
 public static void main(String[] args) {
  //Verbinde mit Datenbank und initalisiere Anweisung
  AS400JDBCDataSource dataSource = new AS400JDBCDataSource("localhost", "user", "password");
  try {
   Connection con=dataSource.getConnection();
   stmt=con.createStatement();
  //Gib verschiedene Namen aus der Adressdatei mit verschiedenen Adressnummern aus.
   Date start=new Date();
   System.out.println(getName("40"));
   System.out.println(getName("41"));
   System.out.println(getName("42"));
   System.out.println(getName("43"));
   System.out.println(new Date().getTime()-start.getTime());
  } catch (SQLException e) {
   e.printStackTrace();
  }
 }
 /**
  * Liest mittels SQL den Namen eines Adressatzes.
  * 
  * Hier  wird jedes mal ein SQLStatement mit String Konkatinierung zusammengebaut 
     * und ausgeführt. 
     *
     * Dies soll man in der Praxis nicht machen.
  * @param adressNummer
  * @return Name
  */
 private static String getName(String adressNummer) throws SQLException {
  ResultSet rs=stmt.executeQuery("select * from adressen where adnr="+adressNummer);
  if(rs.next()) return rs.getString("adnam1");
  return "";
 }

}

Es gibt zwei Gründe warum man seinen SQL Code nicht jedes mal als String zusammenbauen und dann mit execute() ausführen soll.

Bei wiederholter Ausführung des selben SQL Code ist die Performance viel besser, wenn der SQL Code nicht jedesmal geparst, geprüft und in einen Zugriffsplan übersetzt werden muß. Der Vorgang ein SQL Kommando in einen Zugriffsplan zu übersetzen ist je nach verwendeter Datenbank und Komplexität der Anweisung extrem aufwendig. Der oben angeführte Codeblock in dem die Zeit gemessen wird, läuft wenn man Preparedstatements verwendet, ca. 3 mal schneller als mit dem oben angeführten Code.

Was aber fast noch wichtiger ist, wenn ich SQL Befehle mit String Konkatinierung zusammenbaue, dann wird der Code anfällig für SQL Injections. Das heißt, es kann in der Variable mit ein paar Tricks beliebiger SQL Code eingeschleust werden. Dies ist eine der häufigsten Einfallstore für Hacks auf Webseiten. Also selbst wenn ein Statement nur einmal verwendet wird, sollte man auf jeden Fall um SQL Injections zu vermeiden Preparedstatements verwenden.

Noch dazu ist die Verwendung von Preparedstatements eigentlich extrem einfach. Man ändert den Typ von stmt auf PreparedStatement, und erstellt das Objekt mit prepareStatement(). Für jeden Wert der in der Anweisung variabel sein soll fügt man ein Fragezeichen ein. Der sqlCode aus obigen Beispiel heißt dann "select * from adressen where adnr=?". Bei jeder Verwendung der SQL Anweisung muss dann mit der passenden set Methode der Platzhalter im Statement mit dem richtigen Wert befüllt werden. Am leichtesten zu verstehen ist es wenn man sich folgendes Beispiel anschaut.
public class PreparedStatementJDBC {
 private static PreparedStatement stmt;
 /**
  * @param args
  */
 public static void main(String[] args) {
  //Verbinde mit Datenbank und initalisiere Anweisung
  AS400JDBCDataSource dataSource = new AS400JDBCDataSource("localhost", "user", "password");
  try {
   Connection con=dataSource.getConnection();
   stmt=con.prepareStatement("select * from adressen where adnr=?");
  //Gib verschiedene Namen aus der Adressdatei mit verschiedenen Adressnummern aus.
   Date start=new Date();
   System.out.println(getName("40"));
   System.out.println(getName("41"));
   System.out.println(getName("42"));
   System.out.println(getName("43"));
   System.out.println(new Date().getTime()-start.getTime());
  } catch (SQLException e) {
   e.printStackTrace();
  }
 }
 /**
  * Liest mittels SQL den Namen eines Adressatzes.
  * @param adressNummer
  * @return Name
  */
 private static String getName(String adressNummer) throws SQLException {
  //Parameter im vorbereiteten Statement setzen.
  stmt.setInt(1, new Integer(adressNummer).intValue());
  ResultSet rs=stmt.executeQuery();
  if(rs.next()) return rs.getString("adnam1");
  return "";
 }

}

Die Preparedstatements können natürlich nicht nur für "Selects", sondern genauso gut auch für "Inserts" und "Updates" verwendet werden. 

Wer JDBC Zugriffe in seinen xPages, Plugins oder Agenten verwendet, sollte prüfen ob er auch wirklich überall schon PreparedStatements verwendet. Der Performancegewinn und vor allem die zusätzliche Sicherheit vor SQL Injections sollte den geringen Änderungsaufwand auf jeden Fall lohnen.

No comments:

Post a Comment

ad