REST-Webservices basieren wie SOAP-Webservices auf dem HTTP-Protokoll, über das sämtliche Nachrichten zwischen Client und Server ausgetauscht werden. Damit ist ein REST-Webservice auch grundlegend auf synchrone Kommunikation ausgelegt. Die Nachrichten werden üblicherweise ebenso wie bei SOAP in einem standardisierten, textbasierten und damit technologieunabhängigen Dateiformat wie XML oder JSON ausgetauscht. Demzufolge sind auch REST-Webservices unabhängig von einer konkreten Laufzeitumgebung oder Programmiersprache, so dass Client und Server in ganz unterschiedlichen Sprachen implementiert werden können und als Komponenten klar voneinander abgegrenzt sind. Dies führt wie gewünscht zu einer losen Kopplung der Komponenten in der Systemarchitektur.

Prinzipien von REST

Der Begriff REST steht für Representational State Transfer und geht auf die Dissertation von Roy Fielding im Jahr 2000 zurück. Die wesentliche Idee besteht darin, alle Funktionalität über eindeutig adressierbare Ressourcen bereitzustellen – und nicht wie bei RPC-Ansätzen (SOAP, RMI) über Methoden. Der prinzipielle Ablauf der Kommunikation zwischen Client und Server ist in der folgenden Abbildung skizziert.

Zur Adressierung einer Ressource dient eine URI (Unique Resource Identifier), die allgemein nach dem Muster http://<host>:<port>/<context-path>/<resource-path> aufgebaut ist.

REST ist kein Protokoll sondern ein Architekturstil, der auf Konventionen basiert. Es gibt keine Schnittstellenbeschreibungssprache (Interface Definition Language, kurz IDL) wie z.B. WSDL bei SOAP-Webservices. Eine wichtige Konvention ist die Einhaltung eines Uniform Interface, demzufolge die HTTP-Methoden je Ressource wie folgt abzubilden sind:

Es ergeben sich folgende typische Operationen auf einer Ressource, die bei einer Anfrage an der URI entsprechend der gewählten HTTP-Methode ausgeführt werden. Bezüglich der leeren Zellen der Tabelle werden i.d.R. keine Operationen von einem REST-Webservice unterstützt, da z.B. das Löschen einer Menge von Ressourcen oder deren vollständiges Ersetzen durch eine andere Menge nicht benötigt werden.

RessourceGETPOSTPUTPATCHDELETE
Menge, z.B. /accounts Ausgabe einer Liste aller Elemente der Menge. Es werden i.d.R. nicht alle Attribute der Elemente zurückgegeben, aber mind. deren URIs. Die Liste ist ggf. mittels weiterer Parameter des HTTP-Request sortiert, gefiltert und/oder in paginierte Abschnitte unterteilt. Erzeugen eines neuen Elements in der Menge, dessen URI im Header oder Body der HTTP-Response zurückgegeben wird.
Element, z.B. /accounts/1 Lesen aller Attribute des Elements. Ersetzen des Elements an der angegeben URI. Erzeugen des Elements, falls es nicht existiert. Aktualisieren von ausgewählten Attributen des Elements. Löschen des Elements.

Die Ressourcen können vom Server in unterschiedlichen Repräsentationen an einen Client ausgeliefert werden. Die geläufigsten Repräsentationen sind die beiden strukturierten Datenformate XML und JSON, aber eine Ressource könnte auch in HTML, in einem spezifischen Bildformat (JPG, PNG, ...), o.ä. repräsentiert werden. Ein Client kann über den HTTP-Header Accept angeben, welches Format er in der HTTP-Response erhalten möchte. Falls möglich, liefert der Server genau dieses Format aus, andernfalls ein möglichst ähnliches. Es folgt eine Auswahl häufig verwendeter Formate und ihrer MIME-Typen nach RFC 6838.

FormatMIME-Type
JSONapplication/json
XMLapplication/xml
HTMLtext/html
Unstrukturierter Texttext/plain
Spezifische Bildformateimage/jpeg, image/png, image/gif, ...
Spezifische Audioformateaudio/mpeg, audio/wav, audio/webm, ...
Spezifische Videoformatevideo/mp4, video/ogg, video/webm, ...
Unspezifisches Binärformatapplication/octet-stream

Bei einer GET-Anfrage an die Ressource /accounts/1 könnte der Body der HTTP-Response, der die Attribute des Kontos mit der Id 1 darstellt, je nach angeforderter Repräsentation exemplarisch wie folgt aussehen:

{
	"id": 1,
	"owner": "Hendricks",
	"entries": [
		{
			"subject": "Credit 1",
			"value": 10,
			"date": 1612015200
		},
		{
			"subject": "Credit 2",
			"value": 20,
			"date": 1612101600
		}
	]
}

Bei POST-, PUT- und PATCH-Anfragen wird der Client seinerseits Daten in einem bestimmten Format im Body des HTTP-Request an den Server übermitteln wollen. Dazu kann er über den HTTP-Header Content-Type den Server informieren, in welchem Format er diese Daten sendet.

Weitere Konventionen eines REST-Webservice sind:

JAX-RS (Java API for RESTful Web Services)

Um REST-Webservices in Java zu implementierten, ist zunächst die in JSR 370 spezifizierte API namens JAX-RS maßgeblich. Die Referenzimplementierung für JAX-RS ist das Framework Jersey, das wir für das folgende Anwendungsbeispiel verwenden werden. Alternative Implementierungen für JAX-RS sind RESTEasy, Apache CXF und Restlet. Auch mit dem Spring Framework lässt sich einfach ein REST-Webservice in Java implementieren, der aber nicht unbedingt konform zu JAX-RS sein muss, wenn die Implementierung den Empfehlungen von Spring MVC folgt.

Wie die meisten aktuellen Java-Frameworks arbeitet auch JAX-RS intensiv mit Annotationen. Diese Annotationen aus dem Package javax.ws.rs werden anhand der folgenden Code-Beispiele sukzessive erklärt. Der Code bezieht sich auf ein Anwendungsbeispiel, in dem erneut – wie in den vorherigen Kapiteln – Bankkonten über eine API verwaltet werden sollen. Um den REST-Webservice zu realisieren, wird passend zur Datenmodellklasse Account eine zugehörige Ressourcenklasse AccountResource entworfen, die über die oben vorgestellten HTTP-Methoden eine Menge von Konten verwaltet. Diese Klasse verwaltet die Konten in einer Map, deren Schlüssel die Konto-Ids sind (Zeile 12). Diese Map ist noch flüchtig und wird erst im nächsten Kapitel in einer relationalen Datenbank gespeichert (s. Kapitel Objekt-Relationales Mapping). Da die Klasse AccountResource insgesamt relativ umfangreich ist, werden zunächst nur die Signaturen ihrer Methoden dargestellt und deren Implementierungen anschließend schrittweise ergänzt und erläutert.

import com.sun.net.httpserver.HttpServer;
// ...

public class WebServiceStarter {

    public static void main(String[] args) throws Exception {
        ResourceConfig rc = new ResourceConfig().packages("resources");
        HttpServer server = JdkHttpServerFactory.createHttpServer(URI.create("http://localhost:8080/"), rc);
        System.out.println("Hit enter to stop HTTP server.");
        System.in.read();
        server.stop(0);
    }
}
package resources;

import javax.ws.rs.core.Response;
import javax.ws.rs.Path;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
// ...

@Path("/accounts")
public class AccountResource {

    final static Map<Integer, Account> accounts = new ConcurrentHashMap<>();

    @GET
    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})	
    public Collection<Account> getAccounts() { /* ... */ }

    @POST
    @Consumes({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public Response postAccount(Account account) { /* ... */ }	
	
    @GET @Path("{id}")
    public Response getAccount(@PathParam("id") int id) { /* ... */ }

    @PUT @Path("{id}")
    public Response putAccount(@PathParam("id") int id, Account account) { /* ... */ }

    @PATCH @Path("{id}")
    public Response patchAccount(@PathParam("id") int id, Account patchedAccount) { /* ... */ }

    @DELETE @Path("{id}")
    public Response deleteAccount(@PathParam("id") int id) { /* ... */ }
}
package model;

import javax.xml.bind.annotation.XmlRootElement;
// ...

@XmlRootElement // required for XML binding
public class Account {

    public static AtomicInteger nextId = new AtomicInteger(1);

    public int id;
    public String owner;
    public List<AccountEntry> entries;

    public int balance() {
        if (entries == null) return 0;
        return entries.stream().mapToInt(entry -> entry.value).sum();
    }
}
package model;

public class AccountEntry {

    public String subject;
    public int value;
    public Date date;

    public AccountEntry() {
        this.date = new Date();
    }
}
Erläuterungen zu den Annotationen aus dem Package javax.ws.rs in dem obigen Code-Beispiel:

Die gezeigten Code-Beispiele zu REST-Webservices finden sich im Verzeichnis /remote/rest des Modul-Repository.

HTTP-Response

Ein REST-Webservice sollte einem Client grundsätzlich mit HTTP-Status-Codes antworten, die dem vereinbarten Standard entsprechen. Die folgende Tabelle zeigt eine Auswahl von gebräuchlichen HTTP-Status-Codes, wobei der Bereich 2xx stets für erfolgreiche HTTP-Requests steht, 3xx für Weiterleitungen, 4xx für Fehler des aufrufenden Client und 5xx für Fehler im Server.

HTTP-Status-CodeBeschreibung
200OK, generischer Code für eine erfolgreiche Anfrage
201Created, neue Ressource erfolgreich angelegt
204No Content, Anfrage erfolgreich, aber es wird kein Inhalt zurückgegeben
301Moved Permanently, Ressource ist dauerhaft unter anderer URI erreichbar
304Not Modified, unverändert, d.h. Cache muss nicht aktualisiert werden
400Bad Request, generischer Code für einen Fehler des Client in der Parametrisierung der Anfrage
403Forbidden, Zugriff auf die Ressource ist verboten
404Not Found, für die URI konnte keine Ressource gefunden werden
500Internal Server Error, generischer Code für einen nicht weiter behandelten Fehler im Server
503Service Unavailable, Service ist vorübergehend nicht zu erreichen

Die beiden Methoden im folgenden Code-Beispiel geben eine Liste aller Konten bzw. ein einzelnes Konto zurück. Der Rückgabetyp der Methode getAccounts (Pfad /accounts) ist Collection<Account> (Zeile 2). Für diese Methode wird die HTTP-Response implizit durch das JAX-RS-Framework zusammengestellt. Die Liste der Konten wird ohne Zutun des Entwicklers in eine JSON- oder XML-Repräsentation transformiert und in ein Objekt vom Typ javax.ws.rs.core.Response eingebettet, dessen Status-Code 200 ist. Bei der zweiten Methode getAccount (Pfad z.B. /accounts/1) wird das Response-Objekt explizit zusammengestellt (Zeilen 10 und 12). Die Klasse Response bietet dazu statische Methoden um den Status-Code (Methode status), den Body (Methode entity) und spezifische Header-Attribute (Methode header) zu setzen.

@GET
public Collection<Account> getAccounts() {
	return accounts.values();  // return code is 200
}

@GET @Path("{id}")
public Response getAccount(@PathParam("id") int id) {
	Account account = accounts.get(id);
	if (account == null) {
		return Response.status(Response.Status.NOT_FOUND).build(); // return code is 404
	}
	return Response.status(Response.Status.OK).entity(account).build(); // return code is 200
}

Im Body eines HTTP-Request und einer HTTP-Response wird i.d.R. JSON oder XML von einem REST-Webservice empfangen bzw. von diesem versendet. Das JAX-RS-Framework übernimmt implizit die Transformation von Java-Objekten in JSON/XML und andersherum. Dazu wird jeweils eine entsprechende Bibliothek benötigt. Diese Bibliotheken sind üblicherweise Jackson für das JSON-Binding und JAXB für das XML-Binding.

Semantische Konvention von POST, PUT und PATCH

Die folgenden drei Methoden zeigen Implementierungen für die HTTP-Methoden POST, PUT und PATCH, die eine Ressource anlegen bzw. verändern. Dabei sind die im Folgenden erläuterten Unterschiede relevant.

Das Anlegen oder Verändern einer Ressource kann grundsätzlich fehlschlagen, falls die Validierung einzelner Attribute fehlschlägt. Exemplarisch werden einige typische Bedingungen für Validierungsschritte aufgezählt.

Zur Validierung bieten sich die Bean Validation-Annotationen an. Im obigen Code-Beispiel wird exemplarisch bei jeder Methode die selbsterklärende Annotation @NotNull verwendet.

Der Unterschied zwischen PUT und PATCH soll noch anhand eines Beispiels verdeutlicht werden. Zur Vorbereitung wird über folgende POST-Anfrage ein neues Konto angelegt.

Anschließend wird der Eigentümer des Kontos zunächst mittels einer PATCH-Anfrage verändert.

Zum Vergleich wird der Eigentümer des Kontos danach mittels einer PUT-Anfrage verändert. Wie in der jeweiligen HTTP-Response zu erkennen ist, verändert PATCH die Kontoeinträge nicht, während PUT dafür sorgt, dass keine Kontoeinträge mehr existieren, da im Body des HTTP-Request auch keine übergeben worden sind.

Repräsentation einer Ressource in einem Binärformat

Das folgende Code-Beispiel zeigt zwei Ressourcen-Methoden, die beide auf GET-Anfragen bezüglich des Pfads /users/<id> reagieren, aber jeweils eine Ausgabe in unterschiedlichem Format erzeugen. Während die erste Methode getUser JSON oder XML als Ausgabe produziert, erzeugt die zweite Methode getUserImage eine Binärdatei, die das User-Profilbild enthält. Das JAX-RS-Framework wertet den HTTP-Header Accept aus, um zu entscheiden, welche der beiden Methoden ausgeführt werden soll. Dabei soll die Rückgabe in einem Format erfolgen, das möglichst der Anforderung des Client entspricht. Es kann allerdings sein, dass der Client z.B. Accept: image/jpeg sendet, aber eine Antwort mit Content-Type: image/png erhält, da das User-Profilbild serverseitig im PNG-Format vorliegt.

@Path("/users")
public class UserResource {

    @GET @Path("{id}")
    @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML})
    public Response getUser(@PathParam("id") int id) {
        User user = getUserById(id);
        return Response.ok(user).build();
    }

    @GET @Path("{id}")
    @Produces({MediaType.APPLICATION_OCTET_STREAM, "image/png", "image/jpeg"})
    public Response getUserImage(@PathParam("id") int id) throws IOException {
        User user = getUserById(id);
        InputStream is = new ByteArrayInputStream(user.image);
        String type = URLConnection.guessContentTypeFromStream(is); // get MIME type
        String ext = type.substring(type.lastIndexOf("/") + 1); // get file extension
        return Response.ok(is).type(type)
          .header("Content-Disposition", "inline; filename=\"" + user.name + "." + ext + "\"").build();
    }

    private User getUserById(int id) {
        Optional<User> user = UserService.queryById(id); // query user object from database
        if (user.isPresent()) { return user.get(); }
        throw new NotFoundException("User not found.");
    }
}

Behandlung von Exceptions in JAX-RS

Wenn innerhalb einer Ressourcen-Methode eine unbehandelte RuntimeException (z.B. eine NullPointerException) auftritt, wird diese durch das JAX-RS-Framework gefangen und "verschluckt", so dass keine Ausgabe auf der Konsole oder im Log erscheint. JAX-RS sieht vor, dass der Entwickler gezielt sogenannte ExceptionMapper registriert, die auf spezifische Exceptions reagieren und diese auswerten, um eine aussagekräftige HTTP-Response an den aufrufenden Client zu senden. Das folgende Code-Beispiel zeigt exemplarisch drei verschiedene Implementierungen des ExceptionMapper-Interface.
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.ExceptionMapper;
import javax.validation.ConstraintViolationException;
// ...

@Provider
public class ValidationExceptionMapper implements ExceptionMapper<ConstraintViolationException> {

    @Override
    public Response toResponse(ConstraintViolationException e) {
        ConstraintViolation violation = e.getConstraintViolations().stream().findFirst().get();
        String message = violation.getPropertyPath() + " " + violation.getMessage();
        return Response.status(Response.Status.BAD_REQUEST).entity(message).type("text/plain").build();
    }
}
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.ExceptionMapper;
import com.fasterxml.jackson.core.JsonProcessingException;
// ...

@Provider
public class JsonProcessingExceptionMapper implements ExceptionMapper<JsonProcessingException> {

    @Override
    public Response toResponse(JsonProcessingException e) {
        return Response.status(Response.Status.BAD_REQUEST).entity(e.getMessage()).type("text/plain").build();
    }
}
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.ExceptionMapper;
// ...

@Provider
public class GenericExceptionMapper implements ExceptionMapper<RuntimeException> {

    @Override
    public Response toResponse(RuntimeException e) {
        if (e instanceof ClientErrorException) {
            int status = ((ClientErrorException) e).getResponse().getStatus();
            return Response.status(status).entity(e.getMessage()).type("text/plain").build();
        }
        else {
            e.printStackTrace();
            return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
        }
    }
}

Testen eines REST-Webservice

Für automatisierte Unit-Tests bietet Jersey die Klasse JerseyTest (enthalten in der Bibliothek Jersey Test Framework Core), die durch eine eigene Testklasse erweitert werden kann (Zeile 7). Das Jersey-Test-Framework startet automatisch einen HTTP-Server unter Port 9998, auf dem die zu testende Ressource bereitgestellt wird (Zeile 11) und anschließend die Testfälle ausgeführt werden. Mittels der Methode target der JerseyTest-Klasse können Anfragen an den REST-Webservice aus den Testfällen heraus erzeugt werden. Ansonsten gelten die üblichen Konvention für JUnit-Tests. Dazu gehört u.a., dass der gemeinsame initiale Zustand für alle Testfälle durch eine Methode mit der Annotation @BeforeEach hergestellt wird (Zeilen 14-20). Das folgende Code-Beispiel zeigt zwei exemplarische Testfälle für PUT (Zeilen 22-30) und DELETE (Zeilen 32-39).

import org.glassfish.jersey.test.JerseyTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;
// ...

@TestInstance(Lifecycle.PER_CLASS)
public class AccountResourceTest extends JerseyTest {

    @Override
    protected Application configure() {
        return new ResourceConfig(AccountResource.class);
    }

    @BeforeEach
    public void prepareTest() {
        target().path("accounts").request().delete(); // delete all accounts
        String a = "{\"owner\": \"A\", \"entries\": [{\"value\": 20}, {\"value\": -10}]}";
        target("accounts/1").request().put(Entity.json(a)); // put a test account with id = 1
		// ...
    }	
	
    @Test
    public void testPutAccount() {
        String a = "{\"owner\": \"Changed\"}";
        Response res = target("accounts/1").request().put(Entity.json(a));
        assertEquals(200, res.getStatus());
        Account account = res.readEntity(Account.class);
        assertEquals("Changed", account.owner);
        assertEquals(0, account.balance());		
    }

    @Test
    public void testDeleteAccount() {
        Response res = target("accounts/1").request().delete();
        assertEquals(204, res.getStatus());

        res = target("accounts/1").request().get(Response.class);
        assertEquals(404, res.getStatus());
    }

	// ...
}

Zur Entwicklungszeit ist es wichtig, die REST-API einfach stichprobenartig testen zu können. Die erstellten Testfälle sollen zum Austausch mit anderen Entwicklern und zur Versionierung gespeichert werden können, und sie sollen wiederholt und automatisch ausführbar sein. Dazu gibt es mehrere, relativ ähnliche Werkzeuge, die entweder als Desktop-Anwendung oder als Browser-Plugin installiert werden können – z.B. Postman, Insomnia und Advanced REST Client. Alternativ ist es auch möglich die REST-API direkt aus der Entwicklungsumgebung (IDE) zu testen. Die beiden folgenden Videos zeigen kurz, wie die entwickelte REST-API zur Kontenverwaltung exemplarisch mittels Postman und IntelliJ IDEA getestet werden können.

JWT (JSON Web Token)

REST-Webservices sind per Konvention zustandslos, d.h. auf dem Server werden keine Sessions für die aktuell verbundenen Clients verwalten. Demzufolge muss jeder Client selbst seine Session verwaltet und mit jedem HTTP-Request ein Token o.ä. an den Server senden, über das er authentifiziert und für den Zugriff autorisiert werden kann. Eine standardisierte Möglichkeit zur Erzeugung derartiger Tokens findet sich in RFC 7519, in dem sogenannte JSON Web Token (JWT) eingeführt werden.

"JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code (MAC) and/or encrypted." (Internet Engineering Task Force)

Der Kommunikationsablauf zwischen Client und Server bei Verwendung von JWT wird in der folgenden Abbildung dargestellt.

  1. Der Anwender authentifiziert sich beim Server, z.B. in dem er seinen Usernamen und sein Passwort in ein Login-Formular einträgt und dieses absendet.
  2. Der Server generiert ein individuelles Token für den Client. Zur Signatur des Token wird ein Secret verwendet, das ausschließlich dem Erzeuger des Token bekannt ist. Das Token besteht aus 3 Teilen: dem Header, dem eigentlichen Inhalt genannt Payload und der Signatur.
  3. Der Server sendet das Token an den Client, der es sicher vor dem Zugriff von Dritten verwahrt. Das Token mit Header {"alg": "HS256"}, Payload {"sub": 123, "name": "John Doe", "iat": 1612083600} und Secret my-secret wäre z.B. eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOjEyMywibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNjEyMDgzNjAwfQ.FQVhKVWAzvW-Pje0FlB30ITulrRwcI9Mm8HpOinMYYc.
  4. Der Client sendet das Token mit jedem weiteren HTTP-Request im HTTP-Header Authorization an den Server.
  5. Der Server verifiziert das Token, bevor er den Zugriff auf die angeforderte Ressource zulässt. Falls es sich um kein gültiges Token handelt, sollte der Status-Code Unauthorized (401) zurückgegeben werden. Falls es sich zwar um ein gültiges Token handelt, aber der authentifizierte Anwender nicht über die notwendigen Rechte verfügt, um auf die Ressource zuzugreifen, sollte der Status-Code Forbidden (403) zurückgegeben werden. Das Token kann durch ein Expiration Time-Attribut in seiner Gültigkeit eingeschränkt werden. Aufgrund der prinzipiellen Zustandslosigkeit der REST-API kann der Server ein einmal ausgestelltes Token aber nicht sofort für ungültig erklären (z.B. Logout erzwingen). Als Workaround könnte ein Token einer serverseitigen Blacklist hinzugefügt werden, was aber der Idee der Zustandslosigkeit widerspricht.
  6. Bei autorisiertem Zugriff auf die Ressource wird die eigentliche Anfrage mit einer entsprechenden HTTP-Response bedient.

Es existieren diverse JWT-Implementierungen für alle gängigen Programmiersprachen. Eine gute Übersicht über die zur Verfügung stehenden JWT-Bibliotheken sowie eine interaktive Einführung findet sich auf jwt.io. Das folgende Code-Beispiel verwendet die JWT-Bibliothek Nimbus JOSE+JWT für Java.

@Path("/login")
public class AuthenticationResource {

    @POST
    @Produces(MediaType.TEXT_PLAIN)
    public String login(User userClaim) {
        Optional<User> user = UserService.queryByCredentials(userClaim.name, userClaim.passwordHash); // lookup user in database
        if (user.isPresent()) {
            try {
                // map claim object to JSON
                JWTClaim claim = new JWTClaim(String.valueOf(user.get().id), user.get().name);
                String claimJson = new ObjectMapper().writeValueAsString(claim);

                // create JWS object with claim as payload
                JWSObject jwsObject = new JWSObject(new JWSHeader(JWSAlgorithm.HS256), new Payload(claimJson));

                // use a secret key for HS256 and sign the JWS object using HMAC
                byte[] secret = AuthenticationFilter.SECRET.getBytes();
                jwsObject.sign(new MACSigner(secret));

                // provide token to the client
                return Response.ok( jwsObject.serialize() ).build();
                
            } catch (JsonProcessingException | JOSEException e) {
                throw new RuntimeException(e.getMessage());
            }
        }
        return Response.status(401).entity("Login failed").build();
    }
}
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.ext.Provider;
// ...
	
@Provider
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {

    public static final String SECRET = "my-very-private-secret:32-symbols=256-bit";
    static final String TOKEN_TYPE = "Bearer";

    @NameBinding @Retention(RetentionPolicy.RUNTIME)
    public @interface Secured { }

    @Override
    public void filter(ContainerRequestContext request) {
        // validate HTTP authorization header
        String authHeader = request.getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
        if (authHeader == null || !authHeader.startsWith(TOKEN_TYPE + " ")) {
            request.abortWith(Response.status(401).entity("JWT not found in HTTP header").build());
        }
        // extract the token from the HTTP authorization header
        String jwt = authHeader.substring(TOKEN_TYPE.length()).trim();

        // validate JWT signature
        try {
            JWSObject jwsObject = JWSObject.parse(jwt);
            JWSVerifier verifier = new MACVerifier(SECRET);
            if (jwsObject.verify(verifier)) {
                String payload = jwsObject.getPayload().toString();
                JWTClaim claim = new ObjectMapper().readValue(payload, JWTClaim.class);
                System.out.println("User verified: " + claim.name);
                return;
            }
            else {
                request.abortWith(Response.status(401).entity("Invalid JWT").build());
            }
        } catch (ParseException | JOSEException | IOException e) {
            request.abortWith(Response.status(400).entity(e.getMessage()).build());
        }
    }
}
@Path("/users")
public class UserResource {

	@Secured
    @GET
    public Collection<User> getUsers() {
        return UserService.queryAllUsers();
    }
	
	// ...
}
public class JWTClaim {

    // registered JWT claim names, see https://tools.ietf.org/html/rfc7519#section-4.1
    String sub; // subject
    String name;
    Date iat; // issued at
    Date exp; // expiration time

    public JWTClaim(String sub, String name) {
        this.sub = sub;
        this.name = name;
        Calendar cal = Calendar.getInstance();
        this.iat = cal.getTime();
        cal.add(Calendar.HOUR, 1); // tokens are valid for 1 hour
        this.exp = cal.getTime();
    }
}