Java-Authentifizierung mit JSON-Web-Tokens (JWTs) überladen

Supercharge Java-Authentifizierung mit JSON-Web-Token (JWTs)

Sind Sie bereit, eine sichere Authentifizierung in Ihrer Java-Anwendung zu erstellen, oder haben Sie Probleme damit? Sie sind sich nicht sicher, welche Vorteile die Verwendung von Token (und speziell von JSON-Web-Token) bietet oder wie diese bereitgestellt werden sollten? Ich freue mich darauf, diese und weitere Fragen in diesem Tutorial für Sie zu beantworten!

Bevor wir uns mit JSON-Web-Tokens (JWTs) undJJWT library (erstellt von Stormpaths CTO Les Hazlewood und verwaltet von acommunity of contributors) befassen, wollen wir einige Grundlagen behandeln.

1. Authentifizierung vs. Token-Authentifizierung

Die Gruppe von Protokollen, mit denen eine Anwendung die Benutzeridentität bestätigt, ist die Authentifizierung. Anwendungen haben traditionell ihre Identität durch Sitzungscookies bewahrt. Dieses Paradigma beruht auf der serverseitigen Speicherung von Sitzungs-IDs, wodurch Entwickler gezwungen werden, einen Sitzungsspeicher zu erstellen, der entweder eindeutig und serverspezifisch ist oder als vollständig separate Sitzungsspeicherschicht implementiert wird.

Die Token-Authentifizierung wurde entwickelt, um Probleme zu lösen, die serverseitige Sitzungs-IDs nicht und nicht konnten. Wie bei der herkömmlichen Authentifizierung weisen Benutzer überprüfbare Anmeldeinformationen auf, erhalten jedoch anstelle einer Sitzungs-ID eine Reihe von Token. Die anfänglichen Anmeldeinformationen können das Standard-Benutzername / Passwort-Paar, API-Schlüssel oder sogar Token von einem anderen Dienst sein. (Stormpaths API-Schlüsselauthentifizierungsfunktion ist ein Beispiel dafür.)

1.1. Warum Token?

Die Verwendung von Tokens anstelle von Sitzungs-IDs kann ganz einfach die Serverauslastung senken, die Berechtigungsverwaltung rationalisieren und bessere Tools für die Unterstützung einer verteilten oder cloudbasierten Infrastruktur bereitstellen. Im Fall von JWT wird dies hauptsächlich durch die Staatenlosigkeit dieser Tokentypen erreicht (mehr dazu weiter unten).

Tokens bieten eine Vielzahl von Anwendungen, darunter: Schutzschemata für Cross Site Request Forgery (CSRF),OAuth 2.0 Interaktionen, Sitzungs-IDs und (in Cookies) als Authentifizierungsdarstellungen. In den meisten Fällen legen Standards kein bestimmtes Format für Token fest. Hier ist ein Beispiel für ein typischesSpring Security CSRF tokenin einem HTML-Formular:

Wenn Sie versuchen, dieses Formular ohne das richtige CSRF-Token zu veröffentlichen, erhalten Sie eine Fehlerantwort. Dies ist das Dienstprogramm von Token. Das obige Beispiel ist ein "dummes" Token. Dies bedeutet, dass dem Token selbst keine inhärente Bedeutung zukommt. Hier machen JWTs auch einen großen Unterschied.

Weitere Lektüre:

Verwenden von JWT mit Spring Security OAuth

Eine Anleitung zur Verwendung von JSON-Webtoken mit symmetrischer und asymmetrischer Signatur in Spring Security OAuth.

Read more

Spring REST API + OAuth2 + Angular

Erfahren Sie, wie Sie OAuth2 für eine Spring REST-API einrichten und von einem Angular-Client aus verarbeiten.

Read more

OAuth2 für eine Spring-REST-API - Behandeln Sie das Aktualisierungstoken in AngularJS

Wir haben gelernt, wie Sie das Aktualisierungstoken in einer AngularJS-Client-App speichern, wie Sie ein abgelaufenes Zugriffstoken aktualisieren und wie Sie den Zuul-Proxy nutzen.

Read more

2. Was ist in einem JWT?

JWTs (ausgesprochene „Jots“) sind URL-sichere, verschlüsselte, kryptografisch signierte (manchmal verschlüsselte) Zeichenfolgen, die in einer Vielzahl von Anwendungen als Token verwendet werden können. Hier ist ein Beispiel für eine JWT, die als CSRF-Token verwendet wird:

In diesem Fall sehen Sie, dass das Token viel länger ist als in unserem vorherigen Beispiel. Wie wir bereits gesehen haben, erhalten Sie eine Fehlerantwort, wenn das Formular ohne das Token gesendet wird.

Warum also JWT?

Das oben genannte Token ist kryptografisch signiert und kann daher überprüft werden, um nachzuweisen, dass es nicht manipuliert wurde. Außerdem sind JWTs mit einer Vielzahl zusätzlicher Informationen codiert.

Schauen wir uns die Anatomie eines Zeugen Jehovas an, um besser zu verstehen, wie wir all diese Güte aus ihm herausdrücken. Möglicherweise haben Sie bemerkt, dass es drei verschiedene Abschnitte gibt, die durch Punkte (.) getrennt sind:

Header

eyJhbGciOiJIUzI1NiJ9

Nutzlast

eyJqdGkiOiJlNjc4ZjIzMzQ3ZTM0MTBkYjdlNjg3Njc4MjNiMmQ3MCIsImlhdC

Unterschrift

rgx_o8VQGuDa2AqCHSgVOD5G68Ld_YYM7N7THmvLIKc

Jeder Abschnitt istbase64 URL-codiert. Dies stellt sicher, dass es sicher in einer URL verwendet werden kann (dazu später mehr). Schauen wir uns jeden Abschnitt einzeln genauer an.

2.1. Der Header

Wenn Sie base64 zum Dekodieren des Headers verwenden, erhalten Sie die folgende JSON-Zeichenfolge:

{"alg":"HS256"}

Dies zeigt, dass die JWT mitHMAC unter Verwendung vonSHA-256 signiert wurde.

2.2. Die Nutzlast

Wenn Sie die Nutzdaten dekodieren, erhalten Sie die folgende JSON-Zeichenfolge (aus Gründen der Übersichtlichkeit formatiert):

{
  "jti": "e678f23347e3410db7e68767823b2d70",
  "iat": 1466633317,
  "nbf": 1466633317,
  "exp": 1466636917
}

Wie Sie sehen, gibt es in der Nutzlast eine Reihe von Schlüsseln mit Werten. Diese Schlüssel werden als "Ansprüche" bezeichnet, und inJWT specification sind sieben davon als "registrierte" Ansprüche angegeben. Sie sind:

iss

Aussteller

sub

Gegenstand

aud

Publikum

exp

Ablauf

nbf

Nicht bevor

iat

Ausgestellt bei

jti

JWT ID

Wenn Sie ein JWT erstellen, können Sie beliebige benutzerdefinierte Ansprüche einreichen. Die obige Liste stellt lediglich die Ansprüche dar, die sowohl für den verwendeten Schlüssel als auch für den erwarteten Typ reserviert sind. Unser CSRF hat eine JWT-ID, eine "Issued At" -Zeit, eine "Not Before" -Zeit und eine Ablaufzeit. Die Verfallszeit ist genau eine Minute nach der zur Zeit ausgegebenen.

2.3. Die Unterschrift

Abschließend wird der Signaturbereich erstellt, indem Header und Nutzdaten zusammengenommen werden (mit dem. dazwischen) und durchlaufen den angegebenen Algorithmus (in diesem Fall HMAC mit SHA-256) zusammen mit einem bekannten Geheimnis. Beachten Sie, dass das Geheimnisalways ein Byte-Array ist und eine Länge haben sollte, die für den verwendeten Algorithmus sinnvoll ist. Im Folgenden verwende ich eine zufällige Base64-codierte Zeichenfolge (zur besseren Lesbarkeit), die in ein Byte-Array konvertiert wurde.

Im Pseudocode sieht es so aus:

computeHMACSHA256(
    header + "." + payload,
    base64DecodeToByteArray("4pE8z3PBoHjnV1AhvGk+e8h2p+ShZpOnpr8cwHmMh1w=")
)

Solange Sie das Geheimnis kennen, können Sie die Signatur selbst generieren und Ihr Ergebnis mit dem Signaturabschnitt des JWT vergleichen, um sicherzustellen, dass es nicht manipuliert wurde. Technisch gesehen wird eine kryptografisch signierte JWT alsJWS bezeichnet. JWTs können auch verschlüsselt werden und werden dann alsJWE bezeichnet. (In der Praxis wird der Begriff JWT verwendet, um JEWs und JESs zu beschreiben.)

Dies bringt uns zurück zu den Vorteilen der Verwendung eines JWT als CSRF-Token. Wir können die Unterschrift überprüfen und anhand der in der JWT verschlüsselten Informationen deren Gültigkeit bestätigen. Die Zeichenfolgendarstellung des JWT muss also nicht nur mit der auf der Serverseite gespeicherten übereinstimmen, sondern wir können auch sicherstellen, dass sie nicht einfach abgelaufen ist, indem wir den Anspruch vonexpüberprüfen. Dies erspart dem Server, den zusätzlichen Status beizubehalten.

Nun, wir haben hier viel Boden unter den Füßen. Lassen Sie uns in einen Code eintauchen!

3. Richten Sie das JJWT-Tutorial ein

JJWT (https://github.com/jwtk/jjwt) ist eine Java-Bibliothek, die die Erstellung und Überprüfung von JSON-Web-Token durchgängig ermöglicht. Für immer kostenlos und Open Source (Apache License, Version 2.0) wurde es mit einer auf Builder ausgerichteten Oberfläche entwickelt, die den größten Teil seiner Komplexität verbirgt.

Die Hauptoperationen bei der Verwendung von JJWT umfassen das Erstellen und Parsen von JJWTs. Als nächstes werden wir uns diese Vorgänge ansehen, dann einige erweiterte Funktionen des JJWT kennenlernen und schließlich JWTs als CSRF-Token in einer Spring Security, Spring Boot-Anwendung in Aktion sehen.

Der in den folgenden Abschnitten gezeigte Code isthere. Hinweis: Das Projekt verwendet Spring Boot von Anfang an, da es einfach mit der API zu interagieren ist, die es verfügbar macht.

Führen Sie zum Erstellen des Projekts Folgendes aus:

git clone https://github.com/eugenp/tutorials.git
cd tutorials/jjwt
mvn clean install

Eines der großartigen Dinge an Spring Boot ist, wie einfach es ist, eine Anwendung zu starten. Führen Sie einfach die folgenden Schritte aus, um die JJWT Fun-Anwendung auszuführen:

java -jar target/*.jar

In dieser Beispielanwendung sind zehn Endpunkte verfügbar (ich verwende httpie, um mit der Anwendung zu interagieren). Es kannhere gefunden werden.)

http localhost:8080
Available commands (assumes httpie - https://github.com/jkbrzt/httpie):

  http http://localhost:8080/
    This usage message

  http http://localhost:8080/static-builder
    build JWT from hardcoded claims

  http POST http://localhost:8080/dynamic-builder-general claim-1=value-1 ... [claim-n=value-n]
    build JWT from passed in claims (using general claims map)

  http POST http://localhost:8080/dynamic-builder-specific claim-1=value-1 ... [claim-n=value-n]
    build JWT from passed in claims (using specific claims methods)

  http POST http://localhost:8080/dynamic-builder-compress claim-1=value-1 ... [claim-n=value-n]
    build DEFLATE compressed JWT from passed in claims

  http http://localhost:8080/parser?jwt=
    Parse passed in JWT

  http http://localhost:8080/parser-enforce?jwt=
    Parse passed in JWT enforcing the 'iss' registered claim and the 'hasMotorcycle' custom claim

  http http://localhost:8080/get-secrets
    Show the signing keys currently in use.

  http http://localhost:8080/refresh-secrets
    Generate new signing keys and show them.

  http POST http://localhost:8080/set-secrets
    HS256=base64-encoded-value HS384=base64-encoded-value HS512=base64-encoded-value
    Explicitly set secrets to use in the application.

In den folgenden Abschnitten werden wir jeden dieser Endpunkte und den in den Handlern enthaltenen JJWT-Code untersuchen.

4. JWTs mit JJWT erstellen

Aufgrund derfluent interfacevon JJWT ist die Erstellung des JWT im Grunde ein dreistufiger Prozess:

  1. Die Definition der internen Ansprüche des Tokens, z. B. Aussteller, Betreff, Ablaufdatum und ID.

  2. Die kryptografische Signatur des JWT (macht es zu einem JWS).

  3. Die Komprimierung des JWT zu einer URL-sicheren Zeichenfolge gemäß den Regeln vonJWT Compact Serialization.

Die endgültige JWT ist eine dreiteilige Base64-codierte Zeichenfolge, die mit dem angegebenen Signaturalgorithmus und dem bereitgestellten Schlüssel signiert ist. Nach diesem Zeitpunkt kann das Token mit einer anderen Partei geteilt werden.

Hier ist ein Beispiel für das JJWT in Aktion:

String jws = Jwts.builder()
  .setIssuer("Stormpath")
  .setSubject("msilverman")
  .claim("name", "Micah Silverman")
  .claim("scope", "admins")
  // Fri Jun 24 2016 15:33:42 GMT-0400 (EDT)
  .setIssuedAt(Date.from(Instant.ofEpochSecond(1466796822L)))
  // Sat Jun 24 2116 15:33:42 GMT-0400 (EDT)
  .setExpiration(Date.from(Instant.ofEpochSecond(4622470422L)))
  .signWith(
    SignatureAlgorithm.HS256,
    TextCodec.BASE64.decode("Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=")
  )
  .compact();

Dies ist dem Code in derStaticJWTController.fixedBuilder-Methode des Codeprojekts sehr ähnlich.

An dieser Stelle lohnt es sich, über einige Anti-Muster im Zusammenhang mit JWTs und Unterschriften zu sprechen. Wenn Sie schon einmal JWT-Beispiele gesehen haben, sind Sie wahrscheinlich auf eines dieser signierenden Anti-Pattern-Szenarien gestoßen:

  1. .signWith(
        SignatureAlgorithm.HS256,
       "secret".getBytes("UTF-8")
    )
  2. .signWith(
        SignatureAlgorithm.HS256,
        "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=".getBytes("UTF-8")
    )
  3. .signWith(
        SignatureAlgorithm.HS512,
        TextCodec.BASE64.decode("Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=")
    )

Jeder Signaturalgorithmus vom TypHSbenötigt ein Byte-Array. Für Menschen ist es praktisch zu lesen, um einen String zu nehmen und ihn in ein Byte-Array zu konvertieren.

Anti-Muster 1 oben zeigt dies. Dies ist problematisch, da das Geheimnis dadurch geschwächt wird, dass es so kurz ist und es kein Byte-Array in seiner nativen Form ist. Damit es lesbar bleibt, können wir das Byte-Array mit base64 codieren.

Das obige Anti-Muster 2 verwendet jedoch die base64-codierte Zeichenfolge und konvertiert sie direkt in ein Byte-Array. Was getan werden sollte, ist die Base64-Zeichenfolge wieder in das ursprüngliche Byte-Array zu dekodieren.

Nummer 3 oben zeigt dies. Also, warum ist dies auch ein Anti-Muster? Dies ist in diesem Fall ein subtiler Grund. Beachten Sie, dass der Signaturalgorithmus HS512 ist. Das Byte-Array ist nicht die maximale Länge, dieHS512 unterstützen kann, was es zu einem schwächeren Geheimnis macht als das, was für diesen Algorithmus möglich ist.

Der Beispielcode enthält eine Klasse namensSecretService, die sicherstellt, dass Geheimnisse der richtigen Stärke für den angegebenen Algorithmus verwendet werden. Beim Start der Anwendung wird für jeden HS-Algorithmus ein neuer Satz von Geheimnissen erstellt. Es gibt Endpunkte, um die Geheimnisse zu aktualisieren und die Geheimnisse explizit festzulegen.

Wenn das Projekt wie oben beschrieben ausgeführt wird, führen Sie Folgendes aus, damit die folgenden JWT-Beispiele mit den Antworten aus Ihrem Projekt übereinstimmen.

http POST localhost:8080/set-secrets \
  HS256="Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=" \
  HS384="VW96zL+tYlrJLNCQ0j6QPTp+d1q75n/Wa8LVvpWyG8pPZOP6AA5X7XOIlI90sDwx" \
  HS512="cd+Pr1js+w2qfT2BoCD+tPcYp9LbjpmhSMEJqUob1mcxZ7+Wmik4AYdjX+DlDjmE4yporzQ9tm7v3z/j+QbdYg=="

Jetzt können Sie den Endpunkt/static-builder erreichen:

http http://localhost:8080/static-builder

Dies erzeugt eine JWT, die so aussieht:

eyJhbGciOiJIUzI1NiJ9.
eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.
kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ

Nun schlagen Sie:

http http://localhost:8080/parser?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ

Die Antwort enthält alle Angaben, die wir bei der Erstellung des JWT gemacht haben.

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
...
{
    "jws": {
        "body": {
            "exp": 4622470422,
            "iat": 1466796822,
            "iss": "Stormpath",
            "name": "Micah Silverman",
            "scope": "admins",
            "sub": "msilverman"
        },
        "header": {
            "alg": "HS256"
        },
        "signature": "kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ"
    },
    "status": "SUCCESS"
}

Dies ist die Analyseoperation, auf die wir im nächsten Abschnitt eingehen werden.

Lassen Sie uns nun einen Endpunkt erreichen, der Ansprüche als Parameter verwendet und ein benutzerdefiniertes JWT für uns erstellt.

http -v POST localhost:8080/dynamic-builder-general iss=Stormpath sub=msilverman hasMotorcycle:=true

Note: Es gibt einen subtilen Unterschied zwischen dem Anspruch vonhasMotorcycleund den anderen Ansprüchen. httpie geht davon aus, dass JSON-Parameter standardmäßig Zeichenfolgen sind. Um unformatiertes JSON mithilfe von httpie zu senden, verwenden Sie das Formular:=anstelle von=. Ohne das würde es“hasMotorcycle”: “true” einreichen, was wir nicht wollen.

Hier ist die Ausgabe:

POST /dynamic-builder-general HTTP/1.1
Accept: application/json
...
{
    "hasMotorcycle": true,
    "iss": "Stormpath",
    "sub": "msilverman"
}

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
...
{
    "jwt":
      "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwiaGFzTW90b3JjeWNsZSI6dHJ1ZX0.OnyDs-zoL3-rw1GaSl_KzZzHK9GoiNocu-YwZ_nQNZU",
    "status": "SUCCESS"
}

Werfen wir einen Blick auf den Code, der diesen Endpunkt unterstützt:

@RequestMapping(value = "/dynamic-builder-general", method = POST)
public JwtResponse dynamicBuilderGeneric(@RequestBody Map claims)
  throws UnsupportedEncodingException {
    String jws =  Jwts.builder()
        .setClaims(claims)
        .signWith(
            SignatureAlgorithm.HS256,
            secretService.getHS256SecretBytes()
        )
        .compact();
    return new JwtResponse(jws);
}

Zeile 2 stellt sicher, dass der eingehende JSON-Code automatisch in eine Java-Map konvertiert wird, was für JJWT sehr praktisch ist, da die Methode in Zeile 5 diese Map einfach aufnimmt und alle Ansprüche auf einmal festlegt.

So knapp dieser Code ist, wir benötigen etwas Spezifischeres, um sicherzustellen, dass die übergebenen Ansprüche gültig sind. Die Verwendung der.setClaims(Map<String, Object> claims)-Methode ist praktisch, wenn Sie bereits wissen, dass die in der Karte dargestellten Ansprüche gültig sind. Hier kommt die Typensicherheit von Java in die JJWT-Bibliothek.

Für jeden der in der JWT-Spezifikation definierten registrierten Ansprüche gibt es im JJWT eine entsprechende Java-Methode, die den spezifikationsrichtigen Typ annimmt.

Lassen Sie uns in unserem Beispiel einen anderen Endpunkt treffen und sehen, was passiert:

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub:=5 hasMotorcycle:=true

Beachten Sie, dass wir für den Unteranspruch eine Ganzzahl 5 übergeben haben. Hier ist die Ausgabe:

POST /dynamic-builder-specific HTTP/1.1
Accept: application/json
...
{
    "hasMotorcycle": true,
    "iss": "Stormpath",
    "sub": 5
}

HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
    "exceptionType": "java.lang.ClassCastException",
    "message": "java.lang.Integer cannot be cast to java.lang.String",
    "status": "ERROR"
}

Jetzt erhalten wir eine Fehlerantwort, da der Code den Typ der registrierten Ansprüche erzwingt. In diesem Fall musssub eine Zeichenfolge sein. Hier ist der Code, der diesen Endpunkt unterstützt:

@RequestMapping(value = "/dynamic-builder-specific", method = POST)
public JwtResponse dynamicBuilderSpecific(@RequestBody Map claims)
  throws UnsupportedEncodingException {
    JwtBuilder builder = Jwts.builder();

    claims.forEach((key, value) -> {
        switch (key) {
            case "iss":
                builder.setIssuer((String) value);
                break;
            case "sub":
                builder.setSubject((String) value);
                break;
            case "aud":
                builder.setAudience((String) value);
                break;
            case "exp":
                builder.setExpiration(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "nbf":
                builder.setNotBefore(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "iat":
                builder.setIssuedAt(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "jti":
                builder.setId((String) value);
                break;
            default:
                builder.claim(key, value);
        }
    });

    builder.signWith(SignatureAlgorithm.HS256, secretService.getHS256SecretBytes());

    return new JwtResponse(builder.compact());
}

Wie zuvor akzeptiert die MethodeMap<String, Object> von Ansprüchen als Parameter. Dieses Mal rufen wir jedoch die spezifische Methode für jeden der registrierten Ansprüche auf, die den Typ erzwingt.

Eine Verfeinerung hiervon besteht darin, die Fehlermeldung spezifischer zu gestalten. Derzeit wissen wir nur, dass einer unserer Ansprüche nicht der richtige Typ ist. Wir wissen nicht, welche Behauptung fehlerhaft war oder was sie sein sollte. Hier ist eine Methode, die uns eine spezifischere Fehlermeldung gibt. Es geht auch um einen Fehler im aktuellen Code.

private void ensureType(String registeredClaim, Object value, Class expectedType) {
    boolean isCorrectType =
        expectedType.isInstance(value) ||
        expectedType == Long.class && value instanceof Integer;

    if (!isCorrectType) {
        String msg = "Expected type: " + expectedType.getCanonicalName() +
            " for registered claim: '" + registeredClaim + "', but got value: " +
            value + " of type: " + value.getClass().getCanonicalName();
        throw new JwtException(msg);
    }
}

Zeile 3 überprüft, ob der übergebene Wert vom erwarteten Typ ist. Wenn nicht, wird einJwtException mit dem spezifischen Fehler ausgelöst. Schauen wir uns dies in Aktion an, indem wir denselben Anruf tätigen, den wir zuvor getätigt haben:

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub:=5 hasMotorcycle:=true
POST /dynamic-builder-specific HTTP/1.1
Accept: application/json
...
User-Agent: HTTPie/0.9.3

{
    "hasMotorcycle": true,
    "iss": "Stormpath",
    "sub": 5
}

HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
    "exceptionType": "io.jsonwebtoken.JwtException",
    "message":
      "Expected type: java.lang.String for registered claim: 'sub', but got value: 5 of type: java.lang.Integer",
    "status": "ERROR"
}

Jetzt haben wir eine sehr spezifische Fehlermeldung, die uns mitteilt, dass die Behauptung vonsubdie fehlerhafte ist.

Kehren wir zu diesem Fehler in unserem Code zurück. Das Problem hat nichts mit der JJWT-Bibliothek zu tun. Das Problem ist, dass der in Spring Boot integrierte JSON-zu-Java-Objekt-Mapper zu intelligent für unser eigenes Wohl ist.

Wenn es eine Methode gibt, die ein Java-Objekt akzeptiert, konvertiert der JSON-Mapper automatisch eine übergebene Zahl, die kleiner oder gleich 2.147.483.647 ist, in JavaInteger. Ebenso wird eine übergebene Zahl, die größer als 2.147.483.647 ist, automatisch in JavaLong konvertiert. Für die Ansprücheiat,nbf undexp eines JWT soll unser sureType-Test bestehen, ob das zugeordnete Objekt eine Ganzzahl oder ein Long ist. Aus diesem Grund haben wir die zusätzliche Klausel, um festzustellen, ob der übergebene Wert der richtige Typ ist:

 boolean isCorrectType =
     expectedType.isInstance(value) ||
     expectedType == Long.class && value instanceof Integer;

Wenn wir ein Long erwarten, der Wert jedoch eine Instanz von Integer ist, sagen wir immer noch, dass es sich um den richtigen Typ handelt. Mit dem Verständnis, was mit dieser Validierung passiert, können wir sie jetzt in unseredynamicBuilderSpecific-Methode integrieren:

@RequestMapping(value = "/dynamic-builder-specific", method = POST)
public JwtResponse dynamicBuilderSpecific(@RequestBody Map claims)
  throws UnsupportedEncodingException {
    JwtBuilder builder = Jwts.builder();

    claims.forEach((key, value) -> {
        switch (key) {
            case "iss":
                ensureType(key, value, String.class);
                builder.setIssuer((String) value);
                break;
            case "sub":
                ensureType(key, value, String.class);
                builder.setSubject((String) value);
                break;
            case "aud":
                ensureType(key, value, String.class);
                builder.setAudience((String) value);
                break;
            case "exp":
                ensureType(key, value, Long.class);
                builder.setExpiration(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "nbf":
                ensureType(key, value, Long.class);
                builder.setNotBefore(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "iat":
                ensureType(key, value, Long.class);
                builder.setIssuedAt(Date.from(
                    Instant.ofEpochSecond(Long.parseLong(value.toString()))
                ));
                break;
            case "jti":
                ensureType(key, value, String.class);
                builder.setId((String) value);
                break;
            default:
                builder.claim(key, value);
        }
    });

    builder.signWith(SignatureAlgorithm.HS256, secretService.getHS256SecretBytes());

    return new JwtResponse(builder.compact());
}

Note: In allen Beispielcodes in diesem Abschnitt werden JWTs mit dem HMAC unter Verwendung des SHA-256-Algorithmus signiert. Dies soll die Beispiele einfach halten. Die JJWT-Bibliothek unterstützt 12 verschiedene Signaturalgorithmen, die Sie in Ihrem eigenen Code verwenden können.

5. Analysieren von JWTs mit JJWT

Wie wir bereits gesehen haben, hat unser Codebeispiel einen Endpunkt zum Parsen einer JWT. Diesen Endpunkt erreichen:

http http://localhost:8080/parser?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ

erzeugt diese Antwort:

HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
...
{
    "claims": {
        "body": {
            "exp": 4622470422,
            "iat": 1466796822,
            "iss": "Stormpath",
            "name": "Micah Silverman",
            "scope": "admins",
            "sub": "msilverman"
        },
        "header": {
            "alg": "HS256"
        },
        "signature": "kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ"
    },
    "status": "SUCCESS"
}

Dieparser-Methode derStaticJWTController-Klasse sieht folgendermaßen aus:

@RequestMapping(value = "/parser", method = GET)
public JwtResponse parser(@RequestParam String jwt) throws UnsupportedEncodingException {
    Jws jws = Jwts.parser()
        .setSigningKeyResolver(secretService.getSigningKeyResolver())
        .parseClaimsJws(jwt);

    return new JwtResponse(jws);
}

Zeile 4 gibt an, dass die eingehende Zeichenfolge eine signierte JWT (eine JWS) sein soll. Und wir verwenden dasselbe Geheimnis, mit dem die JWT beim Parsen signiert wurde. Zeile 5 analysiert die Ansprüche aus dem JWT. Intern überprüft es die Signatur und löst eine Ausnahme aus, wenn die Signatur ungültig ist.

Beachten Sie, dass in diesem Fall einSigningKeyResolver und kein Schlüssel selbst übergeben wird. Dies ist einer der mächtigsten Aspekte von JJWT. Der Header von JWT gibt den zum Signieren verwendeten Algorithmus an. Wir müssen das JWT jedoch überprüfen, bevor wir es als vertrauenswürdig einstufen. Es scheint ein Haken zu sein 22. Schauen wir uns dieSecretService.getSigningKeyResolver-Methode an:

private SigningKeyResolver signingKeyResolver = new SigningKeyResolverAdapter() {
    @Override
    public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
        return TextCodec.BASE64.decode(secrets.get(header.getAlgorithm()));
    }
};

Mit dem Zugriff aufJwsHeader kann ich den Algorithmus überprüfen und das richtige Byte-Array für das Geheimnis zurückgeben, das zum Signieren des JWT verwendet wurde. Jetzt überprüft JJWT, dass das JWT nicht manipuliert wurde, indem dieses Byte-Array als Schlüssel verwendet wird.

Wenn ich das letzte Zeichen des übergebenen in JWT entferne (das Teil der Signatur ist), ist dies die Antwort:

HTTP/1.1 400 Bad Request
Connection: close
Content-Type: application/json;charset=UTF-8
Date: Mon, 27 Jun 2016 13:19:08 GMT
Server: Apache-Coyote/1.1
Transfer-Encoding: chunked

{
    "exceptionType": "io.jsonwebtoken.SignatureException",
    "message":
      "JWT signature does not match locally computed signature. JWT validity cannot be asserted and should not be trusted.",
    "status": "ERROR"
}

6. JWTs in der Praxis: Spring Security CSRF-Token

Während der Schwerpunkt dieses Beitrags nicht auf Spring Security liegt, werden wir uns hier ein wenig mit der Verwendung der JJWT-Bibliothek in der Praxis befassen.

Cross Site Request Forgery ist eine Sicherheitslücke, bei der eine böswillige Website Sie dazu verleitet, Anforderungen an eine Website zu senden, der Sie vertrauen. Eine der gängigen Abhilfemaßnahmen hierfür ist die Implementierung vonsynchronizer token pattern. Dieser Ansatz fügt ein Token in das Webformular ein und der Anwendungsserver überprüft das eingehende Token anhand seines Repositorys, um zu bestätigen, dass es korrekt ist. Wenn das Token fehlt oder ungültig ist, antwortet der Server mit einem Fehler.

In Spring Security ist das Synchronizer-Token-Muster integriert. Noch besser, wenn SieSpring Boot and Thymeleaf templates verwenden, wird das Synchronisierungstoken automatisch für Sie eingefügt.

Standardmäßig ist das von Spring Security verwendete Token ein "dummes" Token. Es ist nur eine Reihe von Buchstaben und Zahlen. Dieser Ansatz ist in Ordnung und es funktioniert. In diesem Abschnitt erweitern wir die Grundfunktionalität, indem wir JWTs als Token verwenden. Zusätzlich zur Überprüfung, ob das übermittelte Token das erwartete ist, validieren wir das JWT, um weiter nachzuweisen, dass das Token nicht manipuliert wurde, und um sicherzustellen, dass es nicht abgelaufen ist.

Zunächst konfigurieren wir Spring Security mithilfe der Java-Konfiguration. Standardmäßig erfordern alle Pfade eine Authentifizierung und alle POST-Endpunkte CSRF-Token. Wir werden das etwas entspannen, damit das, was wir bisher gebaut haben, immer noch funktioniert.

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    private String[] ignoreCsrfAntMatchers = {
        "/dynamic-builder-compress",
        "/dynamic-builder-general",
        "/dynamic-builder-specific",
        "/set-secrets"
    };

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
                .ignoringAntMatchers(ignoreCsrfAntMatchers)
            .and().authorizeRequests()
                .antMatchers("/**")
                .permitAll();
    }
}

Wir machen hier zwei Dinge. Zunächst sagen wir, dass die CSRF-Tokennot sind, wenn sie auf unseren REST-API-Endpunkten veröffentlicht werden (Zeile 15). Zweitens sagen wir, dass nicht authentifizierter Zugriff für alle Pfade zugelassen werden sollte (Zeilen 17 - 18).

Bestätigen wir, dass Spring Security so funktioniert, wie wir es erwarten. Starten Sie die App und klicken Sie in Ihrem Browser auf diese URL:

http://localhost:8080/jwt-csrf-form

Hier ist die Thymeleaf-Vorlage für diese Ansicht:



    
        
    
    
        

Dies ist ein sehr einfaches Formular, das beim Senden an denselben Endpunkt gesendet wird. Beachten Sie, dass das Formular keinen expliziten Verweis auf CSRF-Token enthält. Wenn Sie die Quelle anzeigen, sehen Sie etwa Folgendes:

Dies ist die Bestätigung, die Sie benötigen, um zu wissen, dass Spring Security funktioniert und dass die Thymeleaf-Vorlagen das CSRF-Token automatisch einfügen.

Um den Wert zu einem JWT zu machen, aktivieren wir ein benutzerdefiniertesCsrfTokenRepository. So ändert sich unsere Spring Security-Konfiguration:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    CsrfTokenRepository jwtCsrfTokenRepository;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
                .csrfTokenRepository(jwtCsrfTokenRepository)
                .ignoringAntMatchers(ignoreCsrfAntMatchers)
            .and().authorizeRequests()
                .antMatchers("/**")
                .permitAll();
    }
}

Um dies zu verbinden, benötigen wir eine Konfiguration, die eine Bean verfügbar macht, die das benutzerdefinierte Token-Repository zurückgibt. Hier ist die Konfiguration:

@Configuration
public class CSRFConfig {

    @Autowired
    SecretService secretService;

    @Bean
    @ConditionalOnMissingBean
    public CsrfTokenRepository jwtCsrfTokenRepository() {
        return new JWTCsrfTokenRepository(secretService.getHS256SecretBytes());
    }
}

Und hier ist unser benutzerdefiniertes Repository (die wichtigen Teile):

public class JWTCsrfTokenRepository implements CsrfTokenRepository {

    private static final Logger log = LoggerFactory.getLogger(JWTCsrfTokenRepository.class);
    private byte[] secret;

    public JWTCsrfTokenRepository(byte[] secret) {
        this.secret = secret;
    }

    @Override
    public CsrfToken generateToken(HttpServletRequest request) {
        String id = UUID.randomUUID().toString().replace("-", "");

        Date now = new Date();
        Date exp = new Date(System.currentTimeMillis() + (1000*30)); // 30 seconds

        String token;
        try {
            token = Jwts.builder()
                .setId(id)
                .setIssuedAt(now)
                .setNotBefore(now)
                .setExpiration(exp)
                .signWith(SignatureAlgorithm.HS256, secret)
                .compact();
        } catch (UnsupportedEncodingException e) {
            log.error("Unable to create CSRf JWT: {}", e.getMessage(), e);
            token = id;
        }

        return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token);
    }

    @Override
    public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
        ...
    }

    @Override
    public CsrfToken loadToken(HttpServletRequest request) {
        ...
    }
}

Die MethodegenerateTokenerstellt eine JWT, die 30 Sekunden nach ihrer Erstellung abläuft. Mit dieser Installation können wir die Anwendung erneut starten und die Quelle von/jwt-csrf-form untersuchen.

Das versteckte Feld sieht nun so aus:

Huzzah! Jetzt ist unser CSRF-Token ein JWT. Das war nicht zu schwer.

Dies ist jedoch nur die Hälfte des Puzzles. Standardmäßig speichert Spring Security das CSRF-Token einfach und bestätigt, dass das in einem Webformular übermittelte Token mit dem gespeicherten übereinstimmt. Wir möchten die Funktionalität erweitern, um das JWT zu validieren und sicherzustellen, dass es nicht abgelaufen ist. Dazu fügen wir einen Filter hinzu. So sieht unsere Spring Security-Konfiguration jetzt aus:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    ...

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .addFilterAfter(new JwtCsrfValidatorFilter(), CsrfFilter.class)
            .csrf()
                .csrfTokenRepository(jwtCsrfTokenRepository)
                .ignoringAntMatchers(ignoreCsrfAntMatchers)
            .and().authorizeRequests()
                .antMatchers("/**")
                .permitAll();
    }

    ...
}

In Zeile 9 haben wir einen Filter hinzugefügt und platzieren ihn nach den StandardwertenCsrfFilter in der Filterkette. Zum Zeitpunkt, an dem unser Filter aktiviert ist, wurde bereits bestätigt, dass das JWT-Token (als Ganzes) den korrekten Wert aufweist, der von Spring Security gespeichert wurde.

Hier sind dieJwtCsrfValidatorFilter (es ist privat, da es eine innere Klasse unserer Spring Security-Konfiguration ist):

private class JwtCsrfValidatorFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
      HttpServletRequest request,
      HttpServletResponse response,
      FilterChain filterChain) throws ServletException, IOException {
        // NOTE: A real implementation should have a nonce cache so the token cannot be reused
        CsrfToken token = (CsrfToken) request.getAttribute("_csrf");

        if (
            // only care if it's a POST
            "POST".equals(request.getMethod()) &&
            // ignore if the request path is in our list
            Arrays.binarySearch(ignoreCsrfAntMatchers, request.getServletPath()) < 0 &&
            // make sure we have a token
            token != null
        ) {
            // CsrfFilter already made sure the token matched.
            // Here, we'll make sure it's not expired
            try {
                Jwts.parser()
                    .setSigningKey(secret.getBytes("UTF-8"))
                    .parseClaimsJws(token.getToken());
            } catch (JwtException e) {
                // most likely an ExpiredJwtException, but this will handle any
                request.setAttribute("exception", e);
                response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
                RequestDispatcher dispatcher = request.getRequestDispatcher("expired-jwt");
                dispatcher.forward(request, response);
            }
        }

        filterChain.doFilter(request, response);
    }
}

Schauen Sie sich Zeile 23 an. Wir analysieren die JWT wie zuvor. In diesem Fall wird die Anforderung an die Vorlageexpired-jwtweitergeleitet, wenn eine Ausnahme ausgelöst wird. Wenn das JWT validiert, wird die Verarbeitung normal fortgesetzt.

Dies schließt die Schleife zum Überschreiben des standardmäßigen Spring Security CSRF-Token-Verhaltens mit einem JWT-Token-Repository und einem Validator.

Wenn Sie die App starten, zu/jwt-csrf-form navigieren, etwas länger als 30 Sekunden warten und auf die Schaltfläche klicken, wird Folgendes angezeigt:

jwt_expired

7. Erweiterte JJWT-Funktionen

Wir schließen unsere JJWT-Reise mit einem Wort zu einigen Funktionen ab, die über die Spezifikation hinausgehen.

7.1. Ansprüche durchsetzen

Im Rahmen des Parsing-Prozesses können Sie in JJWT die erforderlichen Ansprüche und Werte angeben, die diese Ansprüche haben sollten. Dies ist sehr praktisch, wenn Ihre JWT bestimmte Informationen enthalten, die vorhanden sein müssen, damit Sie sie als gültig betrachten können. Es vermeidet viel Verzweigungslogik, um Ansprüche manuell zu validieren. Hier ist die Methode, die dem/parser-enforce-Endpunkt unseres Beispielprojekts dient.

@RequestMapping(value = "/parser-enforce", method = GET)
public JwtResponse parserEnforce(@RequestParam String jwt)
  throws UnsupportedEncodingException {
    Jws jws = Jwts.parser()
        .requireIssuer("Stormpath")
        .require("hasMotorcycle", true)
        .setSigningKeyResolver(secretService.getSigningKeyResolver())
        .parseClaimsJws(jwt);

    return new JwtResponse(jws);
}

In den Zeilen 5 und 6 wird die Syntax für registrierte Ansprüche sowie für benutzerdefinierte Ansprüche angezeigt. In diesem Beispiel wird die JWT als ungültig betrachtet, wenn der iss-Claim nicht vorhanden ist oder nicht den Wert "Stormpath" hat. Es ist auch ungültig, wenn der benutzerdefinierte Anspruch hasMotorcycle nicht vorhanden ist oder nicht den Wert true hat.

Erstellen wir zunächst ein JWT, das dem glücklichen Pfad folgt:

http -v POST localhost:8080/dynamic-builder-specific \
  iss=Stormpath hasMotorcycle:=true sub=msilverman
POST /dynamic-builder-specific HTTP/1.1
Accept: application/json
...
{
    "hasMotorcycle": true,
    "iss": "Stormpath",
    "sub": "msilverman"
}

HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...
{
    "jwt":
      "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0",
    "status": "SUCCESS"
}

Lassen Sie uns nun das JWT validieren:

http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0
GET /parser-enforce?jwt=http
  -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0 HTTP/1.1
Accept: */*
...
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...
{
    "jws": {
        "body": {
            "hasMotorcycle": true,
            "iss": "Stormpath",
            "sub": "msilverman"
        },
        "header": {
            "alg": "HS256"
        },
        "signature": "qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0"
    },
    "status": "SUCCESS"
}

So weit, ist es gut. Lassen wir dieses Mal das hasMotorcycle weg:

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub=msilverman

Dieses Mal, wenn wir versuchen, das JWT zu validieren:

http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIn0.YMONlFM1tNgttUYukDRsi9gKIocxdGAOLaJBymaQAWc

wir bekommen:

GET /parser-enforce?jwt=http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIn0.YMONlFM1tNgttUYukDRsi9gKIocxdGAOLaJBymaQAWc HTTP/1.1
Accept: */*
...
HTTP/1.1 400 Bad Request
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
    "exceptionType": "io.jsonwebtoken.MissingClaimException",
    "message":
      "Expected hasMotorcycle claim to be: true, but was not present in the JWT claims.",
    "status": "ERROR"
}

Dies zeigt an, dass unser Anspruch auf hasMotorcycle erwartet wurde, aber fehlte.

Lassen Sie uns noch ein Beispiel machen:

http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath hasMotorcycle:=false sub=msilverman

Diesmal ist der erforderliche Anspruch vorhanden, hat jedoch den falschen Wert. Sehen wir uns die Ausgabe von:

http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjpmYWxzZSwic3ViIjoibXNpbHZlcm1hbiJ9.8LBq2f0eINB34AzhVEgsln_KDo-IyeM8kc-dTzSCr0c
GET /parser-enforce?jwt=http
  -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjpmYWxzZSwic3ViIjoibXNpbHZlcm1hbiJ9.8LBq2f0eINB34AzhVEgsln_KDo-IyeM8kc-dTzSCr0c HTTP/1.1
Accept: */*
...
HTTP/1.1 400 Bad Request
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: close
Content-Type: application/json;charset=UTF-8
...
{
    "exceptionType": "io.jsonwebtoken.IncorrectClaimException",
    "message": "Expected hasMotorcycle claim to be: true, but was: false.",
    "status": "ERROR"
}

Dies weist darauf hin, dass unser Anspruch auf hasMotorcycle vorhanden war, jedoch einen nicht erwarteten Wert hatte.

MissingClaimException undIncorrectClaimException sind Ihre Freunde, wenn Sie Ansprüche in Ihren JWTs erzwingen, und eine Funktion, die nur die JJWT-Bibliothek bietet.

7.2. JWT-Komprimierung

Wenn Sie viele Ansprüche auf ein JWT haben, kann es groß werden - so groß, dass es in einigen Browsern möglicherweise nicht in eine GET-URL passt.

Machen wir einen großen JWT:

http -v POST localhost:8080/dynamic-builder-specific \
  iss=Stormpath hasMotorcycle:=true sub=msilverman the=quick brown=fox jumped=over lazy=dog \
  somewhere=over rainbow=way up=high and=the dreams=you dreamed=of

Hier ist das JWT, das produziert:

eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIiwidGhlIjoicXVpY2siLCJicm93biI6ImZveCIsImp1bXBlZCI6Im92ZXIiLCJsYXp5IjoiZG9nIiwic29tZXdoZXJlIjoib3ZlciIsInJhaW5ib3ciOiJ3YXkiLCJ1cCI6ImhpZ2giLCJhbmQiOiJ0aGUiLCJkcmVhbXMiOiJ5b3UiLCJkcmVhbWVkIjoib2YifQ.AHNJxSTiDw_bWNXcuh-LtPLvSjJqwDvOOUcmkk7CyZA

Dieser Trottel ist groß! Lassen Sie uns nun einen etwas anderen Endpunkt mit denselben Behauptungen treffen:

http -v POST localhost:8080/dynamic-builder-compress \
  iss=Stormpath hasMotorcycle:=true sub=msilverman the=quick brown=fox jumped=over lazy=dog \
  somewhere=over rainbow=way up=high and=the dreams=you dreamed=of

Dieses Mal bekommen wir:

eyJhbGciOiJIUzI1NiIsImNhbGciOiJERUYifQ.eNpEzkESwjAIBdC7sO4JegdXnoC2tIk2oZLEGB3v7s84jjse_AFe5FOikc5ZLRycHQ3kOJ0Untu8C43ZigyUyoRYSH6_iwWOyGWHKd2Kn6_QZFojvOoDupRwyAIq4vDOzwYtugFJg1QnJv-5sY-TVjQqN7gcKJ3f-j8c-6J-baDFhEN_uGn58XtnpfcHAAD__w.3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE

62 Zeichen kürzer! Hier ist der Code für die Methode zum Generieren der JWT:

@RequestMapping(value = "/dynamic-builder-compress", method = POST)
public JwtResponse dynamicBuildercompress(@RequestBody Map claims)
  throws UnsupportedEncodingException {
    String jws =  Jwts.builder()
        .setClaims(claims)
        .compressWith(CompressionCodecs.DEFLATE)
        .signWith(
            SignatureAlgorithm.HS256,
            secretService.getHS256SecretBytes()
        )
        .compact();
    return new JwtResponse(jws);
}

Beachten Sie, dass wir in Zeile 6 einen zu verwendenden Komprimierungsalgorithmus angeben. Das ist alles dazu.

Was ist mit dem Parsen komprimierter JWTs? Die JJWT-Bibliothek erkennt die Komprimierung automatisch und verwendet denselben Algorithmus zum Dekomprimieren:

GET /parser?jwt=eyJhbGciOiJIUzI1NiIsImNhbGciOiJERUYifQ.eNpEzkESwjAIBdC7sO4JegdXnoC2tIk2oZLEGB3v7s84jjse_AFe5FOikc5ZLRycHQ3kOJ0Untu8C43ZigyUyoRYSH6_iwWOyGWHKd2Kn6_QZFojvOoDupRwyAIq4vDOzwYtugFJg1QnJv-5sY-TVjQqN7gcKJ3f-j8c-6J-baDFhEN_uGn58XtnpfcHAAD__w.3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE HTTP/1.1
Accept: */*
...
HTTP/1.1 200 OK
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Content-Type: application/json;charset=UTF-8
...
{
    "claims": {
        "body": {
            "and": "the",
            "brown": "fox",
            "dreamed": "of",
            "dreams": "you",
            "hasMotorcycle": true,
            "iss": "Stormpath",
            "jumped": "over",
            "lazy": "dog",
            "rainbow": "way",
            "somewhere": "over",
            "sub": "msilverman",
            "the": "quick",
            "up": "high"
        },
        "header": {
            "alg": "HS256",
            "calg": "DEF"
        },
        "signature": "3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE"
    },
    "status": "SUCCESS"
}

Beachten Sie den Anspruch voncalgim Header. Dies wurde automatisch in die JWT codiert und gibt dem Parser einen Hinweis darauf, welcher Algorithmus für die Dekomprimierung verwendet werden soll.

Note
Die JWE-Spezifikation unterstützt die Komprimierung. In einer kommenden Version der JJWT-Bibliothek werden wir JWE und komprimierte JWEs unterstützen. Wir werden die Komprimierung in anderen Arten von JWTs weiterhin unterstützen, auch wenn dies nicht angegeben ist.

8. Token-Tools für Java-Entwickler

Während der Schwerpunkt dieses Artikels nicht auf Spring Boot oder Spring Security lag, war es mit diesen beiden Technologien einfach, alle in diesem Artikel beschriebenen Funktionen zu demonstrieren. Sie sollten in der Lage sein, den Server einzuschalten und mit den verschiedenen Endpunkten zu spielen, die wir besprochen haben. Schlag einfach:

http http://localhost:8080

Stormpath freut sich außerdem, eine Reihe von Open Source-Entwicklertools für die Java-Community bereitzustellen. Diese schließen ein:

8.1. JJWT (Worüber wir gesprochen haben)

JJWT ist eine einfach zu verwendendetool for developers to create and verify JWTs in Java. Wie viele Bibliotheken, die Stormpath unterstützt, ist JJWT völlig kostenlos und Open Source (Apache License, Version 2.0), sodass jeder sehen kann, was es tut und wie es es tut. Zögern Sie nicht, Probleme zu melden, Verbesserungen vorzuschlagen und sogar Code einzureichen!

8.2. jsonwebtoken.io and java.jsonwebtoken.io

jsonwebtoken.io ist ein Entwicklertool, das wir erstellt haben, um das Dekodieren von JWTs zu vereinfachen. Fügen Sie einfach eine vorhandene JWT in das entsprechende Feld ein, um deren Header, Nutzdaten und Signatur zu dekodieren. jsonwebtoken.io is powered by nJWT, the cleanest free and open source (Apache License, Version 2.0) JWT library for Node.js developers. Sie können auf dieser Website auch Code sehen, der für eine Vielzahl von Sprachen generiert wurde. Die Website selbst ist Open Source und kann inhere gefunden werden.

java.jsonwebtoken.io ist speziell für die JJWT-Bibliothek. Sie können die Header und Nutzdaten in der oberen rechten Box ändern, die von JJWT generierte JWT in der oberen linken Box sehen und ein Beispiel für den Builder- und Parser-Java-Code in den unteren Boxen sehen. Die Website selbst ist Open Source und kann inhere gefunden werden.

8.3. JWT Inspector

Das neue Kind auf dem Block,JWT Inspector, ist eine Open-Source-Chrome-Erweiterung, mit der Entwickler JWTs direkt im Browser überprüfen und debuggen können. Der JWT-Inspektor erkennt JWTs auf Ihrer Website (in Cookies, lokalem Speicher / Sitzungsspeicher und Kopfzeilen) und macht sie über die Navigationsleiste und das DevTools-Bedienfeld leicht zugänglich.

9. JWT das runter!

JWTs verleihen gewöhnlichen Token eine gewisse Intelligenz. Die Möglichkeit, kryptografisch zu signieren und zu überprüfen, Ablaufzeiten zu berücksichtigen und andere Informationen in JWTs zu kodieren, bildet die Grundlage für ein wirklich zustandsloses Sitzungsmanagement. Dies hat große Auswirkungen auf die Skalierbarkeit von Anwendungen.

Bei Stormpath verwenden wir JWTs unter anderem für OAuth2-Token, CSRF-Token und Zusicherungen zwischen Microservices.

Sobald Sie mit der Verwendung von JWTs beginnen, kehren Sie möglicherweise nie mehr zu den blöden Token der Vergangenheit zurück. Haben Sie noch Fragen? Schlagen Sie mich bei@afitnerd auf Twitter.