Planet Metax - Chaos & Logik

Dateien ausliefern mit Java Servlets

02. Mai 2010 Dateien ausliefern mit Java Servlets

Wenn man dynamische Web-Applikationen programmieren möchte, stehen einem eine große Auswahl an Technologien zur Verfügung. Sehr verbreitet ist vor allem im Business-Bereich die Java-Servlet-Technologie.

Üblicherweise ist an vordester Front ein gewöhnlicher Webserver (z.B. Apache HTTPD) vorgeschaltet, der eingehende HTTP-Requests zunächst verarbeitet.

Verweist der Request auf eine dynamische Resource (also eine Seite der Web-Applikation), so wird der Request an den Servlet-Container (z.B. Apache Tomcat) weitergeleitet und dort von dem zugehörigen Servlet verarbeitet.

Webserver Konfiguration


Dadurch wird gewährleistet, dass statische Resourcen direkt von dem Webserver verarbeitet werden, was einige Vorteile mit sich bringt:

In einigen Fällen kann es allerdings sinnvoll sein, wenn das Ausliefern von statischen Resourcen (Bilder, Stylesheets, Downloads, ...) nicht vom Webserver durchgeführt wird, sondern von einem Servlet übernommen wird.

Das kann einige Gründe haben:

Diesen Fall, dass ein Java-Servlet das Ausliefern einer beliebigen Datei übernehmen soll, möchte ich nun hier genauer betrachten.

Die grundlegende Servlet-Architektur

Ich gehe in diesem Artikel davon aus, dass die Klasse DownloadHandler zum Ausliefern der Dateien Teil einer größeren Web-Applikation ist.

Wird eine auslieferbare Resource angefordert, so sollte die Web-Applikation diesen Request bereits an eine Instanz der Klasse DownloadHandler weiterleiten, indem die Methode handleFileDownload aufgerufen wird.

Um ihrer Aufgabe gerecht werden zu können, benötigt diese Funktion folgende Parameter:

Bei einem Aufruf des Download Handlers sind folgende Aufgaben aufzuführen

  1. Bestimmen der auszuliefernden Datei aus dem Request-Parameter. Ist diese Datei nicht vorhanden, muss ein Fehlercode erzeugt werden
  2. Bestimmen, ob der Benutzer diese Datei erhalten darf (= Zugriffsschutz). Darf der Benutzer diese Datei nicht herunterladen, muss ein Fehlercode erzeugt werden
  3. Setzen der korrekten HTTP-Header, damit der Client die geschickte Datei richtig interpretieren kann (z.B. Dateigröße, Dateityp, ...)
  4. Öffnen der Datei und Schicken des Dateiinhalts durch den Ausgabe-Stream des Response-Objekts

Es müssen also von der Web-Applikation noch folgende Informationen bereitgestellt werden:

Die grundlegende Struktur des Download Handlers könnte also wie folgt aussehen:

package de.planet_metax.dlservlet; import java.io.File; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Diese Klasse soll das Übermitteln einer Datei an den Client ermöglichen (= Download-Funktion). * @author Christian Simon */ public class DownloadHandler { /** * Bestimme, welche Datei von dem request angefordert wurde. * Falls eine ungültige Datei angefordert wurde, * @param request die Anfrage * @return File-Objekt oder null, falls der Request ungültig war */ private File getFileFromRequest(HttpServletRequest request) { // TODO: Resource in Datei auflösen return null; } /** * Prüfe, ob die Datei an den Client geschickt werden darf (Zugriffsschutz) * @param file die Datei * @return true, falls der Zugriff legitim ist */ private boolean isDownloadAllowed(File file) { // TODO: Zugriffsschutz return true; } /** * Ermittele den Content-Typ ("MIME-Type") der Datei * @param file die Datei * @return Content Typ */ private String getContentType(File file) { // TODO: Medientyp bestimmen return "text/plain"; } /** * Verarbeite den Download-Request. * @param request der Request * @param response der Response * @throws ServletException falls ein Fehler auftritt */ public void handleFileDownload(HttpServletRequest request, HttpServletResponse response) throws ServletException { // TODO: Datei übermitteln } }

Das Auswählen der Datei und der Zugriffsschutz

Ein effektiver Zugriffsschutz sollte eine Verbindung zum Account-System der Web-Applikation haben.
So lassen sich verschiedene Zugriffsrechte für verschiedene Dateien implementieren.
Da dies in diesem Beispiel nicht möglich ist, möchte ich einen sehr einfachen Zugriffsschutz implementieren:
Es ist der Zugriff auf alle Dateien erlaubt, die nicht mit "geheim." beginnen:

private boolean isDownloadAllowed(File file) { if (file.getName().startsWith("geheim.")) { return false; } return true; }


Ähnlich einfach möchte ich das Auswählen der Datei regeln:
Über den HTTP-Parameter downloadfile kann ein Eintrag aus einer White-List ausgewählt werden.
Alle ungültigen Werte führen zu einem Fehler:

private static final Hashtable<String, File> fileMap; static { fileMap = new Hashtable<String, File>(); fileMap.put("file1.txt", new File("downloads/textfiles/text1.txt")); fileMap.put("file2.jpg", new File("downloads/images/image2.jpg")); fileMap.put("file3.png", new File("downloads/images/image3.png")); fileMap.put("file4.dat", new File("downloads/data/data4.dat")); fileMap.put("geheim.zip", new File("downloads/zip/geheim.zip")); } private File getFileFromRequest(HttpServletRequest request) { String downloadfile = request.getParameter("downloadfile"); if (downloadfile == null) { return null; } if (fileMap.containsKey(downloadfile)) { return fileMap.get(downloadfile); } return null; }

Bestimmen des Content-Typs

Der Content-Type (auch: MIME-Type, Medientyp) ist eine wichtige Information für Browser, da diese festlegt, wie die empfangenen Daten zu interpretieren sind.
Der Content-Type "text/html" erzwingt beispielsweise eine Darstellung in der HTML-Engine des Browsers, wogegen der Content-Type "application/pdf" das Dokument im Acrobat Reader (oder einem anderen PDF-Viewer) anzeigt.

In den meisten Fällen lässt sich der Content-Type an der Endung der Datei festmachen. Es ist zwar z.B. auch möglich, eine Textdatei mit der Endung ".jpg" zu versehen, aber ich gehe in diesem Beispiel einmal davon aus, dass der Inhalt aller Dateien zu ihrer Endung passt.

Daher lässt sich der Medientyp einfach anhand eines Endungsvergleiches mit dem Dateinamen bestimmen:

private String getContentType(File file) { String filename = file.getName(); if (filename.endsWith(".txt")) return "text/plain"; if (filename.endsWith(".jpg")) return "image/jpeg"; if (filename.endsWith(".png")) return "image/png"; if (filename.endsWith(".pdf")) return "application/pdf"; if (filename.endsWith(".zip")) return "application/zip"; // ... // Dieser Content-Typ bedeutet "Binärdaten" und erzwingt in jedem // Browser einen Download. return "application/octet-stream"; }

Das Ausliefern der Datei

Mit diesen Methoden ist es nun nicht mehr schwer, die Datei auszuliefern.

Dazu verwende ich den anfangs erläuterten Ablauf unter Einbeziehung der soeben definierten Methoden.

In diesem Beispiel wird (wie oben beschrieben) die herunterzuladende Datei über den HTTP-Parameter downloadfile festgelegt.
Ein korrekter Aufruf des Download-Servlets könnte also wie folgt aussehen:

http://server.tld/DownloadServlet/Download.do?downloadfile=file2.jpg

Hier sieht man nun das Endergebnis der einfachen Download-Klasse.
Den Java-Quelltext dieses Beispiels kann man auch herunterladen.

package de.planet_metax.dlservlet; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.Hashtable; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * Diese Klasse soll das Übermitteln einer Datei an den Client ermöglichen (= Download-Funktion). * @author Christian Simon */ public class DownloadHandler { /** * White-List für die erlaubten Dateien. * Jeder Dateiname (links) wird einer Datei (rechts) zugeordnet. */ private static final Hashtable<String, File> fileMap; static { fileMap = new Hashtable<String, File>(); fileMap.put("file1.txt", new File("downloads/textfiles/text1.txt")); fileMap.put("file2.jpg", new File("downloads/images/image2.jpg")); fileMap.put("file3.png", new File("downloads/images/image3.png")); fileMap.put("file4.dat", new File("downloads/data/data4.dat")); fileMap.put("geheim.zip", new File("downloads/zip/geheim.zip")); } /** * Bestimme, welche Datei von dem request angefordert wurde. * Falls eine ungültige Datei angefordert wurde, * @param request die Anfrage * @return File-Objekt oder null, falls der Request ungültig war */ private File getFileFromRequest(HttpServletRequest request) { String downloadfile = request.getParameter("downloadfile"); if (downloadfile == null) { return null; } if (fileMap.containsKey(downloadfile)) { return fileMap.get(downloadfile); } return null; } /** * Prüfe, ob die Datei an den Client geschickt werden darf (Zugriffsschutz) * @param file die Datei * @return true, falls der Zugriff legitim ist */ private boolean isDownloadAllowed(File file) { if (file.getName().startsWith("geheim.")) { return false; } return true; } /** * Ermittele den Content-Typ ("MIME-Type") der Datei * @param file die Datei * @return Content Typ */ private String getContentType(File file) { String filename = file.getName(); if (filename.endsWith(".txt")) return "text/plain"; if (filename.endsWith(".jpg")) return "image/jpeg"; if (filename.endsWith(".png")) return "image/png"; if (filename.endsWith(".pdf")) return "application/pdf"; if (filename.endsWith(".zip")) return "application/zip"; // ... // Dieser Content-Typ bedeutet "Binärdaten" und erzwingt in jedem // Browser einen Download. return "application/octet-stream"; } /** * Verarbeite den Download-Request. * Die Exceptions werden ggf. von den Servlet-Klassen ausgelöst. * @param request der Request * @param response der Response * @throws ServletException falls ein Fehler auftritt * @throws IOException falls ein Fehler auftritt */ public void handleFileDownload(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // Bestimme die Download-Datei File downloadFile = getFileFromRequest(request); if (downloadFile == null || !downloadFile.exists()) { // Die Datei konnte nicht zugeordnet werden, oder existiert nicht. // Schicke Statuscode: 404 - NOT FOUND response.sendError(HttpServletResponse.SC_NOT_FOUND); return; } if (!isDownloadAllowed(downloadFile)) { // Der Zugriff wurde verweigert. // Schicke Statuscode: 403 - FORBIDDEN response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } String contentType = getContentType(downloadFile); // Schicke HTTP Header response.setContentLength((int) downloadFile.length()); response.setContentType(contentType); response.setDateHeader("Last-Modified", downloadFile.lastModified()); // Öffne Streams InputStream in = new FileInputStream(downloadFile); OutputStream out = response.getOutputStream(); int BUFFER_SIZE = 16384; // Kopiere den Inhalt des Inputstreams in den Outputstream try { byte[] buffer = new byte[BUFFER_SIZE]; int bytesRead = 0; while ((bytesRead = in.read(buffer)) > 0) { out.write(buffer, 0, bytesRead); } out.flush(); } finally { // Schließe Inputstream try { in.close(); } catch (IOException e) { } } } }
Schlagworte: HTTP, HowTo, Java, Programmierung, Servlet
Veröffentlicht am 02.05.2010 20:21 in Java | Keine Kommentare »

Kommentare

Es sind noch keine Kommentare vorhanden

Kommentar schreiben:

Mit (*) gekennzeichnete Felder sind optional.
BBCode im Kommentarfeld erlaubt.