Donnerstag, 21. März 2013

REST-Web-Services mit BASIC-Auth und HTTPS in GlassFish

Nehmen wir mal einen einfachen REST-Web-Service wie den folgenden, der alle Bestellungen des Benutzers liefern soll:

@Path("/bestellungen")
public class BestellungWebService {

  @Context
  private SecurityContext securityContext;

  @Produces({ MediaType.APPLICATION_JSON,
              MediaType.APPLICATION_XML })
  @GET
  public Response getAlleBestellungen() {

    final String username =
      securityContext.getUserPrincipal().getName();

    try {
      List<Bestellung> bestellungen = ...;

      return Response.ok( bestellungen ).build();
    }
    catch (Exception e) {
      return Response.serverError()
                     .entity( e.getMessage() ).build();
    }
  }
}

Mit beispielsweise Wget könnte dieser Service dann wie folgt aufgerufen werden, wenn der Applikationspfad auf "rest" gemappt ist:

wget http://localhost:8080/meinewebapp/rest/bestellungen

Fast immer muss ein solcher Service abgesichert werden, damit nur bestimmte Anwender Zugriff darauf haben. Man könnte natürlich den UserPrincipal programmatisch auswerten, aber sofern die berechtigten Benutzer im Application-Server als User eingetragen sind, ist eine deklarative Absicherung einfacher - und erfordert keine Änderungen am Quelltext.

Am Beispiel von GlassFish 3.1.2 schauen wir uns das an. Wir erlauben dem GlassFish-Admin-User, den Web-Service aufzurufen. Dazu sichern wir am Ende des Deployment-Deskriptors web.xml alle Ressourcen unterhalb /rest so ab, dass nur die "admin"-Rolle darauf zugreifen darf. Mit CONFIDENTIAL erzwingen wir zudem, dass die Übertragung der Daten per https (SSL bzw. TLS) stattfindet:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
    http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
    version="3.0">

  ...

  <security-constraint>
    <web-resource-collection>
      <url-pattern>/rest/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
      <role-name>admin</role-name>
    </auth-constraint>
    <user-data-constraint>
      <transport-guarantee>CONFIDENTIAL</transport-guarantee>
    </user-data-constraint>
  </security-constraint>

  <login-config>
    <auth-method>BASIC</auth-method>
    <realm-name>admin-realm</realm-name>
  </login-config>

  <security-role>
    <role-name>admin</role-name>
  </security-role>

</web-app>

Die BASIC-Authentication, bei der Benutzername und Passwort im Klartext (!) übertragen werden, ist hier dennoch in Ordnung, weil wir gleichzeitig mit https die verschlüsselte Datenübertragung aktivieren.

Die berechtigten Anwender müssen im "admin-realm" eingetragen sein, das ist der Realm mit allen GlassFish-Admins (der in der Standardinstallation den User "admin" enthält). Als Beispiel für einen Security-Realm ist das hier ausreichend; in der Praxis wird man aus Sicherheitsgründen eher einen separaten Realm im GlassFish konfigurieren und in web.xml referenzieren.

Nun müssen wir dem Application-Server noch – produktspezifisch – mitteilen, welche Benutzernamen aus dem Realm auf welche Rollen gemappt werden. Dies erfolgt in der Datei glassfish-web.xml (früher sun-web.xml):

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE glassfish-web-app PUBLIC
  "-//GlassFish.org//DTD GlassFish Application Server 3.1 Servlet 3.0//EN"
  "http://glassfish.org/dtds/glassfish-web-app_3_0-1.dtd">
<glassfish-web-app>

  <context-root>meinewebapp</context-root>
     
  <security-role-mapping>
    <role-name>admin</role-name>
    <principal-name>admin</principal-name>
  </security-role-mapping>

</glassfish-web-app>

Fertig. Der Aufruf des Web-Services erfolgt über Wget nun mit

wget https://localhost:8081/meinewebapp/rest/bestellungen
       --user ... --password ... --no-check-certificate

Ein Aufruf über unverschlüsseltes http ist nicht mehr möglich. Während wir hier der Einfachheit halber auf die Prüfung des https-Zertifikats verzichten, sollte man in der Praxis besser ein geeignetes eigenes Zertifikat im Server installieren und auf Client-Seite verwenden.

Donnerstag, 14. März 2013

JAXB und HashMaps, Teil 2

In Teil 1 zu JAXB und HashMaps ging es vor einiger Zeit darum, mit möglichst wenig Aufwand im Java-Code eine XML-Struktur zu verarbeiten, deren Element-Namen teilweise fest vorgegeben waren. Teil 2 zeigt nun, wie man eine XML-Struktur mit beliebigen Element-Namen aus einer (Hash)Map innerhalb einer JAXB-Entität ausgibt (bzw. dorthin einliest). Konkret soll folgendes XML-Dokument erzeugt werden:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<meineEntitaet>
  <paarliste>
    <paar>
      <schluessel>Antwort</schluessel>
      <wert>42</wert>
    </paar>
  </paarliste>
</meineEntitaet>
Dazu benötigen wir als Erstes für einen einzelnen Eintrag der Map (also für jedes Schlüssel-Wert-Paar) folgende Klasse:

import javax.xml.bind.annotation.XmlElement;

class MapElement {

  @XmlElement(name = "schluessel")
  String key;

  @XmlElement(name = "wert")
  Integer value;

  MapElement() {
  }

  MapElement(String key, Integer value) {
    this.key = key;
    this.value = value;
  }
}

Die Liste der Map-Einträge wird in einer eigenen Klasse verpackt, damit wir den Namen des XML-Elements festlegen können, das die einzelnen Schlüssel-Wert-Paare umschließt:

class MapElements {

  @XmlElement(name = "paar")
  List<MapElement> mapElements;

  MapElements() {
  }

  MapElements(List<MapElement> mapElements) {
    this.mapElements = mapElements;
  }
}

In der JAXB-Entität taucht nun aber nicht diese MapElements-Klasse auf, sondern die ursprünglich gewünschte (Hash)Map:

import java.util.*;
import javax.xml.bind.annotation.*;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class MeineEntitaet {

  @XmlElement(name = "paarliste")
  @XmlJavaTypeAdapter(MapAdapter.class)
  private Map<String, Integer> map = new HashMap<>();
  public Map<String, Integer> getMap() {
    return map;
  }

  public void setMap(Map<String, Integer> map) {
    this.map = map;
  }
}

Für die Umwandlung zwischen der MapElements-Klasse und der HashMap benötigen wir einen XmlAdapter:

import java.util.*;
import javax.xml.bind.annotation.adapters.XmlAdapter;

public class MapAdapter extends XmlAdapter<MapElements, Map<String, Integer>> {

  @Override
  public MapElements marshal(Map<String, Integer> map) {
    List<MapElement> list = new ArrayList<>();
    for (Map.Entry<String, Integer> entry : map.entrySet()) {
      list.add(new MapElement(entry.getKey(), entry.getValue()));
    }
    return new MapElements(list);
  }

  @Override
  public Map<String, Integer> unmarshal(MapElements elements) {
    Map<String, Integer> map = new HashMap<>();
    for (MapElement mapElement : elements.mapElements) {
      map.put(mapElement.key, mapElement.value);
    }
    return map;
  }
}

Damit können wir schließlich das XML-Dokument erzeugen:
MeineEntitaet huelle = new MeineEntitaet();
huelle.getMap().put("Antwort", 42);
JAXB.marshal(huelle, System.out);

Freitag, 8. März 2013

Warum ist StandardCharsets final?

In vielen Anwendungen und Bibliotheken findet man eine Klasse, die Konstanten für häufig benutzte Zeichensätze enthält:

import java.nio.charset.Charset;
 
public class ApplicationCharsets {
  public static final Charset UTF_8
    = Charset.forName( "UTF-8" );
  public static final Charset ISO_8859_1
    = Charset.forName( "ISO-8859-1" );
  public static final Charset WINDOWS_1252
    = Charset.forName( "WINDOWS-1252" );


Seit Java 7 gibt es die Klasse java.nio.charset.StandardCharsets, die Konstanten für die Zeichensätze enthält, die garantiert in jeder Java-Implementierung zur Verfügung stehen – US-ASCII, ISO-8859-1 (ISO Latin 1) sowie diverse Unicode-Kodierungen.

Damit man im Anwendungs-Code nicht verschiedene Klassen für die Standard- und die Anwendungs-Zeichensatzkodierungen referenzieren muss, könnte man die Anwendungs-Konstanten wie folgt formulieren ...

import java.nio.charset.*;
 
public class ApplicationCharsets
    extends StandardCharsets /* geht nicht :-( */ {
 
  public static final Charset WINDOWS_1252
    = Charset.forName( "WINDOWS-1252" );


... wenn die Klasse StandardCharsets nicht final wäre. So bleibt derzeit leider nur folgender Ausweg:

public class ApplicationCharsets {
  public static final Charset UTF_8
    = StandardCharsets.UTF_8;
  public static final Charset ISO_8859_1
    = StandardCharsets.ISO_8859_1;
  public static final Charset WINDOWS_1252
    = Charset.forName( "WINDOWS-1252" );


Warum also darf von StandardCharsets keine Unterklasse abgeleitet werden? Einfach nur aus historischen Gründen, weil ähnliche Klassen bisher auch final waren? Kann man das in Java 8 ändern?