Por que eu devo ler este artigo:Este artigo apresenta um novo padrão, denominado JSON Web Token (JWT), para a criação de tokens capazes de transportar informações de uma forma compacta e confiável. Além disso, aborda também uma maneira de utilizar esses tokens como uma alternativa stateless, de forma semelhante a utilizada por gigantes como Google e Microsoft, aos processos existentes de autenticação para APIs RESTful. No artigo, será analisada a implementação de uma API RESTful com segurança via JWT, baseada no framework Spring Security, para exemplificar todos os conceitos.

Segurança é um requisito fundamental em grande parte dos sistemas atuais, pois é necessário garantir que cada usuário possua acesso somente às funcionalidades que se adequem às suas competências, visando manter a integridade do sistema e das informações armazenadas.

Para aplicações web, a utilização da forma tradicional de segurança, que mantém, após uma autenticação, todas as autorizações de um usuário em sua sessão, geralmente é adequada. Serviços RESTful, por outro lado, são, por definição, stateless. Isso elimina essa forma como opção e nos obriga a buscar alternativas. Atualmente, duas das alternativas mais conhecidas são:

JWT: Web services seguros em Java
  • HTTP Basic Authentication(BA) – Nesse tipo de autenticação, as credencias de acesso (username/password) são enviadas, no header Authorization, em todas as requisições. Elas devem seguir o formato “username:password”, serem codificadas com base64 e precedidas por “Basic ”. Por exemplo: para o username “Alladin” com password “open sesame”, o header seria "Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==". Deve-se notar que, por conta de suas características, o sistema cliente deve guardar as credenciais do usuário para evitar solicitá-las em cada requisição, o que pode representar uma nova vulnerabilidade;
    Nota: O padrão para o header Authorization foi apresentado pela W3C na definição do HTTP 1.0 (RFC 1945) e segue o formato "Authorization: <type> <credentials>", onde <type> indica o tipo de autenticação.
  • Token Based Authentication – Nesse tipo de autenticação, o cliente realiza um login passando as credenciais do usuário e recebe um token, que possui uma assinatura que inviabiliza sua adulteração. As requisições subsequentes adicionam o token a um header e as informações contidas nele garantem acesso aos serviços desejados.

Neste artigo, focaremos em um tipo específico de Token Based Authentication, que utiliza um novo padrão de token, denominado JSON Web Token (JWT). Além de conhecer esse novo tipo de token, veremos como utilizá-lo em conjunto com o framework Spring Security para proteger uma API RESTful.

JWT

JSON Web Token, ou JWT, é um padrão aberto (RFC 7519), de uso geral, que possibilita a troca segura de informações entre partes, na forma de objetos JSON. Como vantagens, o JWT possui o fato de ser mais compacto que alternativas baseadas em XML e a capacidade de ser autocontido, ou seja, possuir todas as informações relevantes sobre um assunto, dispensando consultas adicionais a um eventual banco de dados para recuperar os dados.

Um JWT é composto por três partes: header, payload e signature (assinatura); todas codificadas com base64 (vide BOX 1) e separadas por pontos (.). A seguir, analisaremos cada uma delas em detalhes.

BOX 1. Base64
Base64 é um método para codificação de dados para transferência na internet definido pelo padrão RFC 4648, que, como seu nome indica, utiliza apenas 64 caracteres ([A-Z], [a-z],[0-9], "/" e "+").

O método consiste em, primeiramente, transformar o texto original em um número binário. Essa transformação leva em consideração a codificação original do texto (ASCII). Após isso, o número binário resultante é convertido, por meio de uma tabela, para base64. O texto em ASCII "teste", por exemplo, corresponde ao número binário "01110100 01100101 01110011 01110100 01100101", que, por sua vez, pode ser codificado em base64 como "dGVzdGU=".

Header

O header (cabeçalho) de um JWT é um JSON e, geralmente, contém o algoritmo de hashing utilizado na assinatura e o tipo de token (JWT).

A seguir é apresentado um exemplo de header não codificado:

  {
    "alg": "HS256",
    "typ": "JWT"
  }

Mesmo header codificado com base64:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

Payload

O payload (ou corpo) de um JWT é também um JSON e contém as informações relevantes ao assunto, denominadas claims, na forma de pares chave/valor.

Existem três tipos de claims, a saber:

  • Registradas – Um conjunto de claims cujo uso não é obrigatório, mas recomendado. São elas: iss (Issuer), identificador de quem gerou o JWT; sub (Subject), identificador único que representa o assunto do JWT; aud (Audience), identifica o recipiente do JWT; exp (Expiration Time), data/hora de expiração do JWT; nbf (Not Before), data/hora de ativação do JWT, a partir da qual ele será válido; iat (Issued At), data/hora de geração do JWT; e jti (JWT Id), identificador único do JWT. Aqui, é interessante notar que, para manter o token compacto, os nomes das claims possuem apenas três caracteres;
  • Públicas Claims criadas por usuários de JWTs. Para evitar colisões, os nomes dessas claims devem ser registrados na IANA ou definidos como URIs que contenham um namespace resistente a colisão (como um nome de domínio, por exemplo);
    Nota: Uma colisão ocorre quando um mesmo nome pode possuir diferentes significados/usos. Isso gera problemas de comunicação, fazendo com que informações sejam interpretadas incorretamente quando transferidas para diferentes sistemas ou módulos.
  • Privadas Claims criadas com nomes não registrados na IANA (vide BOX 2) e não resistentes a colisão. Devem ser utilizadas com cuidado, em comum acordo entre as partes envolvidas (consumidores e produtores).
BOX 2. IANA
A IANA (Internet Assigned Numbers Authority) é uma organização privada, sem fins lucrativos, responsável pela atribuição de nomes e números globalmente únicos, utilizados em padrões técnicos que regem a Internet. Suas atividades podem ser agrupadas em três categorias: nomes de domínio, recursos numéricos e atribuições de protocolos.

Um exemplo de payload, contendo claims registradas e privadas, é apresentado na Listagem 1.

Listagem 1. Exemplo de payload com claims registradas e privadas.

  {
    "adm": "true",
    "app": "Postal",
    "iss": "br.com.javamagazine",
    "sub": "Administrator",
    "exp": 1477781160
  }
Nota: É importante mencionar que, geralmente, as informações contidas no payload não são criptografadas. Nada impede, no entanto, de se proteger informações sigilosas. Isso, contudo, pode aumentar a quantidade de processamento necessária para processar o token, causando problemas de desempenho.

Assinatura

A assinatura serve para identificar quem enviou o JWT e validar se ele não foi modificado no trajeto. Diversos algoritmos de chave simétrica e assimétrica (vide BOX 3) estão disponíveis para assinar um JWT, por exemplo: HMAC-SHA256, HMAC-SHA512, RSA-SHA256, RSA-SHA512, ECDSA-SHA256, ECDSA-SHA-512, entre outros. A escolha do algoritmo adequado depende das necessidades de segurança da aplicação desenvolvida.

Os algoritmos de codificação utilizam como entradas:

  • O header codificado com base64 concatenado com o payload, também codificado com base64;
  • Uma chave, definida pelo desenvolvedor do sistema. Chaves, idealmente, devem possuir uma quantidade de bits, gerados aleatoriamente, igual ou superior à do algoritmo escolhido. Um exemplo de chave, em hexadecimal, para um algoritmo de 256 bits, como o HMAC-SHA256, seria:
    "620FD22D31267AFF7D788833E311DA62C1A6CEE8F6A5FB77307B65F2EB2890B7"

Em resumo, o pseudocódigo do processo de codificação se pareceria com a seguinte estrutura:

  AlgoritoCodificacao(
    base64UrlEncoder(header) + '.' +
    base64UrlEncoder(payload),
    privateKey
  )

O processo de validação, no servidor, consiste em gerar uma nova assinatura com o header e o payload recebidos e a comparar com a assinatura original. Qualquer alteração no conteúdo do header ou do payload de um JWT o tornará inválido, pois isso fará com que a nova assinatura gerada seja diferente da original. Todo esse processo de validação pode ser mais facilmente visualizado no diagrama apresentado na Figura 1.

Processo de validação

Figura 1. Processo de validação
BOX 3. Criptografia simétrica e assimétrica
Algoritmos de criptografia de chave simétrica utilizam uma chave secreta para codificar e decodificar um conteúdo. Esse tipo de algoritmo não é eficaz quando há a necessidade de troca de informações, ou seja, quando o responsável por decodificar um determinado conteúdo não é o mesmo que o codificou. Isso ocorre porque, nesses casos, se faz necessário compartilhar a chave secreta, o que acrescenta uma nova vulnerabilidade ao processo.

Algoritmos de criptografia de chave assimétrica, por outro lado, utilizam dois tipos de chave, denominadas privada e pública, para codificar ou decodificar um conteúdo. Como forma de garantir a origem de uma informação, utiliza-se a chave privada para codificação e a pública para decodificação. Com essa configuração, é impossível codificar um conteúdo apenas com a chave pública, que pode ser distribuída livremente entre os receptores da informação.

Segurança com JWT

Por conta de suas características, o JWT é especialmente interessante para uma implementação de Token Based Authentication. Sua capacidade de ser autocontido permite transmitir todas as informações relevantes aos processos de autorização e autenticação. Isso, aliado ao seu formato compacto, faz com que seja possível realizar uma autenticação segura, completamente stateless, com uma carga mínima em cada requisição e sem expor qualquer informação privada do usuário.

Um efeito colateral positivo causado pelo fato do JWT ser autocontido consiste na possibilidade de gerá-lo em uma aplicação e utilizá-lo para autenticação em diversas outras. Essa característica faz com que o JWT seja frequentemente utilizado em OAuth (vide BOX 4).

BOX 4. OAuth
OAuth é um padrão aberto para autorização que permite a um usuário, denominado Resource Owner, compartilhar recursos de uma aplicação, denominada Resource Server, com uma terceira aplicação, denominada Client, sem expor suas credenciais.

O fluxo para uma implementação OAuth 2.0, como ilustrado na Figura 2, se inicia com o Client se registrando no Resource Server, fornecendo dados básicos e uma URI de redirecionamento, e recebendo um Client Id e um Client Secret. Logo após, quando um Resource Owner acessa o Client, ele é direcionado ao Resource Server, que solicita uma autorização para compartilhar determinados recursos. Uma vez que o Resource Owner aceita, o Resource Server fornece um token de acesso ao Client, por meio da URI de redirecionamento, com o qual ele tem acesso aos recursos solicitados para, entre outras coisas, autenticar o Resource Owner. O processo de logar com uma conta Google em uma aplicação, por exemplo, ilustra bem o fluxo do OAuth 2.0.

Fluxo do OAuth 2.0
Figura 2. Fluxo do OAuth 2.0.
Nota: Um header de Authorization que contém um JWT geralmente segue o formato "Authorization: Bearer <token>", o mesmo utilizado pelo OAuth 2.0.

JWT com Spring Security

Nesta seção criaremos uma API RESTful com segurança via JWT, utilizando como base o framework Spring Security. O código-fonte completo do exemplo se encontra na área de downloads desta edição. Durante esse processo de construção, será assumido que o leitor possui conhecimento prévio em criação de APIs RESTful com o framework Spring MVC e configuração de autenticação/autorização convencional utilizando o framework Spring Security.

Inicialmente, vamos configurar o projeto com o Apache Maven. Para isso, especificaremos as dependências necessárias no arquivo pom.xml, apresentado na Listagem 2. Eis uma pequena descrição das principais dependências:

  • Linhas 20 a 24 - O framework Spring Web MVC facilita a criação de APIs REST;
  • Linhas 25 a 34 - O framework Spring Security cuida das funcionalidades de autenticação e autorização e, com algumas modificações, serve como base para nossa implementação de segurança via JWT;
  • Linhas 35 a 39 - A serialização/desserialização JSON fica por conta da biblioteca Jackson;
  • Linhas 40 a 44 - A biblioteca JJWT é responsável por realizar a criação, parsing e validação dos tokens.

Listagem 2. Código do arquivo pom.xml.

  
   <project xmlns="http://maven.apache.org/POM/4.0.0" 
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
      http://maven.apache.org/maven-v4_0_0.xsd">
     <modelVersion>4.0.0</modelVersion>
     <groupId>br.gov.sp.fatec</groupId>
     <artifactId>SpringRestSecurityJwt</artifactId>
     <packaging>war</packaging>
     <version>1.0-SNAPSHOT</version>
     <name>SpringRestSecurityJwt Maven Webapp</name>
     <url>http://maven.apache.org</url>
     <properties>
       <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
       <org.springframework.version>4.2.6.RELEASE</org.springframework.version>
       <org.springframework.security.version>4.0.3.RELEASE
         </org.springframework.security.version>
       <jackson.version>2.4.1</jackson.version>
       <jjwt.version>0.6.0</jjwt.version>
       <javax.servlet.version>2.5</javax.servlet.version>
     </properties>
     <dependencies>
       <dependency>
         <groupId>org.springframework</groupId>
         <artifactId>spring-webmvc</artifactId>
         <version>${org.springframework.version}</version>
       </dependency>
       <dependency>
         <groupId>org.springframework.security</groupId>
         <artifactId>spring-security-web</artifactId>
         <version>${org.springframework.security.version}</version>
       </dependency>
       <dependency>
         <groupId>org.springframework.security</groupId>
         <artifactId>spring-security-config</artifactId>
         <version>${org.springframework.security.version}</version>
       </dependency>
       <dependency>
         <groupId>com.fasterxml.jackson.core</groupId>
         <artifactId>jackson-databind</artifactId>
         <version>${jackson.version}</version>
       </dependency>
       <dependency>
         <groupId>io.jsonwebtoken</groupId>
         <artifactId>jjwt</artifactId>
         <version>${jjwt.version}</version>
       </dependency>
       <dependency>
         <groupId>javax.servlet</groupId>
         <artifactId>servlet-api</artifactId>
         <version>${javax.servlet.version}</version>
         <scope>provided</scope>
       </dependency>
     </dependencies>
     <build>
       <finalName>SpringRestSecurityJwt</finalName>
       <plugins>
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-compiler-plugin</artifactId>
           <configuration>
             <source>1.7</source>
             <target>1.7</target>
           </configuration>
         </plugin>
       </plugins>
     </build>
   </project>

A Figura 3 apresenta a estrutura desejada para nossa aplicação. O fluxo de acesso padrão ocorre quando o usuário realiza um login bem-sucedido por meio de uma API pública (1), recebe um JWT e passa a utilizá-lo nas requisições seguintes que envolvam serviços privados. Para cada uma dessas requisições, o filtro JWT (2) valida o token e insere os dados do usuário no contexto de segurança. A partir daí o filtro do Spring Security (3) realiza o processo de autorização, verificando se o usuário tem acesso ao recurso desejado. Nas próximas seções detalharemos cada um desses componentes, assim como as configurações necessárias para interligá-los.

Estrutura da aplicação
Figura 3. Estrutura da aplicação.

Configuração

Iniciemos, então, com a configuração do sistema. Para isso, utilizaremos três arquivos: web.xml, para a configuração geral da aplicação web; applicationContext.xml, para a configuração dos elementos do framework Spring; e applicationContext-security.xml, para a configuração do framework Spring Security.

No arquivo web.xml, apresentado na Listagem 3, configuramos o servlet do Spring MVC (linhas 6 a 19), indicando que os arquivos de configuração de contexto do Spring seguem o padrão "/WEB-INF/applicationContext*.xml" (linha 11), e o filtro do Spring Security (linhas 21 a 33), indicando que qualquer requisição, antes de ser processada, deve passar por sua verificação (linha 30).

Listagem 3. Código do arquivo web.xml.

  
 <!DOCTYPE xml>
 <web-app xmlns="http://java.sun.com/xml/ns/javaee" version="2.5">
   <display-name>Spring Rest Security</display-name>
 
   <!-- Add Support for Spring -->
   <servlet>
     <servlet-name>spring</servlet-name>
     <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
     <init-param>
       <param-name>contextConfigLocation</param-name>
       <param-value>/WEB-INF/applicationContext*.xml</param-value>
     </init-param>
     <load-on-startup>1</load-on-startup>
   </servlet>
 
   <servlet-mapping>
     <servlet-name>spring</servlet-name>
     <url-pattern>/</url-pattern>
   </servlet-mapping>
 
   <!-- Spring Security -->
   <filter>
     <filter-name>springSecurityFilterChain</filter-name>
     <filter-class>
       org.springframework.web.filter.DelegatingFilterProxy
     </filter-class>
   </filter>
   <filter-mapping>
     <filter-name>springSecurityFilterChain</filter-name>
     <url-pattern>/*</url-pattern>
     <dispatcher>FORWARD</dispatcher>
     <dispatcher>REQUEST</dispatcher>
   </filter-mapping>
 
 </web-app> 

No arquivo applicationContext.xml, apresentado na Listagem 4, indicamos que o framework Spring deve procurar por classes anotadas a partir do pacote br.com.javamazine (linha 13). Além disso, habilitamos o uso de anotações do framework Spring MVC (linha 15).

Listagem 4. Código do arquivo applicationContext.xml.

  
 <?xml version="1.0" encoding="UTF-8"?>
 <beans xmlns="http://www.springframework.org/schema/beans"
   xmlns:context="http://www.springframework.org/schema/context"
   xmlns:mvc="http://www.springframework.org/schema/mvc" 
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
   xsi:schemaLocation="http://www.springframework.org/schema/beans 
     http://www.springframework.org/schema/beans/spring-beans.xsd
     http://www.springframework.org/schema/context 
     http://www.springframework.org/schema/context/spring-context.xsd
     http://www.springframework.org/schema/mvc 
     http://www.springframework.org/schema/mvc/spring-mvc.xsd">
 
   <context:component-scan base-package="br.com.javamagazine" />
 
   <mvc:annotation-driven />
 
 </beans>

Finalmente, no arquivo applicationContext-security.xml, apresentado na Listagem 5, habilitamos o uso das anotações @PreAuthorize e @PostAuthorize (linha 10); definimos um entry point customizado (linha 12); indicamos que a autenticação será stateless (linha 13), ou seja, que os dados do usuário autenticado não permanecerão na sessão; desabilitamos a proteção contra CSRF (linha 14), pois o JWT já previne esse tipo de fraude; incluímos um filtro customizado para tratar os JWTs antes do filtro correspondente ao form de login (linha 15); definimos um bean para nosso filtro customizado (linhas 18 a 20); e configuramos um Authentication Manager (linhas 22 a 26), que utiliza o bean segurancaService como um Authentication Provider customizado (linha 23) para recuperar informações de usuários, cujas senhas não se encontram codificadas (linha 24).

Listagem 5. Arquivo applicationContext-security.xml.

  
 <?xml version="1.0" encoding="UTF-8"?>
 <beans:beans xmlns="http://www.springframework.org/schema/security"
   xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
   xmlns:beans="http://www.springframework.org/schema/beans"
   xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
     http://www.springframework.org/schema/security
     http://www.springframework.org/schema/security/spring-security.xsd">
 
   <global-method-security pre-post-annotations="enabled" />
 
   <http entry-point-ref="restAuthenticationEntryPoint"
     create-session="stateless">
     <csrf disabled="true" />
     <custom-filter before="FORM_LOGIN_FILTER" ref="jwtAuthenticationFilter" />
   </http>
 
   <beans:bean id="jwtAuthenticationFilter"
     class="br.com.javamagazine.security.JwtAuthenticationFilter">
   </beans:bean>
 
   <authentication-manager alias="authenticationManager">
     <authentication-provider user-service-ref="segurancaService">
       <password-encoder hash="plaintext"></password-encoder>
     </authentication-provider>
   </authentication-manager>
 
 </beans:beans>

Nas próximas seções analisaremos cada um dos elementos configurados.

Entry Point

O Entry Point é responsável por definir que ação tomar quando ocorre um acesso de usuário não autenticado (que não realizou login). O comportamento normal é redirecionar para a página de login, contudo, por se tratarem de APIs REST, esse comportamento seria indesejado. Tendo isso em vista, definimos um Entry Point customizado, apresentado na Listagem 6, que sempre retorna um erro 401 — UNAUTHORIZED (linha 19) para qualquer tipo de erro de autenticação. Caso seja necessário um tratamento mais elaborado, dependente do erro, os dados da requisição recusada e do erro se encontram disponíveis nos parâmetros request e authException, respectivamente (linhas 16 e 17).

Listagem 6. Código do Entry Point (RestAuthenticationEntryPoint).

  
 package br.com.javamagazine.security;
 
 import java.io.IOException;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import org.springframework.security.core.AuthenticationException;
 import org.springframework.security.web.AuthenticationEntryPoint;
 import org.springframework.stereotype.Component;
 
 @Component
 public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
 
     @Override
     public void commence(HttpServletRequest request,
             HttpServletResponse response, AuthenticationException authException)
             throws IOException {
         response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
     }
 }

Authentication Provider

Para que o Spring Security possa gerenciar os processos de autenticação e autorização, precisamos definir como gerenciar usuários e suas permissões. Para isso, utilizaremos as classes User e Authority, que implementam, respectivamente, as interfaces UserDetails e GrantedAuthority do Spring Security. A Figura 4 apresenta um diagrama com ambas as classes.

Diagrama
de classes
Figura 4. Diagrama de classes

Nosso Authentication Provider customizado precisa implementar a interface UserDetailsService do Spring Security. Essa interface possui um único método, denominado loadUserByUserName(), que recebe como parâmetro o nome de usuário informado durante o login e deve retornar uma implementação da interface UserDetails contendo seus dados e suas autorizações. Idealmente, buscaríamos tais informações no banco de dados, mas como esse não é o foco desse artigo, utilizaremos uma abordagem simplificada. A Listagem 7 apresenta nossa implementação, que sempre retorna um User com username e password idênticos ao parâmetro recebido. Se o parâmetro possuir valor admin, a Authority associada a esse User será ROLE_ADMIN; caso contrário a Authority será ROLE_USER.

Listagem 7. Código do Authentication Provider customizado.

  
 package br.com.javamagazine.service;
 
 import java.util.ArrayList;
 
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.security.core.userdetails.UserDetailsService;
 import org.springframework.security.core.userdetails.UsernameNotFoundException;
 import org.springframework.stereotype.Service;
 
 import br.com.javamagazine.model.Authority;
 import br.com.javamagazine.model.User;
 
 @Service("segurancaService")
 public class SegurancaServiceImpl implements UserDetailsService {
 
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
          if(username == null) {
                 throw new UsernameNotFoundException(username);
          }
          Authority authority = new Authority();
          if(username.equals("admin")) {
                 authority.setAuthority("ROLE_ADMIN");
          }
          else {
                 authority.setAuthority("ROLE_USER");
          }
          User user = new User();
          user.setUsername(username);
          user.setPassword(username);
          user.setAuthorities(new ArrayList<Authority>());
          user.getAuthorities().add(authority);
          return user;
    }
 
 }

Até este ponto, realizamos apenas customizações básicas no Spring Security. Os próximos elementos, entretanto, são exclusivos do JWT e fogem do fluxo usual de autenticação/autorização, no qual, após uma autenticação (login) bem-sucedida, todos os acessos subsequentes utilizam as informações de autorização armazenadas na sessão.

Geração e parsing de tokens

Para gerar os JWTs durante o processo de autenticação e realizar o parsing deles durante as requisições, foi criada uma classe utilitária, denominada JwtUtils, cujo código se encontra na Listagem 8. Nesse projeto, utilizaremos um algoritmo de chave simétrica (HMAC SHA256) para gerar a assinatura. A chave privada se encontra definida na linha 20. Em projetos reais seria recomendável manter essa chave em um arquivo de configuração e gerá-la utilizando as melhores práticas (idealmente, ela é formada por caracteres aleatórios, com tamanho mínimo de 256 bits). Ao analisar a classe JwtUtils, são de especial interesse os métodos generateToken() e parseToken(), responsáveis por gerar e realizar o parsing dos tokens, respectivamente.

Listagem 8. Código da classe utilitária JwtUtils.

  
 package br.com.javamagazine.security;
 
 import io.jsonwebtoken.Jwts;
 import io.jsonwebtoken.SignatureAlgorithm;
 
 import java.io.IOException;
 import java.util.Date;
 
 import org.springframework.security.core.userdetails.UserDetails;
 
 import br.com.javamagazine.model.User;
 
 import com.fasterxml.jackson.core.JsonParseException;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonMappingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
 public class JwtUtils {
 
     private static final String secretKey = "j4v4_s3cr3t";
 
     public static String generateToken(User user)
             throws JsonProcessingException {
         final Long hora = 1000L * 60L * 60L;
         ObjectMapper mapper = new ObjectMapper();
         String userJson = mapper.writeValueAsString(user);
         Date agora = new Date();
         return Jwts.builder().claim("usr", userJson)
                 .setIssuer("br.com.javamagazine")
                 .setSubject(user.getUsername())
                 .setExpiration(new Date(agora.getTime() + hora))
                 .signWith(SignatureAlgorithm.HS256, secretKey).compact();
     }
 
     public static UserDetails parseToken(String token)
             throws JsonParseException, JsonMappingException, IOException {
         ObjectMapper mapper = new ObjectMapper();
         String userJson = Jwts.parser().setSigningKey(secretKey)
                 .parseClaimsJws(token).getBody().get("usr", String.class);
         return mapper.readValue(userJson, User.class);
     }
 
 }

O método generateToken() utiliza a biblioteca JJWT para gerar um token (linhas 28 a 32). Em sua construção, optamos por utilizar as claims reservadas iss (linha 29), sub (linha 30) e exp (linha 31). A claim exp, em especial, garante que a validade de nosso token é de apenas 1 hora após sua criação. O grande diferencial de nosso token, entretanto, se encontra em nossa claim privada usr (linha 28), na qual inserimos a serialização (realizada pelo Jackson nas linhas 25 e 26) de um objeto do tipo User, contendo todas as características e autorizações do usuário autenticado. Essa claim permite transmitirmos, por meio do token, todas as informações necessárias para autenticar o usuário em requisições futuras, sem necessidade de qualquer acesso a um Authentication Manager e, consequentemente, a um eventual banco de dados, refletindo o caráter autocontido de um JWT. Finalmente, geramos a assinatura com o algoritmo HMAC SHA256 e a compactamos em base64 (linha 32).

O método parseToken(), por sua vez, utiliza a biblioteca JJWT para realizar o procedimento inverso, ou seja, validar um token e recuperar as informações do usuário de seu corpo (payload). Para tanto, ele faz uso da chave secreta para validar o token (linha 38). Após isso, ele recupera a informação desejada informando a chave da claim (usr) e seu tipo (String). Durante esse processo, exceptions podem ser geradas se o token for incorreto ou se a data/hora atual for superior à de sua expiração. Finalmente, utilizamos o Jackson para desserializar o JSON recuperado em um objeto do tipo User (linha 40).

Nota: A JJWT é uma biblioteca Java que utiliza uma interface fluída para construção e parsing de tokens. Segundo o site oficial do padrão, ela é capaz de validar todas as claims reservadas e suportar todos os algoritmos de criptografia disponíveis. Dentro de sua estrutura para encadeamento de métodos, os processos de construção e parsing são iniciados pela chamada aos métodos builder() e parser(), respectivamente.

Login

Devido ao fato de necessitarmos gerar e enviar um JWT quando ocorre uma autenticação bem-sucedida, iremos construir um mecanismo de login customizado, disponibilizado por meio da URL "/login", conforme apresenta a Listagem 9. Esse serviço deve ser de acesso público, sem qualquer segurança, para possibilitar o acesso de usuários não autenticados.

Como o Authentication Manager do Spring Security implementa toda a lógica de autenticação de usuários, vamos utilizá-lo (injeção nas linhas 25 e 26), ao invés de reescrever todo o processo.

Nosso serviço de login (linhas 33 a 42) receberá um objeto do tipo Login (linha 34) contendo o username e o password e repassará esses dados para o Authentication Manager para autenticação. Em caso de sucesso, será recuperado do Principal um objeto do tipo User, contendo as características e autorizações do usuário autenticado (linha 38). Com base nessas informações recuperadas, com exceção do password, geramos um JWT e o carregamos no header Token da resposta (linhas 39 e 40). Adicionalmente, retornamos também o objeto User (linha 41) para utilização no front-end (mostrar o nome do usuário logado, por exemplo).

Nota: No contexto de segurança, Principal é um objeto identificador que representa o usuário autenticado. Em Java, corresponde a uma implementação da interface java.security.Principal.

Listagem 9. Código do mecanismo de login.

  
 package br.com.javamagazine.controller;
 
 import javax.servlet.http.HttpServletResponse;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.security.authentication.AuthenticationManager;
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 import br.com.javamagazine.model.User;
 import br.com.javamagazine.security.JwtUtils;
 import br.com.javamagazine.security.Login;
 
 import com.fasterxml.jackson.core.JsonProcessingException;
 
 @RestController
 @RequestMapping(value = "/login")
 public class LoginController {
 
     @Autowired
     @Qualifier("authenticationManager")
     private AuthenticationManager auth;
 
     public void setAuth(AuthenticationManager auth) {
         this.auth = auth;
     }
 
     @RequestMapping(path = "")
     public UserDetails login(@RequestBody Login login,
             HttpServletResponse response) throws JsonProcessingException {
         Authentication credentials = new UsernamePasswordAuthenticationToken(
                 login.getUsername(), login.getPassword());
         User user = (User) auth.authenticate(credentials).getPrincipal();
         user.setPassword(null);
         response.setHeader("Token", JwtUtils.generateToken(user));
         return user;
     }
 
 }

Filtro JWT

Precisamos agora incluir em nosso projeto um elemento capaz de utilizar o JWT, enviado em cada requisição, para realizar o processo de autenticação, liberando acesso aos recursos protegidos. Para isso, criaremos um filtro, JwtAuthenticationFilter, apresentado na Listagem 10. Nele, nós tentamos recuperar um JWT do header Authorization (linhas 27 e 28). Caso ele exista, realizamos a validação e o parsing (linha 30), criamos um objeto Authentication com as informações recuperadas (linhas 31 a 35) e o carregamos no contexto de segurança (linha 36), efetivamente autenticando o usuário para essa requisição. Caso alguma exception ocorra durante a validação do JWT (validade expirada, assinatura inválida, etc.), retornamos um erro 401 - UNAUTHORIZED.

Nosso filtro aceita headers Authorization no formato OAuth 2.0 (com prefixo "Bearer ") ou que contenham apenas o JWT. Por esse motivo, na linha 30, eliminamos o texto "Bearer ", se existir, antes de processar o token. Podemos verificar também que utilizamos apenas as informações contidas no JWT, sem necessidade de acessar qualquer outro recurso para realizar a autenticação do usuário.

Listagem 10. Código do filtro JWT, JwtAuthenticationFilter.

  
 package br.com.javamagazine.security;
 
 import java.io.IOException;
 
 import javax.servlet.FilterChain;
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
 import org.springframework.security.core.Authentication;
 import org.springframework.security.core.context.SecurityContextHolder;
 import org.springframework.security.core.userdetails.UserDetails;
 import org.springframework.web.filter.GenericFilterBean;
 
 public class JwtAuthenticationFilter extends GenericFilterBean {
 
     private String tokenHeader = "Authorization";
 
     @Override
     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) 
             throws IOException, ServletException {
 
         try {
             HttpServletRequest servletRequest = (HttpServletRequest) request;
             String authorization = servletRequest.getHeader(tokenHeader);
             if (authorization != null) {
                 UserDetails user = JwtUtils.parseToken(authorization.replaceAll("Bearer ", ""));
                 Authentication credentials = 
                         new UsernamePasswordAuthenticationToken(
                                 user.getUsername(), 
                                 user.getPassword(), 
                                 user.getAuthorities());
                 SecurityContextHolder.getContext().setAuthentication(credentials);
             }
             chain.doFilter(request, response);
         }
         catch(Throwable t) {
             HttpServletResponse servletResponse = (HttpServletResponse) response;
             servletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, t.getMessage());
         }
     }
 }

Após esse filtro, o Spring Security reconhecerá o usuário como autenticado, pulando todos os filtros de autenticação e validando somente se ele possui as autorizações necessárias para acessar os recursos desejados.

Nota: Por conta de ser autocontido, um JWT dispensa o uso de mecanismos tradicionais (Authentication Manager, Authentication Provider, etc.) para realização da autenticação. Isso pode gerar problemas em sistemas com modificações constantes de autorizações e, por esse motivo, é importante definir uma política de expiração adequada, capaz de garantir que a vida de um token possua uma duração mínima necessária para realizar seu propósito.

API privada

Nada do que criamos até agora faria sentido se não existissem serviços a proteger, com acesso privado, em nossa API. A classe TesteController, exposta na Listagem 11, apresenta dois desses serviços. O primeiro deles, disponível pela URL /api/hello/{nome}, utiliza a anotação @PreAuthorize para restringir o acesso apenas a usuários autenticados (linha 13) e retorna uma mensagem de boas-vindas direcionada ao nome enviado como parâmetro na URL (mapeado com a ajuda da anotação @PathVariable na linha 14). O segundo serviço, disponível pela URL /api/helloAdmin, utiliza a anotação @PreAuthorize para restringir o acesso apenas a usuários autenticados que possuam a autorização ROLE_ADMIN (linha 19) e retorna uma mensagem de boas-vindas direcionada ao administrador.

Listagem 11. Código exemplo da API privada.

  
 package br.com.javamagazine.controller;
 
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
 @RestController
 @RequestMapping(value="/api")
 public class TesteController {
     
     @RequestMapping(value = "/hello/{nome}")
     @PreAuthorize("isAuthenticated()")
     public String hello(@PathVariable("nome") String nome) {
         return "Hello " + nome + "!";
     }
     
     @RequestMapping(value = "/helloAdmin")
     @PreAuthorize("hasRole('ROLE_ADMIN')")
     public String helloAdmin() {
         return "Hello administrator!";
     }
     
 }

Avaliação

Para avaliar nosso sistema, precisaremos enviar algumas requisições HTTP para os recursos privados da API. Existem diversas ferramentas que possibilitam isso, mas vamos utilizar o add-on Postman para o navegador Google Chrome, por sua facilidade de instalação e uso.

Os casos de teste apresentados nas seções a seguir visam garantir que nossa API possui as funcionalidades mínimas de segurança necessárias. Para todos os casos, assume-se que o nome da aplicação é SpringRestSecurityJwt e o servidor web escolhido utiliza a porta local 8080.

Acesso não autorizado

O primeiro teste a realizar consiste em tentar acessar um serviço sem qualquer autenticação. A Tabela 1 apresenta a configuração da requisição. O resultado esperado para esse teste é um erro de acesso não autorizado (401), pois, fora o login, nenhum serviço de nossa API aceita acessos não autenticados. É importante notar que o servidor utilizado utiliza a porta 8080. Portanto, isso deve ser alterado de acordo com o ambiente de desenvolvimento disponível.

Tipo de requisição GET
URL http://localhost:8080/SpringRestSecurityJwt/api/hello/teste

Tabela 1. Requisição sem autenticação.

Como esperado, essa requisição recebe um erro 401 – UNAUTHORIZED, conforme verificado na Figura 5, que apresenta a tela do Postman contendo a requisição e a resposta.

Requisição
não autenticada
Figura 5. Requisição não autenticada

Login

Nosso próximo teste consiste em realizar um login utilizando nosso serviço customizado. A Tabela 2 apresenta a configuração dessa requisição, onde passamos os parâmetros no corpo (body) como um JSON e, por esse motivo, precisamos informar um header Content-Type com valor application/json. O resultado esperado consiste na recepção de um JWT correspondente ao usuário informado.

Tipo de requisição POST
URL http://localhost:8080/SpringRestSecurityJwt/login
Headers Content-Type: application/json
Body { "username":"admin", "password":"admin" }

Tabela 2. Login para um usuário admin.

Essa requisição é processada com sucesso (Status 200 – OK), como podemos visualizar na Figura 6. O elemento mais importante da resposta, contudo, é o header Token, que contém nosso JWT.

Requisição de login
Figura 6. Requisição de login

Ao decodificarmos o segundo trecho do JWT (payload), obtemos o JSON apresentado na Listagem 12, onde podemos visualizar nossa claim privada usr e as claims registradas iss, sub e exp.

Listagem 12. Payload decodificado do JWT.

  {
    "usr": "{\"username\":\"admin\",\"password\":null,\"authorities\":
      [{\"authority\":\"ROLE_ADMIN\"}]}",
    "iss": "br.com.javamagazine",
    "sub": "admin",
    "exp": 1477781160
  }

Acesso com JWT

Finalmente, realizaremos um acesso idêntico ao primeiro teste, mas utilizando o JWT como meio de autenticação. A Tabela 3 apresenta a configuração dessa requisição. Para reproduzir o teste, o token deve ser alterado pelo recebido no processo de login.

Ao contrário de nosso primeiro teste, temos um header Authorization, onde passamos o JWT recebido após a autenticação, prefixado por "Bearer ". Dessa vez, esperamos que a requisição seja processada com sucesso (Status 200 – Ok) e que recebamos uma mensagem de boas-vindas.

Tipo de requisição GET
URL http://localhost:8080/SpringRestSecurityJwt/api/hello/teste
Headers Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c3IiOiJ7XCJ1c2 VybmFtZVwiOlwiYWRtaW5cIixcInBhc3N3b3JkXCI6bnVsbCx cImF1dGhvcml0aWVzXCI6W3tcImF1dGhvcml0eVwiOlwiUk9M RV9BRE1JTlwifV19IiwiaXNzIjoiYnIuY29tLmphdmFtYWdhe mluZSIsInN1YiI6ImFkbWluIiwiZXhwIjoxNDc3NzgxMTYwfQ .Xt_3mGTjHe7sX-SVc6E7KjfFA70ErWnPLYcsynUX4xw

Tabela 3. Requisição com JWT.

Confirmando a expectativa, a requisição é processada com sucesso (Status 200 – OK), o que pode ser observado na Figura 7. Isso indica que nosso filtro funciona e é capaz de autenticar o usuário apenas com o token.

Requisição com JWT
Figura 7. Requisição com JWT

Acesso com JWT inválido

Como um teste adicional, tentaremos a mesma requisição com um JWT modificado, mantendo a mesma assinatura, mas com um corpo diferente. A Tabela 4 apresenta a configuração dessa requisição. Para reproduzir o teste é necessário alterar o payload do token recebido no processo de login. O resultado esperado para esse teste é um erro de acesso não autorizado (401).

Tipo de requisição GET
URL http://localhost:8080/SpringRestSecurityJwt/api/hello/teste
Headers Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c3IiOiJ7XCJ1c2 VybmFtZVwiOlwiYWrtaW5cIixcInBhc3N3b3JkXCI6bnVsbCx cImF1dGhvcml0aWVzXCI6W3tcImF1dGhvcml0eVwiOlwiUk9M RV9BRE1JTlwifV19IiwiaXNzIjoiYnIuY29tLmphdmFtYWdhe mluZSIsInN1YiI6ImFkbWluIiwiZXhwIjoxNDc3NzgxMTYwfQ .Xt_3mGTjHe7sX-SVc6E7KjfFA70ErWnPLYcsynUX4xw

Tabela 4. Requisição com JWT inválido.

Essa requisição recebe um erro 401 – UNAUTHORIZED, como verificado na Figura 8, mostrando que nosso filtro valida o conteúdo, utilizando para isso a assinatura e a chave privada. É importante notar que a mensagem de erro que acompanha o erro indica que a assinatura do JWT não bate com a assinatura computada localmente, no servidor.

Requisição com JWT inválido
Figura 8. Requisição com JWT inválido

Acesso com JWT expirado

Neste outro teste adicional, tentaremos a mesma requisição com o JWT válido, mas após seu tempo de expiração (conforme configurado em seu corpo). Nesse caso, a requisição segue a mesma configuração previamente apresentada na Tabela 3, mas o resultado esperado dessa vez é um erro de acesso não autorizado (401).

Ao executarmos a requisição, recebemos um erro 401 – UNAUTHORIZED, como verificado na Figura 9, mostrando que nosso filtro também valida a data de expiração do token, conforme a mensagem que acompanha o erro.

Requisição com JWT expirado
Figura 9. Requisição com JWT expirado

Acesso com JWT contendo autorizações insuficientes

Por fim, testaremos um caso onde o JWT possui autorizações insuficientes para o serviço acessado. Para tanto, geraremos um novo token para um usuário de nome teste, segundo os parâmetros indicados na Tabela 5.

Tipo de requisição POST
URL http://localhost:8080/SpringRestSecurityJwt/login
Headers Content-Type: application/json
Body { "username":"teste", "password":"teste" }

Tabela 5. Login do usuário teste.

O token gerado por essa requisição, presente no header Token, é o seguinte:

eyJhbGciOiJIUzI1NiJ9.eyJ1c3IiOiJ7XCJ1c2VybmFtZVwiOlwidGVzdGVcIixcInBhc3N3b3JkXCI6bnVsbCxcImF1dGhvcml0
aWVzXCI6W3tcImF1dGhvcml0eVwiOlwiUk9MRV9VU0VSXCJ9XX0iLCJpc3MiOiJici5jb20uamF2YW1hZ2F6aW5lIiwic3ViIj
oidGVzdGUiLCJleHAiOjE0Nzc4MjgxNTl9.2gGMeLIOq9UEv7ghoS80wNuf2pIfvdzYzkvZEeGGuYk

Além disso, o corpo (body) da resposta contém um JSON com as características do usuário teste, como visualizado na Listagem 13.

Listagem 13. JSON recebido no corpo da resposta.

  {
    "username": "teste",
    "password": null,
    "authorities": [
      {
        "authority": "ROLE USER"
      }
    ]
  }

Utilizaremos esse token para acessar o serviço helloAdmin, com os parâmetros listados na Tabela 6. O resultado esperado é um erro 403 – FORBIDDEN, indicando que o usuário não possui a autorização necessária para acessar o recurso.

Tipo de requisição GET
URL http://localhost:8080/SpringRestSecurityJwt/api/helloAdmin
Headers Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJ1c3IiOiJ7XCJ1c2 VybmFtZVwiOlwidGVzdGVcIixcInBhc3N3b3JkXCI6bnVsbCx cImF1dGhvcml0aWVzXCI6W3tcImF1dGhvcml0eVwiOlwiUk9M RV9VU0VSXCJ9XX0iLCJpc3MiOiJici5jb20uamF2YW1hZ2F 6aW5lIiwic3ViIjoidGVzdGUiLCJleHAiOjE0Nzc4MjgxNTl9.2 gGMeLIOq9UEv7ghoS80wNuf2pIfvdzYzkvZEeGGuYk

Tabela 6. Requisição com autorizações insuficientes.

A Figura 10 apresenta o resultado da requisição, o qual condiz com o esperado.

Requisição com JWT contendo autorizações insuficientes
Figura 10. Requisição com JWT contendo autorizações insuficientes

Este artigo apresentou uma abordagem simples e funcional, baseada em Spring Security, de um mecanismo de Token Based Authentication com JWT. Como indicado, entretanto, a implementação apresentada não busca completude e, por esse motivo, não se preocupa com a persistência dos usuários e suas autorizações ou com problemas de acesso, como CORS (Cross-Origin Resource Sharing).

A persistência pode ser realizada com o auxílio de outros frameworks, como o Spring Data JPA, e um filtro CORS também pode ser facilmente criado com o próprio framework Spring Security, utilizado neste artigo.

Por fim, outro ponto que merece atenção, agora que conhecemos JWT, é a possibilidade de criar um mecanismo de autenticação/autorização com OAuth 2.0. O Spring oferece uma solução completa para isso, inclusive com suporte nativo a JWT, com o framework Spring Security OAuth.

Links Úteis

Saiba mais sobre ASP.NET Web API ;)

  • O que é ASP.NET Web API?:
    O ASP.NET Web API é um framework para desenvolvimento de web services RESTful sobre o .NET Framework.
  • O que é RESTful?:
    Este curso possui por objetivo apresentar o que é o REST e qual a diferença entre REST e RESTful.
  • Web Services REST versus SOAP:
    Aprenda as diferenças e vantagens ao adotar cada uma dessas tecnologias em seu projeto Java.