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.
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).
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.
{
"adm": "true",
"app": "Postal",
"iss": "br.com.javamagazine",
"sub": "Administrator",
"exp": 1477781160
}
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.
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).
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.
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.
<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.
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).
<!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).
<?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).
<?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).
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.
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.
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.
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).
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).
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.
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.
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.
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 |
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.
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" } |
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.
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.
{
"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 |
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.
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 |
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.
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.
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" } |
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.
{
"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 |
A Figura 10 apresenta o resultado da requisição, o qual condiz com o esperado.
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
- Formulário de cadastro com JSF e Bootstrap:
Aprenda neste exemplo como criar interfaces ricas com Bootstrap e JSF. Saiba como o Pass-through elements pode te ajudar a ter mais controle sobre o HTML gerado pelos componentes nativos. - Web services: testes funcionais e automatizados com Cucumber:
Como simplificar os testes de web services com Java e Cucumber eliminando as etapas de parser de XML. - WebLogic Multitenant: A nuvem dentro do servidor de aplicação:
Consolidando suas aplicações Java EE com controle e isolamento na mesma JVM.
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.
Confira outros conteúdos:
<Perguntas frequentes>
Nossos casos de sucesso
Eu sabia pouquíssimas coisas de programação antes de começar a estudar com vocês, fui me especializando em várias áreas e ferramentas que tinham na plataforma, e com essa bagagem consegui um estágio logo no início do meu primeiro período na faculdade.
Estudo aqui na Dev desde o meio do ano passado!
Nesse período a Dev me ajudou a crescer muito aqui no trampo.
Fui o primeiro desenvolvedor contratado pela minha
empresa. Hoje eu lidero um time de desenvolvimento!
Minha meta é continuar estudando e praticando para ser um
Full-Stack Dev!
Economizei 3 meses para assinar a plataforma e sendo sincero valeu muito a pena, pois a plataforma é bem intuitiva e muuuuito didática a metodologia de ensino. Sinto que estou EVOLUINDO a cada dia. Muito obrigado!
Nossa! Plataforma maravilhosa. To amando o curso de desenvolvimento front-end, tinha coisas que eu ainda não tinha visto. A didática é do jeito que qualquer pessoa consegue aprender. Sério, to apaixonado, adorando demais.
Adquiri o curso de vocês e logo percebi que são os melhores do Brasil. É um passo a passo incrível. Só não aprende quem não quer. Foi o melhor investimento da minha vida!
Foi um dos melhores investimentos que já fiz na vida e tenho aprendido bastante com a plataforma. Vocês estão fazendo parte da minha jornada nesse mundo da programação, irei assinar meu contrato como programador graças a plataforma.
Wanderson Oliveira
Comprei a assinatura tem uma semana, aprendi mais do que 4 meses estudando outros cursos. Exercícios práticos que não tem como não aprender, estão de parabéns!
Obrigado DevMedia, nunca presenciei uma plataforma de ensino tão presente na vida acadêmica de seus alunos, parabéns!
Eduardo Dorneles
Aprendi React na plataforma da DevMedia há cerca de 1 ano e meio... Hoje estou há 1 ano empregado trabalhando 100% com React!
Adauto Junior
Já fiz alguns cursos na área e nenhum é tão bom quanto o de vocês. Estou aprendendo muito, muito obrigado por existirem. Estão de parabéns... Espero um dia conseguir um emprego na área.
Utilizamos cookies para fornecer uma melhor experiência para nossos usuários, consulte nossa política de privacidade.