Sobrecarregar a autenticação Java com JSON Web Tokens (JWTs)
Preparando-se para criar ou enfrentar uma autenticação segura no seu aplicativo Java? Não tem certeza dos benefícios do uso de tokens (e especificamente tokens da Web JSON), ou como eles devem ser implantados? Estou animado para responder a essas perguntas, e mais, para você neste tutorial!
Antes de mergulharmos em JSON Web Tokens (JWTs) e osJJWT library (criados pelo CTO da Stormpath, Les Hazlewood e mantidos porcommunity of contributors), vamos cobrir alguns princípios básicos.
1. Autenticação vs. Autenticação de Token
O conjunto de protocolos que um aplicativo usa para confirmar a identidade do usuário é autenticação. Os aplicativos tradicionalmente mantêm a identidade por meio de cookies de sessão. Esse paradigma depende do armazenamento no servidor de IDs de sessão, o que força os desenvolvedores a criar um armazenamento de sessão exclusivo e específico do servidor ou implementado como uma camada de armazenamento de sessão completamente separada.
A autenticação por token foi desenvolvida para resolver problemas que os IDs de sessão do lado do servidor não resolviam e não podiam. Assim como a autenticação tradicional, os usuários apresentam credenciais verificáveis, mas agora recebem um conjunto de tokens em vez de um ID da sessão. As credenciais iniciais podem ser o par padrão de nome de usuário / senha, chaves de API ou até tokens de outro serviço. (O recurso de autenticação de chave de API do Stormpath é um exemplo disso.)
1.1. Por que tokens?
Muito simplesmente, o uso de tokens no lugar das IDs de sessão pode reduzir a carga do servidor, otimizar o gerenciamento de permissões e fornecer melhores ferramentas para oferecer suporte a uma infraestrutura distribuída ou baseada em nuvem. No caso da JWT, isso é realizado principalmente pela natureza sem estado desses tipos de tokens (mais sobre isso abaixo).
Os tokens oferecem uma ampla variedade de aplicativos, incluindo: esquemas de proteção de falsificação de solicitação entre sites (CSRF), interaçõesOAuth 2.0, IDs de sessão e (em cookies) como representações de autenticação. Na maioria dos casos, os padrões não especificam um formato específico para tokens. Aqui está um exemplo de umSpring Security CSRF token típico em um formulário HTML:
Se você tentar postar esse formulário sem o token CSRF correto, obterá uma resposta de erro, e isso é a utilidade dos tokens. O exemplo acima é um token "burro". Isso significa que não há significado inerente a ser coletado do próprio token. É também aqui que as JWTs fazem uma grande diferença.
Leitura adicional:
Usando JWT com Spring Security OAuth
Um guia para usar JSON Web Tokens com assinatura simétrica e assimétrica no Spring Security OAuth.
API REST do Spring + OAuth2 + Angular
Aprenda a configurar o OAuth2 para uma API REST Spring e como consumi-lo em um cliente Angular.
OAuth2 para uma API REST do Spring - manipule o token de atualização no AngularJS
Aprendemos como armazenar o token de atualização em um aplicativo cliente AngularJS, como atualizar um token de acesso expirado e como aproveitar o proxy Zuul.
2. O que há em um JWT?
JWTs (pronunciados “jots”) são seqüências seguras para URL, codificadas, assinadas criptograficamente (algumas vezes criptografadas) que podem ser usadas como tokens em uma variedade de aplicativos. Aqui está um exemplo de um JWT sendo usado como um token CSRF:
Nesse caso, você pode ver que o token é muito mais longo do que no nosso exemplo anterior. Assim como vimos antes, se o formulário for enviado sem o token, você receberá uma resposta de erro.
Então, por que JWT?
O token acima é assinado criptograficamente e, portanto, pode ser verificado, fornecendo prova de que não foi adulterado. Além disso, os JWTs são codificados com uma variedade de informações adicionais.
Vejamos a anatomia de um JWT para entender melhor como extraímos toda essa bondade dele. Você deve ter notado que existem três seções distintas separadas por pontos (.
):
Cabeçalho |
eyJhbGciOiJIUzI1NiJ9 |
Carga útil |
eyJqdGkiOiJlNjc4ZjIzMzQ3ZTM0MTBkYjdlNjg3Njc4MjNiMmQ3MCIsImlhdC I6MTQ2NjYzMzMxNywibmJmIjoxNDY2NjMzMzE2JLCE2 |
Assinatura |
rgx_o8VQGuDa2AqCHSgVOD5G68Ld_YYM7N7THmvLIKc |
Cada seção é codificada em URLbase64. Isso garante que ele possa ser usado com segurança em um URL (mais sobre isso posteriormente). Vamos dar uma olhada em cada seção individualmente.
2.1. O cabeçalho
Se você base64 decodificar o cabeçalho, obterá a seguinte string JSON:
{"alg":"HS256"}
2.2. The Payload
Se você decodificar a carga útil, obterá a seguinte sequência JSON (formatada para maior clareza):
{
"jti": "e678f23347e3410db7e68767823b2d70",
"iat": 1466633317,
"nbf": 1466633317,
"exp": 1466636917
}
Dentro da carga, como você pode ver, há várias chaves com valores. Essas chaves são chamadas de “reivindicações” e oJWT specification tem sete delas especificadas como reivindicações “registradas”. Eles são:
iss |
Emissor |
sub |
Sujeito |
aud |
Público |
exp |
Expiração |
nbf |
Não antes |
iat |
Emitido em |
jti |
ID JWT |
Ao criar um JWT, você pode colocar qualquer reivindicação personalizada que desejar. A lista acima representa simplesmente as declarações reservadas na chave usada e no tipo esperado. Nosso CSRF possui um ID JWT, um horário “Emitido em”, um horário “Antes” e um tempo de vencimento. O tempo de expiração é exatamente um minuto após o emitido no momento.
2.3. A assinatura
Finalmente, a seção de assinatura é criada levando o cabeçalho e a carga útil juntos (com o. no meio) e transmiti-lo pelo algoritmo especificado (HMAC usando SHA-256, neste caso) junto com um segredo conhecido. Observe que o segredo éalways uma matriz de bytes e deve ter um comprimento que faça sentido para o algoritmo usado. Abaixo, eu uso uma string codificada em base64 aleatória (para legibilidade) que é convertida em uma matriz de bytes.
É assim no pseudo-código:
computeHMACSHA256(
header + "." + payload,
base64DecodeToByteArray("4pE8z3PBoHjnV1AhvGk+e8h2p+ShZpOnpr8cwHmMh1w=")
)
Desde que você conheça o segredo, você pode gerar a assinatura e comparar seu resultado com a seção de assinatura do JWT para verificar se ele não foi violado. Tecnicamente, um JWT que foi assinado criptograficamente é chamado deJWS. Os JWTs também podem ser criptografados e, então, chamados deJWE. (Na prática, o termo JWT é usado para descrever JWEs e JWSs.)
Isso nos traz de volta aos benefícios do uso de um JWT como nosso token CSRF. Podemos verificar a assinatura e podemos usar as informações codificadas no JWT para confirmar sua validade. Portanto, não apenas a representação da string do JWT precisa corresponder ao que está armazenado no lado do servidor, mas podemos garantir que não expirou simplesmente inspecionando a declaraçãoexp. Isso evita que o servidor mantenha estado adicional.
Bem, nós cobrimos muito terreno aqui. Vamos mergulhar em alguns códigos!
3. Configure o Tutorial JJWT
JJWT (https://github.com/jwtk/jjwt) é uma biblioteca Java que fornece criação e verificação JSON Web Token de ponta a ponta. Para sempre gratuito e de código aberto (Apache License, Versão 2.0), foi projetado com uma interface focada no construtor, ocultando grande parte de sua complexidade.
As operações principais no uso de JJWT envolvem a construção e análise de JWTs. Veremos essas operações a seguir, em seguida, entraremos em alguns recursos estendidos do JJWT e, finalmente, veremos os JWTs em ação como tokens CSRF em um aplicativo Spring Security, Spring Boot.
O código demonstrado nas seções a seguir pode ser encontradohere. Nota: O projeto usa Spring Boot desde o início, pois é fácil interagir com a API que ele expõe.
Para criar o projeto, execute o seguinte:
git clone https://github.com/eugenp/tutorials.git
cd tutorials/jjwt
mvn clean install
Uma das grandes vantagens do Spring Boot é como é fácil iniciar um aplicativo. Para executar o aplicativo JJWT Fun, basta fazer o seguinte:
java -jar target/*.jar
Existem dez pontos de extremidade expostos neste aplicativo de exemplo (eu uso o httpie para interagir com o aplicativo. Pode ser encontradohere.)
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.
Nas seções a seguir, examinaremos cada um desses pontos de extremidade e o código JJWT contido nos manipuladores.
4. Construindo JWTs com JJWT
Por causa dofluent interface do JJWT, a criação do JWT é basicamente um processo de três etapas:
-
A definição das reivindicações internas do token, como Emissor, Assunto, Expiração e ID.
-
A assinatura criptográfica do JWT (tornando-o um JWS).
-
A compactação do JWT em uma string segura para URL, de acordo com as regrasJWT Compact Serialization.
O JWT final será uma cadeia de três partes codificada em base64, assinada com o algoritmo de assinatura especificado e usando a chave fornecida. Após esse ponto, o token está pronto para ser compartilhado com a outra parte.
Aqui está um exemplo do JJWT em ação:
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();
Isso é muito semelhante ao código que está no métodoStaticJWTController.fixedBuilder do projeto de código.
Neste ponto, vale a pena falar sobre alguns antipadrões relacionados a JWTs e assinatura. Se você já viu exemplos de JWT antes, provavelmente já encontrou um destes cenários de antipadrão de assinatura:
-
.signWith( SignatureAlgorithm.HS256, "secret".getBytes("UTF-8") )
-
.signWith( SignatureAlgorithm.HS256, "Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=".getBytes("UTF-8") )
-
.signWith( SignatureAlgorithm.HS512, TextCodec.BASE64.decode("Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=") )
Qualquer um dos algoritmos de assinatura de tipoHS usa uma matriz de bytes. É conveniente para humanos ler para pegar uma string e convertê-la em uma matriz de bytes.
O anti-padrão 1 acima demonstra isso. Isso é problemático porque o segredo é enfraquecido por ser tão curto e não é uma matriz de bytes em sua forma nativa. Portanto, para mantê-lo legível, podemos codificar em base64 a matriz de bytes.
No entanto, o antipadrão 2 acima pega a cadeia codificada em base64 e a converte diretamente em uma matriz de bytes. O que deve ser feito é decodificar a string base64 novamente na matriz de bytes original.
O número 3 acima demonstra isso. Então, por que esse também é um anti-padrão? É uma razão sutil neste caso. Observe que o algoritmo de assinatura é HS512. A matriz de bytes não é o comprimento máximo queHS512 pode suportar, tornando-se um segredo mais fraco do que o que é possível para aquele algoritmo.
O código de exemplo inclui uma classe chamadaSecretService que garante que os segredos da força adequada sejam usados para o algoritmo fornecido. No momento da inicialização do aplicativo, um novo conjunto de segredos é criado para cada um dos algoritmos HS. Existem pontos de extremidade para atualizar os segredos, bem como definir explicitamente os segredos.
Se você tiver o projeto em execução conforme descrito acima, execute o seguinte para que os exemplos JWT abaixo correspondam às respostas do seu projeto.
http POST localhost:8080/set-secrets \
HS256="Yn2kjibddFAWtnPJ2AFlL8WXmohJMCvigQggaEypa5E=" \
HS384="VW96zL+tYlrJLNCQ0j6QPTp+d1q75n/Wa8LVvpWyG8pPZOP6AA5X7XOIlI90sDwx" \
HS512="cd+Pr1js+w2qfT2BoCD+tPcYp9LbjpmhSMEJqUob1mcxZ7+Wmik4AYdjX+DlDjmE4yporzQ9tm7v3z/j+QbdYg=="
Agora, você pode atingir o endpoint/static-builder:
http http://localhost:8080/static-builder
Isso produz um JWT que se parece com isso:
eyJhbGciOiJIUzI1NiJ9.
eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.
kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ
Agora, pressione:
http http://localhost:8080/parser?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ
A resposta tem todas as reivindicações que incluímos quando criamos o JWT.
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"
}
Esta é a operação de análise, que abordaremos na próxima seção.
Agora, vamos chegar a um endpoint que usa declarações como parâmetros e criará um JWT personalizado para nós.
http -v POST localhost:8080/dynamic-builder-general iss=Stormpath sub=msilverman hasMotorcycle:=true
Note: há uma diferença sutil entre a afirmaçãohasMotorcycle e as outras afirmações. httpie assume que os parâmetros JSON são cadeias por padrão. Para enviar JSON bruto usando httpie, use o formulário:= em vez de=. Sem isso, ele enviaria“hasMotorcycle”: “true”, que não é o que queremos.
Aqui está a saída:
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"
}
Vamos dar uma olhada no código que suporta este endpoint:
@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);
}
A linha 2 garante que o JSON recebido seja convertido automaticamente em um mapa Java
Por mais conciso que seja esse código, precisamos de algo mais específico para garantir que as declarações passadas sejam válidas. Usar o método.setClaims(Map<String, Object> claims) é útil quando você já sabe que as declarações representadas no mapa são válidas. É aqui que a segurança de tipo do Java entra na biblioteca JJWT.
Para cada uma das reivindicações registradas definidas na especificação JWT, há um método Java correspondente no JJWT que leva o tipo de especificação correta.
Vamos atingir outro endpoint em nosso exemplo e ver o que acontece:
http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub:=5 hasMotorcycle:=true
Observe que passamos um número inteiro, 5, para a declaração “sub”. Aqui está a saída:
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"
}
Agora, estamos recebendo uma resposta de erro porque o código está impondo o tipo de reivindicações registradas. Nesse caso,sub deve ser uma string. Este é o código que suporta este endpoint:
@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());
}
Assim como antes, o método aceitaMap<String, Object> de declarações como seu parâmetro. No entanto, desta vez, estamos chamando o método específico para cada uma das reivindicações registradas que aplica o tipo.
Um refinamento para isso é tornar a mensagem de erro mais específica. No momento, sabemos apenas que uma de nossas reivindicações não é do tipo correto. Não sabemos qual declaração estava errada ou o que deveria ser. Este é um método que nos dará uma mensagem de erro mais específica. Ele também lida com um bug no código atual.
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);
}
}
A linha 3 verifica se o valor passado é do tipo esperado. Caso contrário, umJwtException é lançado com o erro específico. Vamos dar uma olhada nisso em ação fazendo a mesma chamada que fizemos anteriormente:
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"
}
Agora, temos uma mensagem de erro muito específica nos informando que a declaraçãosub está com erro.
Vamos voltar a esse bug em nosso código. O problema não tem nada a ver com a biblioteca JJWT. O problema é que o mapeador de objetos JSON para Java incorporado ao Spring Boot é muito inteligente para nosso próprio bem.
Se houver um método que aceite um objeto Java, o mapeador JSON converterá automaticamente um número passado que é menor ou igual a 2.147.483.647 em um JavaInteger. Da mesma forma, ele converterá automaticamente um número passado maior que 2.147.483.647 em JavaLong. Para as declaraçõesiat,nbf eexp de um JWT, queremos que nosso teste assegureType passe se o objeto mapeado é um inteiro ou longo. É por isso que temos a cláusula adicional para determinar se o valor transmitido é do tipo correto:
boolean isCorrectType =
expectedType.isInstance(value) ||
expectedType == Long.class && value instanceof Integer;
Se estamos esperando um Long, mas o valor é uma instância de Integer, ainda dizemos que é o tipo correto. Com uma compreensão do que está acontecendo com esta validação, podemos agora integrá-la em nosso métododynamicBuilderSpecific:
@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: em todos os códigos de exemplo nesta seção, os JWTs são assinados com o HMAC usando o algoritmo SHA-256. Isso é para manter os exemplos simples. A biblioteca JJWT suporta 12 algoritmos de assinatura diferentes dos quais você pode tirar proveito em seu próprio código.
5. Análise de JWTs com JJWT
Vimos anteriormente que nosso exemplo de código tem um ponto de extremidade para analisar um JWT. Atingindo este endpoint:
http http://localhost:8080/parser?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIiwibmFtZSI6Ik1pY2FoIFNpbHZlcm1hbiIsInNjb3BlIjoiYWRtaW5zIiwiaWF0IjoxNDY2Nzk2ODIyLCJleHAiOjQ2MjI0NzA0MjJ9.kP0i_RvTAmI8mgpIkDFhRX3XthSdP-eqqFKGcU92ZIQ
produz esta resposta:
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"
}
O métodoparser da classeStaticJWTController se parece com isto:
@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);
}
A linha 4 indica que esperamos que a sequência recebida seja um JWT assinado (um JWS). E estamos usando o mesmo segredo que foi usado para assinar o JWT na análise. A linha 5 analisa as reivindicações do JWT. Internamente, está verificando a assinatura e lançará uma exceção se a assinatura for inválida.
Observe que, neste caso, estamos passandoSigningKeyResolver em vez de uma chave em si. Este é um dos aspectos mais poderosos do JJWT. O cabeçalho do JWT indica o algoritmo usado para assiná-lo. No entanto, precisamos verificar o JWT antes de confiar nele. Parece ser um problema 22. Vejamos o métodoSecretService.getSigningKeyResolver:
private SigningKeyResolver signingKeyResolver = new SigningKeyResolverAdapter() {
@Override
public byte[] resolveSigningKeyBytes(JwsHeader header, Claims claims) {
return TextCodec.BASE64.decode(secrets.get(header.getAlgorithm()));
}
};
Usando o acesso aJwsHeader, posso inspecionar o algoritmo e retornar a matriz de bytes adequada para o segredo que foi usado para assinar o JWT. Agora, o JJWT verificará se o JWT não foi violado com o uso dessa matriz de bytes como chave.
Se eu remover o último caractere do passado no JWT (que faz parte da assinatura), esta é a resposta:
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 na prática: Tokens CSRF Spring Security
Embora o foco deste post não seja o Spring Security, vamos nos aprofundar um pouco aqui para mostrar alguns usos reais da biblioteca JJWT.
Cross Site Request Forgery é uma vulnerabilidade de segurança por meio da qual um site malicioso o leva a enviar solicitações a um site com o qual você estabeleceu confiança. Um dos remédios comuns para isso é implementar umsynchronizer token pattern. Essa abordagem insere um token no formulário da web e o servidor de aplicativos verifica o token recebido em seu repositório para confirmar se está correto. Se o token estiver ausente ou for inválido, o servidor responderá com um erro.
O Spring Security possui o padrão de token do sincronizador incorporado. Melhor ainda, se você estiver usandoSpring Boot and Thymeleaf templates, o token do sincronizador é inserido automaticamente para você.
Por padrão, o token usado pelo Spring Security é um token "burro". É apenas uma série de letras e números. Essa abordagem é ótima e funciona. Nesta seção, aprimoramos a funcionalidade básica usando JWTs como token. Além de verificar se o token enviado é o esperado, validamos o JWT para provar ainda mais que o token não foi violado e para garantir que ele não tenha expirado.
Para começar, vamos configurar o Spring Security usando a configuração Java. Por padrão, todos os caminhos exigem autenticação e todos os pontos de extremidade POST exigem tokens CSRF. Vamos relaxar um pouco para que o que construímos até agora ainda funcione.
@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();
}
}
Estamos fazendo duas coisas aqui. Primeiro, estamos dizendo que os tokens CSRF sãonot necessários ao postar em nossos endpoints da API REST (linha 15). Segundo, estamos dizendo que o acesso não autenticado deve ser permitido para todos os caminhos (linhas 17 a 18).
Vamos confirmar se Spring Security está funcionando da maneira que esperamos. Inicie o aplicativo e pressione este URL no seu navegador:
http://localhost:8080/jwt-csrf-form
Aqui está o modelo de Thymeleaf para esta visualização:
Este é um formulário muito básico que será enviado para o mesmo terminal quando enviado. Observe que não há referência explícita aos tokens de CSRF no formulário. Se você visualizar a fonte, verá algo como:
Essa é toda a confirmação de que você precisa saber que o Spring Security está funcionando e que os modelos do Thymeleaf estão inserindo automaticamente o token CSRF.
Para tornar o valor um JWT, vamos habilitar umCsrfTokenRepository personalizado. Veja como nossa configuração Spring Security muda:
@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();
}
}
Para conectar isso, precisamos de uma configuração que exponha um bean que retorne o repositório de token personalizado. Esta é a configuração:
@Configuration
public class CSRFConfig {
@Autowired
SecretService secretService;
@Bean
@ConditionalOnMissingBean
public CsrfTokenRepository jwtCsrfTokenRepository() {
return new JWTCsrfTokenRepository(secretService.getHS256SecretBytes());
}
}
E, aqui está nosso repositório personalizado (as partes importantes):
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) {
...
}
}
O métodogenerateToken cria um JWT que expira 30 segundos depois de ser criado. Com esse encanamento instalado, podemos iniciar o aplicativo novamente e ver a origem de/jwt-csrf-form.
Agora, o campo oculto fica assim:
Huzzah! Agora, nosso token CSRF é um JWT. Isso não foi muito difícil.
No entanto, isso é apenas metade do quebra-cabeça. Por padrão, Spring Security simplesmente salva o token CSRF e confirma que o token enviado em um formulário da web corresponde ao que foi salvo. Queremos estender a funcionalidade para validar o JWT e garantir que ele não tenha expirado. Para fazer isso, vamos adicionar um filtro. Esta é a aparência de nossa configuração Spring Security agora:
@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();
}
...
}
Na linha 9, adicionamos um filtro e o estamos colocando na cadeia de filtros após oCsrfFilter padrão. Portanto, quando o filtro for atingido, o token JWT (como um todo) já estará confirmado como o valor correto salvo pelo Spring Security.
Aqui está oJwtCsrfValidatorFilter (é privado, pois é uma classe interna de nossa configuração Spring Security):
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);
}
}
Dê uma olhada na linha 23. Estamos analisando o JWT como antes. Nesse caso, se uma exceção for lançada, a solicitação será encaminhada para o modeloexpired-jwt. Se o JWT for validado, o processamento continuará normalmente.
Isso fecha o ciclo de substituição do comportamento padrão do token Spring Security CSRF por um repositório e validador de token JWT.
Se você iniciar o aplicativo, navegue até/jwt-csrf-form, espere um pouco mais de 30 segundos e clique no botão, você verá algo assim:
7. Recursos estendidos do JJWT
Encerraremos nossa jornada JJWT com uma palavra sobre alguns dos recursos que vão além das especificações.
7.1. Aplicar reivindicações
Como parte do processo de análise, o JJWT permite especificar reivindicações e valores requeridos para essas reivindicações. Isso é muito útil se houver determinadas informações em suas JWTs que devem estar presentes para que você as considere válidas. Evita muita lógica de ramificação para validar manualmente reivindicações. Este é o método que atende ao endpoint/parser-enforce do nosso projeto de amostra.
@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);
}
As linhas 5 e 6 mostram a sintaxe para reivindicações registradas e reivindicações personalizadas. Neste exemplo, o JWT será considerado inválido se a declaração iss não estiver presente ou não tiver o valor: Stormpath. Também será inválido se a reivindicação hasMotorcycle personalizada não estiver presente ou não tiver o valor: true.
Vamos primeiro criar um JWT que segue o caminho da felicidade:
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"
}
Agora, vamos validar esse JWT:
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"
}
Por enquanto, tudo bem. Agora, desta vez, vamos deixar o hasMotorcycle de fora:
http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath sub=msilverman
Desta vez, se tentarmos validar o JWT:
http -v localhost:8080/parser-enforce?jwt=eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJzdWIiOiJtc2lsdmVybWFuIn0.YMONlFM1tNgttUYukDRsi9gKIocxdGAOLaJBymaQAWc
Nós temos:
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"
}
Isso indica que nossa reivindicação hasMotorcycle era esperada, mas estava ausente.
Vamos fazer mais um exemplo:
http -v POST localhost:8080/dynamic-builder-specific iss=Stormpath hasMotorcycle:=false sub=msilverman
Desta vez, a reivindicação necessária está presente, mas tem o valor errado. Vamos ver o resultado de:
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"
}
Isso indica que nossa reivindicação hasMotorcycle estava presente, mas tinha um valor que não era esperado.
MissingClaimException eIncorrectClaimException são seus amigos ao impor reivindicações em seus JWTs e um recurso que apenas a biblioteca JJWT possui.
7.2. Compressão JWT
Se você tem muitas reivindicações em um JWT, ele pode ficar grande - tão grande que pode não caber em um URL GET em alguns navegadores.
Vamos fazer um grande 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
Aqui está o JWT que produz:
eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJTdG9ybXBhdGgiLCJoYXNNb3RvcmN5Y2xlIjp0cnVlLCJzdWIiOiJtc2lsdmVybWFuIiwidGhlIjoicXVpY2siLCJicm93biI6ImZveCIsImp1bXBlZCI6Im92ZXIiLCJsYXp5IjoiZG9nIiwic29tZXdoZXJlIjoib3ZlciIsInJhaW5ib3ciOiJ3YXkiLCJ1cCI6ImhpZ2giLCJhbmQiOiJ0aGUiLCJkcmVhbXMiOiJ5b3UiLCJkcmVhbWVkIjoib2YifQ.AHNJxSTiDw_bWNXcuh-LtPLvSjJqwDvOOUcmkk7CyZA
Esse otário é grande! Agora, vamos atingir um ponto final ligeiramente diferente com as mesmas reivindicações:
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
Desta vez, temos:
eyJhbGciOiJIUzI1NiIsImNhbGciOiJERUYifQ.eNpEzkESwjAIBdC7sO4JegdXnoC2tIk2oZLEGB3v7s84jjse_AFe5FOikc5ZLRycHQ3kOJ0Untu8C43ZigyUyoRYSH6_iwWOyGWHKd2Kn6_QZFojvOoDupRwyAIq4vDOzwYtugFJg1QnJv-5sY-TVjQqN7gcKJ3f-j8c-6J-baDFhEN_uGn58XtnpfcHAAD__w.3_wc-2skFBbInk0YAQ96yGWwr8r1xVdbHn-uGPTFuFE
62 caracteres mais curtos! Aqui está o código para o método usado para gerar o 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);
}
Observe na linha 6 que estamos especificando um algoritmo de compactação a ser usado. É tudo o que há para isso.
Que tal analisar JWTs compactados? A biblioteca JJWT detecta automaticamente a compactação e usa o mesmo algoritmo para descompactar:
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"
}
Observe a declaraçãocalg no cabeçalho. Isso foi codificado automaticamente no JWT e fornece a dica para o analisador sobre qual algoritmo usar para descompactação.
Note
|
A especificação JWE oferece suporte à compactação. Em uma próxima versão da biblioteca JJWT, ofereceremos suporte a JWE e JWEs compactados. Continuaremos a oferecer suporte à compactação em outros tipos de JWTs, mesmo que não seja especificado. |
8. Ferramentas de token para Java Devs
Embora o foco principal deste artigo não tenha sido o Spring Boot ou o Spring Security, o uso dessas duas tecnologias facilitou a demonstração de todos os recursos discutidos neste artigo. Você deve ser capaz de iniciar o servidor e começar a brincar com os vários endpoints que discutimos. Apenas aperte:
http http://localhost:8080
Stormpath também está animado para trazer uma série de ferramentas de desenvolvedor de software livre para a comunidade Java. Esses incluem:
8.1. JJWT (o que estamos falando)
JJWT é umtool for developers to create and verify JWTs in Java fácil de usar. Como muitas bibliotecas suportadas pelo Stormpath, o JJWT é totalmente gratuito e de código aberto (Apache License, Versão 2.0), para que todos possam ver o que faz e como faz. Não hesite em relatar problemas, sugerir melhorias e até enviar algum código!
8.2. jsonwebtoken.io and java.jsonwebtoken.io
jsonwebtoken.io é uma ferramenta de desenvolvedor que criamos para facilitar a decodificação de JWTs. Basta colar um JWT existente no campo apropriado para decodificar seu cabeçalho, carga útil e assinatura. jsonwebtoken.io is powered by nJWT, the cleanest free and open source (Apache License, Version 2.0) JWT library for Node.js developers. Você também pode ver o código gerado para vários idiomas neste site. O próprio site é de código aberto e pode ser encontradohere.
java.jsonwebtoken.io é especificamente para a biblioteca JJWT. Você pode alterar os cabeçalhos e a carga útil na caixa superior direita, ver o JWT gerado pelo JJWT na caixa superior esquerda e ver uma amostra do código Java do construtor e analisador nas caixas inferiores. O próprio site é de código aberto e pode ser encontradohere.
8.3. JWT Inspector
O novo garoto no bloco,JWT Inspector é uma extensão de código aberto do Chrome que permite aos desenvolvedores inspecionar e depurar JWTs diretamente no navegador. O JWT Inspector descobrirá JWTs em seu site (em cookies, armazenamento local / sessão e cabeçalhos) e os tornará facilmente acessíveis através da barra de navegação e do painel do DevTools.
9. JWT isto para baixo!
Os JWTs adicionam alguma inteligência aos tokens comuns. A capacidade de assinar e verificar criptograficamente, aumentar os tempos de expiração e codificar outras informações nos JWTs prepara o terreno para o gerenciamento de sessões verdadeiramente sem estado. Isso tem um grande impacto na capacidade de dimensionar aplicativos.
No Stormpath, usamos JWTs para tokens OAuth2, tokens CSRF e asserções entre microsserviços, entre outros usos.
Depois de começar a usar JWTs, você nunca poderá voltar aos tokens idiotas do passado. Tem alguma pergunta? Fale comigo em@afitnerd no twitter.