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.