Geef Java-authenticatie een boost met JSON Web Tokens (JWT's)

Klaar om te bouwen, of worstelt u met, veilige authenticatie in uw Java-applicatie? Weet u niet zeker wat de voordelen zijn van het gebruik van tokens (en met name JSON-webtokens) of hoe ze moeten worden ingezet? Ik ben verheugd om deze vragen, en meer, voor u te beantwoorden in deze tutorial!

Voordat we ingaan op JSON Web Tokens (JWT's) en de JJWT-bibliotheek (gemaakt door Stormpath's CTO, Les Hazlewood en onderhouden door een gemeenschap van bijdragers), laten we eerst enkele basisprincipes bespreken.

1. Authenticatie versus tokenauthenticatie

De set protocollen die een applicatie gebruikt om de identiteit van de gebruiker te bevestigen, is authenticatie. Toepassingen hebben traditioneel hun identiteit behouden door middel van sessiecookies. Dit paradigma is gebaseerd op server-side opslag van sessie-ID's, wat ontwikkelaars dwingt om sessieopslag te creëren die ofwel uniek en serverspecifiek is, of geïmplementeerd als een volledig aparte sessieopslaglaag.

Token-authenticatie is ontwikkeld om problemen op te lossen die server-side sessie-ID's niet deden en niet konden. Net als bij traditionele authenticatie presenteren gebruikers verifieerbare inloggegevens, maar krijgen ze nu een set tokens in plaats van een sessie-ID. De initiële inloggegevens kunnen het standaard gebruikersnaam / wachtwoord-paar, API-sleutels of zelfs tokens van een andere service zijn. (De API Key Authentication Feature van Stormpath is hier een voorbeeld van.)

1.1. Waarom tokens?

Heel eenvoudig: het gebruik van tokens in plaats van sessie-ID's kan uw serverbelasting verlagen, machtigingsbeheer stroomlijnen en betere tools bieden voor het ondersteunen van een gedistribueerde of cloudgebaseerde infrastructuur. In het geval van JWT wordt dit voornamelijk bereikt door het staatloze karakter van dit soort tokens (meer daarover hieronder).

Tokens bieden een breed scala aan toepassingen, waaronder: Cross Site Request Forgery (CSRF) -beschermingsschema's, OAuth 2.0-interacties, sessie-ID's en (in cookies) als authenticatie-representaties. In de meeste gevallen specificeren standaarden geen bepaald formaat voor tokens. Hier is een voorbeeld van een typisch CSRF-token van Spring Security in HTML-vorm:

Als je dat formulier probeert te posten zonder het juiste CSRF-token, krijg je een foutreactie, en dat is het nut van tokens. Het bovenstaande voorbeeld is een "dom" teken. Dit betekent dat er geen inherente betekenis is die uit het token zelf kan worden afgeleid. Dit is ook waar JWT's een groot verschil maken.

2. Wat zit er in een JWT?

JWT's (uitgesproken als "jots") zijn URL-veilige, gecodeerde, cryptografisch ondertekende (soms gecodeerde) tekenreeksen die als tokens in verschillende toepassingen kunnen worden gebruikt. Hier is een voorbeeld van een JWT die wordt gebruikt als een CSRF-token:

In dit geval kun je zien dat het token veel langer is dan in ons vorige voorbeeld. Zoals we eerder zagen, krijg je een foutmelding als het formulier zonder het token wordt verzonden.

Dus waarom JWT?

Het bovenstaande token is cryptografisch ondertekend en kan daarom worden geverifieerd, als bewijs dat er niet mee is geknoeid. JWT's zijn ook gecodeerd met een verscheidenheid aan aanvullende informatie.

Laten we eens kijken naar de anatomie van een JWT om beter te begrijpen hoe we al deze goedheid eruit persen. Het is je misschien opgevallen dat er drie verschillende secties zijn, gescheiden door punten (.):

HeadereyJhbGciOiJIUzI1NiJ9
LaadvermogeneyJqdGkiOiJlNjc4ZjIzMzQ3ZTM0MTBkYjdlNjg3Njc4MjNiMmQ3MCIsImlhdC

I6MTQ2NjYzMzMxNywibmJmIjoxNDY2NjMzMzE3LCJleHAiOjE0NjY2MzY5MTd9

Handtekeningrgx_o8VQGuDa2AqCHSgVOD5G68Ld_YYM7N7THmvLIKc

Elke sectie is base64 URL-gecodeerd. Dit zorgt ervoor dat het veilig kan worden gebruikt in een URL (hierover later meer). Laten we elke sectie afzonderlijk van naderbij bekijken.

2.1. De koptekst

Als je base64 gebruikt om de header te decoderen, krijg je de volgende JSON-string:

{"alg": "HS256"}

Dit toont aan dat de JWT is ondertekend met HMAC met behulp van SHA-256.

2.2. Het laadvermogen

Als je de payload decodeert, krijg je de volgende JSON-string (voor de duidelijkheid opgemaakt):

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

Binnen de payload zijn er, zoals u kunt zien, een aantal sleutels met waarden. Deze sleutels worden "claims" genoemd en de JWT-specificatie heeft er zeven gespecificeerd als "geregistreerde" claims. Zij zijn:

issUitgever
subOnderwerpen
audPubliek
expVervaldatum
nbfNiet Hiervoor
iatAfgegeven op
jtiJWT-ID

Wanneer u een JWT bouwt, kunt u elke gewenste aangepaste claim indienen. De bovenstaande lijst geeft eenvoudigweg de claims weer die zijn gereserveerd, zowel in de sleutel die wordt gebruikt als in het verwachte type. Onze CSRF heeft een JWT-ID, een "Issued At" -tijd, een "Not Before" -tijd en een Vervaltijd. De vervaltijd is precies één minuut na de opgegeven tijd.

2.3. De handtekening

Ten slotte wordt de handtekeningsectie gemaakt door de header en payload samen te nemen (met de. Ertussen) en deze door het gespecificeerde algoritme te leiden (in dit geval HMAC met SHA-256) samen met een bekend geheim. Merk op dat het geheim is altijd een byte-array en moet een lengte hebben die logisch is voor het gebruikte algoritme. Hieronder gebruik ik een willekeurige base64-gecodeerde string (voor leesbaarheid) die is geconverteerd naar een byte-array.

Het ziet er zo uit in pseudo-code:

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

Zolang u het geheim kent, kunt u zelf de handtekening genereren en uw resultaat vergelijken met het handtekeninggedeelte van de JWT om te controleren of er niet mee is geknoeid. Technisch gezien wordt een JWT die cryptografisch ondertekend is, een JWS genoemd. JWT's kunnen ook worden versleuteld en zouden dan een JWE worden genoemd. (In de praktijk wordt de term JWT gebruikt om JWE's en JWS'en te beschrijven.)

Dit brengt ons terug bij de voordelen van het gebruik van een JWT als ons CSRF-token. We kunnen de handtekening verifiëren en we kunnen de informatie die in het JWT is gecodeerd, gebruiken om de geldigheid ervan te bevestigen. De tekenreeksweergave van de JWT moet dus niet alleen overeenkomen met wat er op de server is opgeslagen, we kunnen er ook voor zorgen dat deze niet vervallen is door simpelweg de exp beweren. Dit zorgt ervoor dat de server geen extra status behoudt.

Welnu, we hebben hier veel terrein afgelegd. Laten we in wat code duiken!

3. Stel de JJWT-zelfstudie in

JJWT (//github.com/jwtk/jjwt) is een Java-bibliotheek die end-to-end JSON Web Token-creatie en verificatie biedt. Voor altijd gratis en open-source (Apache-licentie, versie 2.0), het is ontworpen met een op de bouwer gerichte interface die het grootste deel van zijn complexiteit verbergt.

De primaire bewerkingen bij het gebruik van JJWT zijn het bouwen en ontleden van JWT's. We zullen deze bewerkingen vervolgens bekijken, dan ingaan op enkele uitgebreide functies van de JJWT, en ten slotte zullen we JWT's in actie zien als CSRF-tokens in een Spring Security, Spring Boot-applicatie.

De code die in de volgende secties wordt gedemonstreerd, is hier te vinden. Opmerking: het project maakt vanaf het begin gebruik van Spring Boot omdat het gemakkelijk te communiceren is met de API die het blootlegt.

Voer het volgende uit om het project te bouwen:

git clone //github.com/eugenp/tutorials.git cd tutorials / jjwt mvn schone installatie

Een van de geweldige dingen van Spring Boot is hoe gemakkelijk het is om een ​​applicatie te starten. Om de JJWT Fun-applicatie uit te voeren, doet u gewoon het volgende:

java -jar doel / *. jar 

Er zijn tien eindpunten weergegeven in deze voorbeeldtoepassing (ik gebruik httpie om met de toepassing te communiceren. U kunt deze hier vinden.)

http localhost: 8080
Beschikbare commando's (veronderstelt httpie - //github.com/jkbrzt/httpie): http // localhost: 8080 / Dit gebruiksbericht http // localhost: 8080 / static-builder bouwt JWT op van hardgecodeerde claims http POST // localhost: 8080 / dynamic-builder-general claim-1 = value-1 ... [claim-n = value-n] build JWT op basis van doorgegeven claims (met behulp van algemene claimkaart) http POST // localhost: 8080 / dynamic-builder-specifieke claim -1 = waarde-1 ... [claim-n = waarde-n] bouw JWT op basis van doorgegeven claims (met behulp van specifieke claimmethoden) http POST // localhost: 8080 / dynamic-builder-compress claim-1 = waarde-1 ... [claim-n = waarde-n] build DEFLATE gecomprimeerde JWT van geslaagd in claims http // localhost: 8080 / parser? jwt = Parse doorgegeven in JWT http // localhost: 8080 / parser-enforce? jwt = Parse geslaagd in JWT het afdwingen van de 'iss' geregistreerde claim en de 'hasMotorcycle' aangepaste claim http // localhost: 8080 / get-secrets Toon de ondertekeningssleutels die momenteel in gebruik zijn. http // localhost: 8080 / refresh-secrets Genereer nieuwe ondertekeningssleutels en laat ze zien. http POST // localhost: 8080 / set-secrets HS256 = base64-gecodeerde-waarde HS384 = base64-gecodeerde-waarde HS512 = base64-gecodeerde-waarde Stel expliciet geheimen in voor gebruik in de toepassing.

In de volgende secties zullen we elk van deze eindpunten en de JJWT-code in de handlers onderzoeken.

4. JWT's bouwen met JJWT

Vanwege de vloeiende interface van JJWT is het maken van de JWT in feite een proces in drie stappen:

  1. De definitie van de interne claims van het token, zoals Issuer, Subject, Expiration en ID.
  2. De cryptografische ondertekening van de JWT (waardoor het een JWS wordt).
  3. Het samenpersen van de JWT tot een URL-veilige string, volgens de JWT Compact Serialization-regels.

De laatste JWT is een driedelige base64-gecodeerde tekenreeks, ondertekend met het opgegeven handtekeningalgoritme en met behulp van de meegeleverde sleutel. Na dit punt is het token klaar om te worden gedeeld met de andere partij.

Hier is een voorbeeld van de JJWT in actie:

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

Dit lijkt erg op de code in de StaticJWTController.fixedBuilder methode van het codeproject.

Op dit punt is het de moeite waard om te praten over een paar antipatronen met betrekking tot JWT's en ondertekening. Als je ooit JWT-voorbeelden hebt gezien, ben je waarschijnlijk een van deze anti-patroonscenario's tegengekomen:

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

Elk van de HS type handtekening algoritmen neemt een byte-array. Het is handig voor mensen om te lezen om een ​​string te nemen en deze om te zetten in een byte-array.

Antipatroon 1 hierboven laat dit zien. Dit is problematisch omdat het geheim wordt verzwakt doordat het zo kort is en het geen byte-array in zijn oorspronkelijke vorm is. Om het leesbaar te houden, kunnen we de byte-array met base64 coderen.

Antipatroon 2 hierboven neemt echter de met base64 gecodeerde reeks en converteert deze rechtstreeks naar een byte-array. Wat moet worden gedaan, is om de base64-reeks terug te decoderen in de originele byte-array.

Nummer 3 hierboven demonstreert dit. Dus waarom is deze ook een antipatroon? Het is in dit geval een subtiele reden. Merk op dat het handtekeningalgoritme HS512 is. De byte-array is niet de maximale lengte die HS512 kan ondersteunen, waardoor het een zwakker geheim wordt dan wat mogelijk is voor dat algoritme.

De voorbeeldcode bevat een klasse met de naam Geheime dienst dat ervoor zorgt dat geheimen van de juiste sterkte worden gebruikt voor het gegeven algoritme. Bij het opstarten van de applicatie wordt een nieuwe set geheimen gemaakt voor elk van de HS-algoritmen. Er zijn eindpunten om de geheimen te vernieuwen en om de geheimen expliciet in te stellen.

Als u het project laat draaien zoals hierboven beschreven, voer dan het volgende uit zodat de onderstaande JWT-voorbeelden overeenkomen met de antwoorden van uw project.

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

Nu kun je op de / static-builder eindpunt:

http // localhost: 8080 / static-builder

Dit levert een JWT op die er als volgt uitziet:

eyJhbGciOiJIUzI1NiJ9. eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIoxiaMODIYNJIQK2DYWRtaW5jIQk2YWRtaW5zIQk2WFIjNjQzYWRtaW5zIQk2ODIY0JJWJWFIjNJWJDYWJJ0JJWJDY0IjNJWDYWRTa kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ

Druk nu op:

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

Het antwoord bevat alle claims die we hebben opgenomen toen we de JWT creëerden.

HTTP / 1.1 200 OK Inhoudstype: 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 ":" SUCCES "}

Dit is de parseerbewerking, die we in de volgende sectie zullen bespreken.

Laten we nu een eindpunt bereiken dat claims als parameters aanneemt en een aangepaste JWT voor ons zal bouwen.

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

Opmerking: Er is een subtiel verschil tussen de hasMotorcycle claim en de andere claims. httpie gaat ervan uit dat JSON-parameters standaard strings zijn. Om onbewerkte JSON in te dienen met httpie, gebruik je de := vorm in plaats van =. Zonder dat zou het zich onderwerpen "HasMotorcycle": "true", dat is niet wat we willen.

Hier is de output:

POST / dynamic-builder-general HTTP / 1.1 Accepteer: application / json ... {"hasMotorcycle": true, "iss": "Stormpath", "sub": "msilverman"} HTTP / 1.1 200 OK Inhoudstype: application / json; charset = UTF-8 ... { "jwt": "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwiaGFzTW90b3JjeWNsZSI6dHJ1ZX0.OnyDs-zoL3-rw1GaSl_KzZzHK9GoiNocu-YwZ_nQNZU", "Status": "succes"} 

Laten we eens kijken naar de code die dit eindpunt ondersteunt:

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

Regel 2 zorgt ervoor dat de inkomende JSON automatisch wordt geconverteerd naar een Java Map, wat superhandig is voor JJWT aangezien de methode op regel 5 gewoon die Map neemt en alle claims tegelijk instelt.

Hoe beknopt deze code ook is, we hebben iets specifiekers nodig om ervoor te zorgen dat de claims die worden doorgegeven geldig zijn. De ... gebruiken .setClaims (kaartclaims) methode is handig als u al weet dat de claims die op de kaart worden weergegeven, geldig zijn. Dit is waar de typeveiligheid van Java in de JJWT-bibliotheek komt.

Voor elk van de geregistreerde claims die zijn gedefinieerd in de JWT-specificatie, is er een overeenkomstige Java-methode in de JJWT die het specific-correcte type aanneemt.

Laten we in ons voorbeeld naar een ander eindpunt gaan en kijken wat er gebeurt:

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

Merk op dat we een geheel getal, 5, hebben doorgegeven voor de "sub" -claim. Hier is de output:

POST / dynamic-builder-specifieke HTTP / 1.1 Accepteer: 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.Geheel getal kan niet worden gecast naar java.lang.String", "status ": "FOUT" }

Nu krijgen we een foutreactie omdat de code het type geregistreerde claims afdwingt. In dit geval, sub moet een string zijn. Hier is de code die dit eindpunt ondersteunt:

@RequestMapping (waarde = "/ dynamic-builder-specific", method = POST) public JwtResponse dynamicBuilderSpecific (@RequestBody Map claims) gooit 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) waarde); 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 ()); retourneer nieuwe JwtResponse (builder.compact ()); }

Net als voorheen accepteert de methode een Kaart van claims als parameter. Deze keer noemen we echter de specifieke methode voor elk van de geregistreerde claims die het type afdwingt.

Een verfijning hiervan is om de foutmelding specifieker te maken. Op dit moment weten we alleen dat een van onze claims niet het juiste type is. We weten niet welke claim fout was of wat het zou moeten zijn. Hier is een methode die ons een specifiekere foutmelding geeft. Het behandelt ook een bug in de huidige code.

private void sureType (String registeredClaim, objectwaarde, klasse verwachtType) {boolean isCorrectType = verwachtType.isInstance (waarde) || verwachtType == Long.class && waarde instanceof Integer; if (! isCorrectType) {String msg = "Verwacht type:" + wantedType.getCanonicalName () + "voor geregistreerde claim: '" + registeredClaim + "', maar kreeg waarde:" + waarde + "van type:" + waarde. getClass (). getCanonicalName (); gooi nieuwe JwtException (msg); }}

Regel 3 controleert of de doorgegeven waarde van het verwachte type is. Zo niet, dan a JwtException wordt gegenereerd met de specifieke fout. Laten we dit eens in actie bekijken door dezelfde oproep te doen als eerder:

http -v POST localhost: 8080 / dynamic-builder-specific iss = Stormpath sub: = 5 hasMotorcycle: = true
POST / dynamic-builder-specifieke HTTP / 1.1 Accepteer: 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": "Verwacht type: java.lang.String voor geregistreerde claim: 'sub', maar kreeg waarde: 5 van type: java.lang .Geheel getal "," status ":" ERROR "}

Nu hebben we een heel specifieke foutmelding die ons vertelt dat de sub claim is degene die fout is.

Laten we teruggaan naar die bug in onze code. Het probleem heeft niets te maken met de JJWT-bibliotheek. Het probleem is dat de JSON naar Java Object-mapper die in Spring Boot is ingebouwd, te slim is voor ons eigen bestwil.

Als er een methode is die een Java-object accepteert, converteert de JSON-mapper automatisch een doorgegeven getal dat kleiner is dan of gelijk is aan 2.147.483.647 naar een Java Geheel getal. Evenzo zal het automatisch een doorgegeven getal dat groter is dan 2.147.483.647 naar een Java converteren Lang. Voor de iat, nbf, en exp claims van een JWT, willen we dat onze sureType-test slaagt, ongeacht of het toegewezen object een geheel getal of een lang object is. Daarom hebben we de aanvullende clausule om te bepalen of de doorgegeven waarde het juiste type is:

 boolean isCorrectType = verwachtType.isInstance (waarde) || verwachtType == Long.class && waarde instanceof Integer;

Als we een Long verwachten, maar de waarde is een instantie van Integer, zeggen we nog steeds dat dit het juiste type is. Met een goed begrip van wat er met deze validatie gebeurt, kunnen we deze nu integreren in onze dynamicBuilderSpecific methode:

@RequestMapping (waarde = "/ dynamic-builder-specific", method = POST) public JwtResponse dynamicBuilderSpecific (@RequestBody Map claims) gooit UnsupportedEncodingException {JwtBuilder builder = Jwts.builder (); claims.forEach ((key, value) -> {switch (key) {case "iss": sureType (key, value, String.class); builder.setIssuer ((String) waarde); break; case "sub": sureType (sleutel, waarde, String.class); builder.setSubject ((String) waarde); break; case "aud": sureType (sleutel, waarde, String.class); builder.setAudience ((String) waarde); breken ; case "exp": sureType (sleutel, waarde, Long.class); builder.setExpiration (Date.from (Instant.ofEpochSecond (Long.parseLong (value.toString ())))); break; case "nbf": sureType (key, value, Long.class); builder.setNotBefore (Date.from (Instant.ofEpochSecond (Long.parseLong (value.toString ())))); break; case "iat": sureType (key, value, Long.class); builder.setIssuedAt (Date.from (Instant.ofEpochSecond (Long.parseLong (value.toString ())))); break; case "jti": sureType (key, value, String.class); builder .setId ((String) waarde); break; standaard: builder.claim (sleutel, waarde);}}); builder.signWith (SignatureAlgorithm.HS256, secretService.getHS256SecretBytes ()); retourneer nieuwe JwtResponse (builder.compact ()); }

Opmerking: In alle voorbeeldcode in deze sectie zijn JWT's ondertekend met de HMAC met behulp van het SHA-256-algoritme. Dit om de voorbeelden eenvoudig te houden. De JJWT-bibliotheek ondersteunt 12 verschillende handtekeningalgoritmen waarvan u in uw eigen code kunt profiteren.

5. JWT's parseren met JJWT

We zagen eerder dat ons codevoorbeeld een eindpunt heeft voor het parseren van een JWT. Dit eindpunt raken:

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

produceert dit antwoord:

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 ":" SUCCES "}

De parser methode van de StaticJWTController klasse ziet er als volgt uit:

@RequestMapping (value = "/ parser", method = GET) openbare JwtResponse-parser (@RequestParam String jwt) genereert UnsupportedEncodingException {Jws jws = Jwts.parser () .setSigningKeyResolver (secretService.getSolverse (secretService). retourneer nieuwe JwtResponse (jws); }

Regel 4 geeft aan dat we verwachten dat de inkomende tekenreeks een ondertekende JWT (een JWS) is. En we gebruiken hetzelfde geheim dat werd gebruikt om de JWT te ondertekenen bij het ontleden ervan. Regel 5 ontleedt de claims van de JWT. Intern verifieert het de handtekening en het genereert een uitzondering als de handtekening ongeldig is.

Merk op dat we in dit geval een SigningKeyResolver in plaats van een sleutel zelf. Dit is een van de krachtigste aspecten van JJWT. De koptekst van JWT geeft het algoritme aan dat is gebruikt om het te ondertekenen. We moeten de JWT echter verifiëren voordat we deze vertrouwen. Het lijkt een addertje onder het gras. 22. Laten we eens kijken naar de SecretService.getSigningKeyResolver methode:

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

Met behulp van de toegang tot het JwsHeaderKan ik het algoritme inspecteren en de juiste byte-array retourneren voor het geheim dat werd gebruikt om de JWT te ondertekenen. Nu zal JJWT verifiëren dat er niet met de JWT is geknoeid door deze byte-array als sleutel te gebruiken.

Als ik het laatste teken van het doorgegeven in JWT verwijder (dat deel uitmaakt van de handtekening), is dit het antwoord:

HTTP / 1.1 400 Bad Request Connection: close Content-Type: application / json; charset = UTF-8 Datum: Mon, 27 Jun 2016 13:19:08 GMT Server: Apache-Coyote / 1.1 Transfer-Encoding: chunked {"exceptionType ":" io.jsonwebtoken.SignatureException "," message ":" JWT-handtekening komt niet overeen met lokaal berekende handtekening. JWT-geldigheid kan niet worden bevestigd en mag niet worden vertrouwd. "," status ":" ERROR "}

6. JWT's in de praktijk: Spring Security CSRF-tokens

Hoewel de focus van dit bericht niet Spring Security is, gaan we er hier een beetje op ingaan om wat real-world gebruik van de JJWT-bibliotheek te laten zien.

Cross Site Request Forgery is een beveiligingsprobleem waarbij een kwaadwillende website u misleidt om verzoeken in te dienen bij een website waarmee u een vertrouwensrelatie heeft opgebouwd. Een van de meest voorkomende remedies hiervoor is het implementeren van een synchronisatietokenpatroon. Deze benadering voegt een token in het webformulier in en de applicatieserver controleert het inkomende token met zijn repository om te bevestigen dat het correct is. Als het token ontbreekt of ongeldig is, zal de server reageren met een foutmelding.

Spring Security heeft het synchronisatietokenpatroon ingebouwd. Beter nog, als u de Spring Boot- en Thymeleaf-sjablonen gebruikt, wordt het synchronisatietoken automatisch voor u ingevoegd.

Standaard is het token dat Spring Security gebruikt een "dom" token. Het is maar een reeks letters en cijfers. Deze aanpak is prima en het werkt. In deze sectie verbeteren we de basisfunctionaliteit door JWT's als token te gebruiken. Naast het verifiëren dat het verzonden token het verwachte is, valideren we de JWT om verder te bewijzen dat er niet met het token is geknoeid en om ervoor te zorgen dat het niet is verlopen.

Om te beginnen gaan we Spring Security configureren met behulp van Java-configuratie. Standaard is voor alle paden verificatie vereist en voor alle POST-eindpunten CSRF-tokens. We gaan dat een beetje ontspannen, zodat wat we tot nu toe hebben gebouwd nog steeds werkt.

@Configuration public class WebSecurityConfig breidt WebSecurityConfigurerAdapter {private String [] ignoreCsrfAntMatchers = {"/ dynamic-builder-compress", "/ dynamic-builder-general", "/ dynamic-builder-specific", "/ set-secrets"} uit; @Override protected void configure (HttpSecurity http) gooit uitzondering {http .csrf () .ignoringAntMatchers (ignoreCsrfAntMatchers) .and (). AuthorizeRequests () .antMatchers ("/ **") .permitAll (); }}

We doen hier twee dingen. Ten eerste zeggen we dat de CSRF-tokens dat zijn niet vereist bij het posten naar onze REST API-eindpunten (regel 15). Ten tweede zeggen we dat niet-geauthenticeerde toegang moet worden toegestaan ​​voor alle paden (regels 17 - 18).

Laten we bevestigen dat Spring Security werkt zoals we verwachten. Start de app en klik op deze url in uw browser:

// localhost: 8080 / jwt-csrf-form

Hier is de Thymeleaf-sjabloon voor deze weergave:

Dit is een heel eenvoudig formulier dat bij verzending naar hetzelfde eindpunt POST. Merk op dat er geen expliciete verwijzing naar CSRF-tokens in het formulier is. Als je de bron bekijkt, zie je zoiets als:

Dit is de enige bevestiging die u nodig hebt om te weten dat Spring Security werkt en dat de Thymeleaf-sjablonen automatisch het CSRF-token invoegen.

Om van de waarde een JWT te maken, zullen we een aangepaste inschakelen CsrfTokenRepository. Dit is hoe onze Spring Security-configuratie verandert:

@Configuration openbare klasse WebSecurityConfig breidt WebSecurityConfigurerAdapter {@Autowired CsrfTokenRepository jwtCsrfTokenRepository uit; @Override protected void configure (HttpSecurity http) genereert uitzondering {http .csrf () .csrfTokenRepository (jwtCsrfTokenRepository) .ignoringAntMatchers (ignoreCsrfAntMatchers) .and (). AuthorizeRequests () .ermantMatchers (). }}

Om dit te verbinden, hebben we een configuratie nodig die een bean blootstelt die de aangepaste tokenrepository retourneert. Hier is de configuratie:

@Configuration openbare klasse CSRFConfig {@Autowired SecretService secretService; @Bean @ConditionalOnMissingBean openbare CsrfTokenRepository jwtCsrfTokenRepository () {retourneer nieuwe JWTCsrfTokenRepository (secretService.getHS256SecretBytes ()); }}

En hier is onze aangepaste repository (de belangrijke stukjes):

openbare klasse JWTCsrfTokenRepository implementeert CsrfTokenRepository {privé statisch eindlogboeklogboek = LoggerFactory.getLogger (JWTCsrfTokenRepository.class); privé-byte [] geheim; openbare JWTCsrfTokenRepository (byte [] geheim) {this.secret = geheim; } @Override openbare CsrfToken genererenToken (HttpServletRequest-verzoek) {String id = UUID.randomUUID (). ToString (). Replace ("-", ""); Datum nu = nieuwe datum (); Datum exp = nieuwe datum (System.currentTimeMillis () + (1000 * 30)); // 30 seconden String-token; probeer {token = Jwts.builder () .setId (id) .setIssuedAt (nu) .setNotBefore (nu) .setExpiration (exp) .signWith (SignatureAlgorithm.HS256, geheim) .compact (); } catch (UnsupportedEncodingException e) {log.error ("Kon CSRf JWT niet maken: {}", e.getMessage (), e); token = id; } retourneer nieuwe DefaultCsrfToken ("X-CSRF-TOKEN", "_csrf", token); } @Override public void saveToken (CsrfToken-token, HttpServletRequest-verzoek, HttpServletResponse-antwoord) {...} @Override public CsrfToken loadToken (HttpServletRequest-verzoek) {...}}

De genererenToken methode maakt een JWT aan die 30 seconden verloopt nadat deze is gemaakt. Met dit sanitair op zijn plaats, kunnen we de applicatie opnieuw starten en kijken naar de bron van / jwt-csrf-formulier.

Nu ziet het verborgen veld er als volgt uit:

Huzzah! Nu is ons CSRF-token een JWT. Dat was niet zo moeilijk.

Dit is echter slechts de helft van de puzzel. Spring Security slaat standaard het CSRF-token op en bevestigt dat het token dat in een webformulier is ingediend, overeenkomt met het token dat is opgeslagen. We willen de functionaliteit uitbreiden om de JWT te valideren en ervoor te zorgen dat deze niet is verlopen. Om dat te doen, voegen we een filter toe. Dit is hoe onze Spring Security-configuratie er nu uitziet:

@Configuration openbare klasse WebSecurityConfig breidt WebSecurityConfigurerAdapter uit {... @Override beschermde ongeldige configuratie (HttpSecurity http) gooit Uitzondering {http .addFilterAfter (nieuw JwtCsrfValidatorFilter (), CsrfFiltertokenClass) .csrf (j). .en (). authorizeRequests () .antMatchers ("/ **") .permitAll (); } ...}

Op regel 9 hebben we een filter toegevoegd en we plaatsen het in de filterketen na de standaardinstelling CsrfFilter. Dus tegen de tijd dat ons filter wordt geraakt, is al bevestigd dat het JWT-token (als geheel) de juiste waarde is die is opgeslagen door Spring Security.

Hier is de JwtCsrfValidatorFilter (het is privé omdat het een innerlijke klasse is van onze Spring Security-configuratie):

private class JwtCsrfValidatorFilter verlengt OncePerRequestFilter {@Override protected void doFilterInternal (HttpServletRequest-verzoek, HttpServletResponse-antwoord, FilterChain filterChain) gooit ServletException, IOException {// OPMERKING: een echte implementatie zou een niet-cachetoken moeten worden hergebruikt, dus de tosTfoken-code moet niet worden gebruikt. request.getAttribute ("_ csrf"); if (// geeft alleen als het een POST is "POST" .equals (request.getMethod ()) && // negeer of het verzoekpad in onze lijst staat Arrays.binarySearch (ignoreCsrfAntMatchers, request.getServletPath ()) <0 && / / zorg ervoor dat we een token-token hebben! = null) {// CsrfFilter heeft er al voor gezorgd dat het token overeenkomt. // Hier zullen we ervoor zorgen dat het niet is verlopen, probeer {Jwts.parser () .setSigningKey (secret.getBytes ("UTF-8")) .parseClaimsJws (token.getToken ()); } catch (JwtException e) {// hoogstwaarschijnlijk een ExpiredJwtException, maar dit zal elk request.setAttribute ("exception", e) afhandelen; response.setStatus (HttpServletResponse.SC_BAD_REQUEST); RequestDispatcher dispatcher = request.getRequestDispatcher ("expired-jwt"); dispatcher.forward (verzoek, antwoord); }} filterChain.doFilter (verzoek, antwoord); }}

Bekijk lijn 23 op. We ontleden de JWT zoals eerder. Als in dit geval een uitzondering wordt gegenereerd, wordt het verzoek doorgestuurd naar het expired-jwt sjabloon. Als de JWT valideert, gaat de verwerking gewoon door.

Dit sluit de lus bij het overschrijven van het standaard Spring Security CSRF-tokengedrag met een JWT-tokenrepository en validator.

Als u de app start, bladert u naar / jwt-csrf-formulier, wacht iets meer dan 30 seconden en klik op de knop, je ziet zoiets als dit:

7. Uitgebreide JJWT-functies

We sluiten onze JJWT-reis af met een opmerking over enkele functies die verder gaan dan de specificatie.

7.1. Claims afdwingen

Als onderdeel van het ontledingsproces stelt JJWT u in staat om vereiste claims en waarden te specificeren die die claims zouden moeten hebben. Dit is erg handig als er bepaalde informatie in uw JWT's aanwezig moet zijn om ze als geldig te beschouwen. Het vermijdt veel vertakkende logica om claims handmatig te valideren. Dit is de methode die de / parser-enforce eindpunt van ons voorbeeldproject.

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

Regel 5 en 6 tonen u de syntaxis voor zowel geregistreerde claims als aangepaste claims. In dit voorbeeld wordt de JWT als ongeldig beschouwd als de iss-claim niet aanwezig is of niet de waarde heeft: Stormpath. Het is ook ongeldig als de aangepaste hasMotorcycle-claim niet aanwezig is of niet de waarde heeft: true.

Laten we eerst een JWT maken die het gelukkige pad volgt:

http -v POST localhost: 8080 / dynamic-builder-specific \ iss = Stormpath hasMotorcycle: = true sub = msilverman
POST / dynamic-builder-specifieke HTTP / 1.1 Accepteer: 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":, "status "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIn0.qrH-U6TLSVlHkZdYuqPRDtgKNr1RilFYQJtJbcgwhR0" ":" SUCCES "}

Laten we nu valideren dat JWT:

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

Tot nu toe zo goed. Laten we deze keer de hasMotorcycle laten staan:

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

Als we dit keer proberen om de JWT te valideren:

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

we krijgen:

? GET / parser-afdwingen jwt = http -v localhost: 8080 / parser-afdwingen 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": "Verwachte hasMotorcycle-claim is: true, maar was niet aanwezig in de JWT-claims.", "Status": "ERROR"}

Dit geeft aan dat onze claim hasMotorcycle werd verwacht, maar ontbrak.

Laten we nog een voorbeeld doen:

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

Deze keer is de vereiste claim aanwezig, maar deze heeft de verkeerde waarde. Laten we eens kijken naar de output van:

http -v localhost: 8080 / parser-afdwingen JWT = eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjpmYWxzZSwic3ViIjoibXNpbHZlcm1hbiJ9.8LBq2f0eINB34AzhVEgsln_KDo-IyeM8kc-dTzSCr0c?
GET / parser-afdwingen jwt = http -v localhost: 8080 / parser-afdwingen 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": "Verwacht dat hasMotorcycle-claim: waar is, maar was: false.", "status": "ERROR"}

Dit geeft aan dat onze claim hasMotorcycle aanwezig was, maar een waarde had die niet werd verwacht.

MissingClaimException en IncorrectClaimException zijn je vrienden bij het afdwingen van claims in je JWT's en een functie die alleen de JJWT-bibliotheek heeft.

7.2. JWT-compressie

Als je veel claims hebt op een JWT, kan deze groot worden - zo groot dat het in sommige browsers misschien niet in een GET-url past.

Laten we een grote JWT maken:

http -v POST localhost: 8080 / dynamic-builder-specific \ iss = Stormpath hasMotorcycle: = true sub = msilverman the = quick brown = vos sprong = over lui = hond \ ergens = over regenboog = ver omhoog = hoog en = de dromen = je droomde = van

Hier is de JWT die produceert:

eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIiwidGhlIjoicXVpY2siLCJicm93biI6ImZveCIsImp1bXBlZCI6Im92ZXIiLCJsYXp5IjoiZG9nIiwic29tZXdoZXJlIjoib3ZlciIsInJhaW5ib3ciOiJ3YXkiLCJ1cCI6ImhpZ2giLCJhbmQiOiJ0aGUiLCJkcmVhbXMiOiJ5b3UiLCJkcmVhbWVkIjoib2YifQ.AHNJxSTiDw_bWNXcuh-LtPLvSjJqwDvOOUcmkk7CyZA

Die sukkel is groot! Laten we nu een iets ander eindpunt bereiken met dezelfde claims:

http -v POST localhost: 8080 / dynamic-builder-compress \ iss = Stormpath hasMotorcycle: = true sub = msilverman the = quick brown = vos sprong = over lui = hond \ ergens = over regenboog = ver omhoog = hoog en = de dromen = je droomde = van

Deze keer krijgen we:

eyJhbGciOiJIUzI1NiIsImNhbGciOiJERUYifQ.eNpEzkESwjAIBdC7sO4JegdXnoC2tIk2oZLEGB3v7s84jjse_AFe5FOikc5ZLRycHQ3kOJ0Untu8C43ZigyUyoRYSH6_iwWOyGWHKd2Kn6_QZFojvOoDupRwyAIq4vDOzwYtugFJg1QnJv-5SY-TVjQqN7gcKJ3f-J8c-6J-baDFhEN_uGn58XtnpfcHAAD__w.3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE

62 karakters korter! Hier is de code voor de methode die wordt gebruikt om de JWT te genereren:

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

Merk op dat we op regel 6 een compressie-algoritme specificeren om te gebruiken. Dat is alles wat er is.

Hoe zit het met het parseren van gecomprimeerde JWT's? De JJWT-bibliotheek detecteert automatisch de compressie en gebruikt hetzelfde algoritme om te decomprimeren:

GET /parser?jwt=eyJhbGciOiJIUzI1NiIsImNhbGciOiJERUYifQ.eNpEzkESwjAIBdC7sO4JegdXnoC2tIk2oZLEGB3v7s84jjse_AFe5FOikc5ZLRycHQ3kOJ0Untu8C43ZigyUyoRYSH6_iwWOyGWHKd2Kn6_QZFojvOoDupRwyAIq4vDOzwYtugFJg1QnJv-5sY-TVjQqN7gcKJ3f-j8c-6J-baDFhEN_uGn58XtnpfcHAAD__w.3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE HTTP / 1.1 Accepteer: * / * ... HTTP / 1.1 200 OK Cache-Control: no-cache, geen -store, max-age = 0, moet opnieuw valideren 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", "ergens": "over", "sub": "msilverman", "the": "quick", "up": "high"}, "header": {"alg" : "HS256", "calg": "DEF"}, "signature": "3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE"}, "status": "SUCCESS"}

Let op de calg claim in de kop. Dit werd automatisch in de JWT gecodeerd en geeft de parser een hint over welk algoritme moet worden gebruikt voor decompressie.

OPMERKING: De JWE-specificatie ondersteunt compressie. In een aanstaande release van de JJWT-bibliotheek zullen we JWE en gecomprimeerde JWE's ondersteunen. We zullen compressie in andere typen JWT's blijven ondersteunen, ook al is dit niet gespecificeerd.

8. Token Tools voor Java Devs

Hoewel de kern van dit artikel niet Spring Boot of Spring Security was, was het door het gebruik van deze twee technologieën eenvoudig om alle functies te demonstreren die in dit artikel worden besproken. Je zou in staat moeten zijn om de server op te starten en te spelen met de verschillende eindpunten die we hebben besproken. Druk gewoon op:

http // localhost: 8080

Stormpath is ook verheugd om een ​​aantal open source-ontwikkelaarstools naar de Java-gemeenschap te brengen. Waaronder:

8.1. JJWT (waar we het over hebben gehad)

JJWT is een gebruiksvriendelijke tool voor ontwikkelaars om JWT's in Java te maken en te verifiëren. Zoals veel bibliotheken die Stormpath ondersteunt, is JJWT volledig gratis en open source (Apache-licentie, versie 2.0), zodat iedereen kan zien wat het doet en hoe het het doet. Aarzel niet om problemen te melden, verbeteringen voor te stellen en zelfs code in te dienen!

8.2. jsonwebtoken.io en java.jsonwebtoken.io

jsonwebtoken.io is een ontwikkelaarstool die we hebben gemaakt om het decoderen van JWT's gemakkelijk te maken. Plak gewoon een bestaande JWT in het juiste veld om de header, payload en handtekening te decoderen. jsonwebtoken.io wordt mogelijk gemaakt door nJWT, de schoonste gratis en open source (Apache-licentie, versie 2.0) JWT-bibliotheek voor Node.js-ontwikkelaars. U kunt op deze website ook code zien die voor verschillende talen is gegenereerd. De website zelf is open-source en is hier te vinden.

java.jsonwebtoken.io is specifiek voor de JJWT-bibliotheek. U kunt de kopteksten en payload wijzigen in het vak rechtsboven, de JWT bekijken die door JJWT is gegenereerd in het vak linksboven en een voorbeeld van de Java-code voor de bouwer en parser in de onderste vakken bekijken. De website zelf is open source en is hier te vinden.

8.3. JWT-inspecteur

De nieuwe jongen, JWT Inspector, is een open source Chrome-extensie waarmee ontwikkelaars JWT's rechtstreeks in de browser kunnen inspecteren en debuggen. De JWT Inspector zal JWT's op uw site ontdekken (in cookies, lokale / sessie-opslag en headers) en ze gemakkelijk toegankelijk maken via uw navigatiebalk en DevTools-paneel.

9. JWT This Down!

JWT's voegen wat intelligentie toe aan gewone tokens. De mogelijkheid om cryptografisch te ondertekenen en verifiëren, vervaltijden in te bouwen en andere informatie in JWT's te coderen, vormt de basis voor echt stateless sessiebeheer. Dit heeft een grote impact op de mogelijkheid om applicaties te schalen.

Bij Stormpath gebruiken we JWT's voor onder meer OAuth2-tokens, CSRF-tokens en beweringen tussen microservices.

Als je eenmaal JWT's begint te gebruiken, ga je misschien nooit meer terug naar de domme tokens uit het verleden. Heeft u nog vragen? Raak me op @afitnerd op twitter.