Agora que os endpoints de administração estão prontos, podemos nos questionar: não bastaria para qualquer um instalar o Postman para conseguir forjar requisições de administração, como solicitar a criação ou cancelamento de um filme em cartaz? Sim! Hoje nossa aplicação está completamente vulnerável a esse tipo de ataque, e é por isso que temos que ficar atentos sempre que estivermos desenvolvendo uma API de dados:
Garanta primeiro a segurança da API, e NUNCA leve em conta artifícios de frontend (como esconder um campo ou tratar alguma condição em JavaScript) nesse processo. Por exemplo, não é por que o frontend some com o botão Cancelar se o usuário não for administrador que o endpoint DELETE /filmes/:id não deve verificar novamente. Como vimos, basta um
curl
ou um aplicativo como o Postman para que qualquer um passe por trás de todo seu frontend.
A segurança de qualquer aplicação normalmente envolve dois conceitos, autenticação e autorização.
-
Autenticação: é o processo de confirmar que um usuário diz ser quem é. Isso pode ser feito com um formulário de login/senha, com uma autenticação delegada para terceiros (Facebook, conta Google etc), integração com serviços de diretório (AD, LDAP) etc. Essa parte não diz o que cada um pode fazer, apenas garante que uma sessão de uso está relacionada com um usuário específico.
-
Autorização: é o processo de descobrir o que um usuário pode fazer. Essa parte normalmente não se preocupa com credenciais, apenas assume que o módulo de autenticação fez seu trabalho e confia que o usuário é quem diz ser, e à partir dessa identidade busca os grupos, papéis, perfis e permissões relacionadas a ele.
Em conjunto, autenticação e autorização garantem a segurança de uma API de dados.
Vamos começar a implementação então. Precisamos de um endpoint para a parte de autenticação, que
recebe o usuário e a senha, valida essas informações, e de alguma forma retorna um passaporte
para futuras requisições. Esse endpoint será o POST /login
, e o passaporte será primeiramente
um cookie, mas depois mudaremos isso para um header customizado e por fim utilizaremos o header
padrão Authorization em conjunto com um JSON Web Token (JWT).
Por quê (não) usar cookies? Um cookie
nada mais é do que um valor dentro de um armazenamento
especial do navegador, que pode ser manipulado de maneira transparente pelo header também
especial chamado Cookies
, que pode ser usado tanto em requisições quanto retornado em respostas
do servidor. Basicamente o navegador:
- Coleta todos os cookies relacionados ao domínio de todas as requisições (páginas, imagens,
requisições ajax) e embute eles no header
Cookies
da requisição; - Adiciona todos os cookies que o servidor manda na resposta de requisições nesse armazenamento especial.
Esse mecanismo funciona muito bem para a definição de sessões web, veja:
- O usuário abre seu site pela primeira vez. O servidor não encontra nenhum cookie, gera um
valor pseudo-aleatório e o retorna como um cookie chamado
SessaoDaMinhaApp
; - O usuário continua navegando na aplicação, e a cada requisição o navegador embute o valor
do cookie
SessaoDaMinhaApp
no headerCookies
, e assim o servidor consegue saber que continua sendo a mesma pessoa. - Eventualmente se o usuário efetua login na aplicação, o servidor apenas faz um mapeamento do valor pseudo-randômico (chamado de identificador da sessão) com o e-mail ou nome de usuário, passsando a saber assim não só que se trata da mesma pessoa mas quem essa pessoa é, sem precisar ficar pedindo o usuário e senha sempre.
Por que motivo então cookies são tão mal vistos? Justamente por serem transparentes, o simples fato de adicionar uma imagem ou fazer qualquer requisição ajax para um site de terceiro, por exemplo um site de publicidade, permite que um serviço rastreie todo seu comportamento na web, informação valiosa e que de certa forma denigre a privacidade individual. Muitas vezes isso é feito sem consentimento do usuário, o que gerou a má fama que os cookies possuem hoje.
Vamos então implementar nossa autenticação baseada em cookies! Para isso, vamos começar criando
o endpoint POST /login
, que recebe um objeto JSON com o usuário e a senha desejados e retorna
200 OK
se a autenticação deu certo (após adicionar o usuário na sessão, o que vai acarretar
no retorno do cookie identificador de sessão como poderemos ver), ou 401 Unauthorized
se as
credenciais estiverem inválidas.
Crie uma classe chamada LoginAPI
no pacote com.opensanca.trilharest.filmes.seguranca
com a seguinte definição:
package com.opensanca.trilharest.filmes.seguranca;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/login")
public class LoginAPI {
@PostMapping
public void login(@RequestBody CredenciaisDTO credenciais) {
}
}
Crie também uma classe chamada CredenciaisDTO
no mesmo pacote:
public class CredenciaisDTO {
private String usuario;
private String senha;
public String getUsuario() {
return usuario;
}
public void setUsuario(String usuario) {
this.usuario = usuario;
}
public String getSenha() {
return senha;
}
public void setSenha(String senha) {
this.senha = senha;
}
}
Vamos agora criar uma tabela de usuários no banco de dados, começando pela classe de entidade
Usuario
:
package com.opensanca.trilharest.filmes.seguranca;
import javax.persistence.Entity;
import javax.persistence.Id;
import java.util.UUID;
@Entity
public class Usuario {
@Id
private UUID id;
private String usuario;
private String senha;
// getters e setters omitidos
}
Precisamos também de uma migração liquibase para adicionar a tabela nas bases relacionais. O
primeiro arquivo a ser modificado é o db.changelog-master
, adicionando um novo changeset:
databaseChangeLog:
- changeSet:
id: 1
author: samuelgrigolato
changes:
- sqlFile:
dbms: postgresql
encoding: utf8
path: sqls/0001-migracao-inicial.sql
relativeToChangelogFile: true
- changeSet:
id: 2
author: samuelgrigolato
changes:
- sqlFile:
dbms: postgresql
encoding: utf8
path: sqls/0002-adicao-da-tabela-de-usuarios.sql
relativeToChangelogFile: true
E por fim o arquivo 0002-adicao-da-tabela-de-usuarios.sql
no diretório
src/main/resources/db/changelog/sqls
:
create table usuario (
id uuid not null,
usuario character varying(100) not null,
senha char(64) not null,
constraint pk_usuario primary key (id)
);
Repare que estamos usando o tipo char(64)
para a senha. Qual o motivo? Não
é muito interessante armazenar senhas no que chamamos de texto plano, ou seja,
da forma como são informadas pelo usuário. É uma ótima prática usar funções
de espalhamento (ou funções de hash) unidirecionais, ou seja, dado um resultado
de execução dessa função não é possível descobrir qual o valor que o gerou.
Funções de hash normalmente possuem uma saída de tamanho único para qualquer tamanho de entrada, e no caso do SHA256 são 256 bits que podem ser codificados em 64 caracteres hexadecimais (256 bits = 32 bytes, cada byte pode ser representado por dois caracteres hexadecimais, visto que cada caractere hexadecimal pode representar 16 valores distintos e 16*16=256, que é a quantidade de valores possíveis de um byte).
Alguns exemplos de função de hash: MD5, SHA1, SHA256 etc. Hoje em dia não se
considera mais o MD5 e o SHA1 como funções seguras para fins de autenticação,
então utilizaremos a função SHA256. Para inserir usuários de exemplo podemos
usar serviços online de geração de hash, como por exemplo o
http://www.xorbin.com/tools/sha256-hash-calculator
. Não usaremos apenas
a senha na geração do hash, utilizaremos um prefixo em todas as senhas,
chamado de salt, para aumentar ainda mais a segurança da nossa aplicação.
Esse salt costuma ficar no arquivo application.properties
ou até mesmo ser
carregado de variável de ambiente, para que possa ser diferente em
desenvolvimento, teste e produção.
Insira alguns usuários na tabela, usando o salt a5KeT1
como prefixo de
senha. Por exemplo, para a senha 123456
, o hash SHA256 deve ser computado
à partir da entrada a5KeT1123456
, que dá o seguinte resultado:
80054d123d90990e012bad576ac9e23f670ddcc8f1ab4698e00b9c1e3ee7860a
insert into usuario (id, usuario, senha)
values ('1c729117-e65d-4a36-b171-5f4a73586b63', 'adm1', '80054d123d90990e012bad576ac9e23f670ddcc8f1ab4698e00b9c1e3ee7860a'),
('f8fcc35b-5515-4e06-9c47-7332c9494918', 'adm2', '80054d123d90990e012bad576ac9e23f670ddcc8f1ab4698e00b9c1e3ee7860a');
Agora que temos nossos usuários na base vamos implementar um repositório
e um método para buscar um registro de usuário pelo seu login. Crie uma
interface chamada UsuariosRepository
no pacote com.opensanca.trilharest.filmes.seguranca
:
package com.opensanca.trilharest.filmes.seguranca;
import org.springframework.data.repository.CrudRepository;
import java.util.Optional;
import java.util.UUID;
public interface UsuariosRepository extends CrudRepository<Usuario, UUID> {
Optional<Usuario> findByUsuario(String usuario);
}
Mas e o texto da consulta? Não precisamos dele, pois usamos um nome de método que o Spring Data consegue entender e derivar a consulta para nós. Veja mais detalhes aqui: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#repositories.query-methods.query-creation.
Por fim podemos implementar a autenticação na classe LoginAPI
:
package com.opensanca.trilharest.filmes.seguranca;
import com.opensanca.trilharest.filmes.comum.BindingException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import javax.xml.bind.DatatypeConverter;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
@RestController
@RequestMapping("/login")
public class LoginAPI {
@Autowired
private UsuariosRepository usuariosRepository;
@Value("${autenticacao.salt}")
private String salt;
@PostMapping
public void login(@Valid @RequestBody CredenciaisDTO credenciais, BindingResult results) {
if (results.hasErrors()) {
throw new BindingException(results);
}
Usuario usuario = this.usuariosRepository.findByUsuario(credenciais.getUsuario())
.orElseThrow(CredencialInvalidaException::new);
if (!isSenhaCorreta(credenciais.getSenha(), usuario.getSenha())) {
throw new CredencialInvalidaException();
}
// falta algo aqui!!!
}
private boolean isSenhaCorreta(String senha, String espalhamento) {
MessageDigest digest = null;
try {
digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest((salt + senha).getBytes(StandardCharsets.UTF_8));
byte[] hashDoEspalhamento = DatatypeConverter.parseHexBinary(espalhamento);
return Arrays.equals(hash, hashDoEspalhamento);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256 não suportado pelo servidor");
}
}
}
Antes de testar precisamos adicionar a seguinte propriedade no arquivo application.properties
:
autenticacao.salt=a5KeT1
Vamos testar nossa implementação, emitindo as seguintes requisições usando Postman:
POST /login
{ usuario: 'naoexistente': senha: '123456' }
POST /login
{ usuario: 'adm1': senha: '123456789' }
POST /login
{ usuario: 'adm1': senha: '123456' }
Note que apenas no último caso o retorno será 200 OK
, nos outros o retorno será 401 Unauthorized
, da forma como desejamos.
Nossa implementação está interessante, mas podemos melhorá-la! O Spring fornece um bean
chamado PasswordEncripter
, que pode facilitar nossa vida com a parte de verificação de
hash. Para usá-lo temos que adicionar primeiramente o spring-security
no projeto (isso
é feito no build.gradle
):
dependencies {
...
compile group: 'org.springframework.boot', name: 'spring-boot-starter-security', version: '1.5.7.RELEASE'
...
}
Atenção: lembre-se de atualizar o projeto na IDE (via gradle idea
ou equivalente).
Voltando a classe LoginAPI
, podemos implementá-la de uma maneira mais limpa:
package com.opensanca.trilharest.filmes.seguranca;
import com.opensanca.trilharest.filmes.comum.BindingException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
@RequestMapping("/login")
public class LoginAPI {
@Autowired
private UsuariosRepository usuariosRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping
public void login(@Valid @RequestBody CredenciaisDTO credenciais, BindingResult results) {
if (results.hasErrors()) {
throw new BindingException(results);
}
Usuario usuario = this.usuariosRepository.findByUsuario(credenciais.getUsuario())
.orElseThrow(CredencialInvalidaException::new);
if (!passwordEncoder.matches(credenciais.getSenha(), usuario.getSenha())) {
throw new CredencialInvalidaException();
}
// falta algo aqui!!!
}
}
Repare que removemos toda a lógica de verificação, encapsulando ela nessa chamada ao método
matches
do componente passwordEncoder
. Como o Spring Security não provê uma implementação
padrão deste bean, temos que criá-la nós mesmos, o que faremos em uma nova classe chamada
SegurancaConfiguration
no pacote com.opensanca.trilharest.filmes.seguranca
. Classes de
configuração são úteis para instanciarmos beans Spring de maneira programática. Também
usaremos ela para desabilitar a parte de autorização do Spring Security
por enquanto, já
que ainda estamos na solução caseira.
package com.opensanca.trilharest.filmes.seguranca;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;
@Configuration
public class SegurancaConfiguration extends WebSecurityConfigurerAdapter {
@Value("${autenticacao.secret}")
private String secret;
@Bean
public PasswordEncoder passwordEncoder() {
return new StandardPasswordEncoder(secret);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable();
http.anonymous();
}
}
Repare que mudamos o nome da propriedade salt
para secret, isso se dá pelo fato do salt
na implementação do StandardPasswordEncoder
é gerado randomicamente para cada execução, e
este é embutido junto com o hash gerado e por sua vez persistido no banco. Essa mudança deve
ser refletida no application.properties
também:
autenticacao.secret=a5KeT1
Antes de conseguirmos fazer funcionar precisamos dar um jeito de atualizar os espalhamentos no
banco, afinal o algoritmo do password encoder do Spring não é nada compatível com nosso simples
hash de salt + senha
. Existem várias maneiras de fazer isso, a mais fácil delas é usar o bom
e velho debug da sua IDE para inspecionar o resultado dessa chamada aqui:
passwordEncoder.encode("123456")
Caso não saiba fazer isso uma opção mais simples é adicionar um
System.out.println(passwordEncoder.encode("123456"))
logo acima da condicional que analisa
se a senha é válida ou não. Só não se esqueça de tirá-lo de lá :).
Temos um problema! O valor do hash possui 80 caracteres de tamanho, ao invés de 64 (isso se
dá pois esse algoritmo embute mais dados no resultado, como o algoritmo utilizado, quantidade
de iterações etc). Mas sem pânico, usaremos um novo changeset
liquibase para resolver este
problema. Primeiro o declararemos no db.changelog-master.yml
:
- changeSet:
id: 3
author: samuelgrigolato
changes:
- sqlFile:
dbms: postgresql
encoding: utf8
path: sqls/0003-aumenta-tamanho-coluna-senha.sql
relativeToChangelogFile: true
E depois criamos o arquivo SQL referenciado pelo changeset:
alter table usuario alter column senha type char(80);
E por fim podemos ajustar os registros existentes (esse script deve ser executado direto, não faz sentido colocá-lo como um changeset do liquibase!):
update usuario set senha = 'COLOQUE_AQUI_O_HASH_OBTIDO_MAIS_ACIMA';
Neste ponto podemos testar novamente via Postman que o comportamento será igual à solução
manual! Deu muito mais trabalho, então por quê fazer dessa maneira? Dois motivos: esse
algoritmo do Spring é muito mais seguro do que o nosso algoritmo manual, e também pois
estamos desacoplando nossa classe LoginAPI
dos internos de criptografia de senha, o que
facilita com que outros componentes da nossa aplicação venham a usar a mesma coisa no futuro,
incentivando o reúso.
Repare que nossa autenticação ainda não presta para absolutamente nada. O que acontece se
executarmos um endpoint de administração via Postman? Ele ainda não está obrigando que o
usuário esteja logado para permitir a operação. Lembrando que ainda não estamos usando
o Spring Security para isso, podemos continuar com nossa estratégia manual e adicionar um
interceptador de requisições. Crie uma classe chamada LoginHandlerInterceptor
no pacote
de segurança:
package com.opensanca.trilharest.filmes.seguranca;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class LoginHandlerInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println(request.getRequestURI());
return super.preHandle(request, response, handler);
}
}
Repare que os interceptadores nos permitem investigar (e bloquear, retornando false
no método
preHandle
) de maneira transparente todas as requisições destinadas aos endpoints MVC.
Note que precisamos registrar nossos interceptadores em uma outra classe de configuração, que
deve estender WebMvcConfigurerAdapter
. Criaremos ela com o nome MvcConfiguration
no pacote
com.opensanca.trilharest.filmes
:
package com.opensanca.trilharest.filmes;
import com.opensanca.trilharest.filmes.seguranca.LoginHandlerInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
@Configuration
public class MvcConfiguration extends WebMvcConfigurerAdapter {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor());
}
}
Neste ponto execute a aplicação, verá que a URL de todas as requisições são apresentadas no console,
pois passaram pelo método preHandle
do LoginHandlerInterceptor
. Mas o que queremos não é
registrar as requisições, mas bloqueá-las caso sejam de determinados tipos e o usuário não
esteja autenticado. Vamos fazer assim, verificaremos se o método da requisição é diferente de
GET
e se sim verificaremos se existe um usuário na sessão (é aqui que entram os cookies). Veja:
package com.opensanca.trilharest.filmes.seguranca;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LoginHandlerInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (HttpMethod.resolve(request.getMethod()) == HttpMethod.GET) {
// todos podem executar buscas de informação
return true;
}
if ("/login".equals(request.getRequestURI())) {
// o endpoint de auteneticação também deve ser liberado
return true;
}
// falta coisa aqui!
response.sendError(HttpStatus.UNAUTHORIZED.value());
return false;
}
}
Mas como verificar se existe um usuário logado? Neste ponto precisaremos do conceito de sessão,
e felizmente o Spring MVC
nos fornece uma maneira muito fácil de lidar com ela, através da
definição de beans
com o escopo Session
. Vamos criar uma classe chamada UsuarioLogadoDTO
, no
pacote de segurança, que representará os dados de autenticação de um usuário da aplicação:
package com.opensanca.trilharest.filmes.seguranca;
import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Component
@Scope("session")
public class UsuarioLogadoDTO {
private UUID id;
// getters e setters omitidos
}
Agora que temos um lugar para armazenar o identificador do usuário logado em uma determinada sessão, podemos fazer isso, primeiro na autenticação (que dirá quem está logado) e depois na autorização (que dirá se o usuário logado pode fazer a operação):
...
import org.springframework.context.annotation.Scope;
...
@RestController
@RequestMapping("/login")
@Scope("prototype")
public class LoginAPI {
@Autowired
private UsuariosRepository usuariosRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private UsuarioLogadoDTO usuarioLogadoDTO;
@PostMapping
public void login(@Valid @RequestBody CredenciaisDTO credenciais, BindingResult results) {
if (results.hasErrors()) {
throw new BindingException(results);
}
Usuario usuario = this.usuariosRepository.findByUsuario(credenciais.getUsuario())
.orElseThrow(CredencialInvalidaException::new);
if (!passwordEncoder.matches(credenciais.getSenha(), usuario.getSenha())) {
throw new CredencialInvalidaException();
}
this.usuarioLogadoDTO.setId(usuario.getId());
}
}
package com.opensanca.trilharest.filmes.seguranca;
import org.springframework.context.ApplicationContext;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import org.springframework.web.servlet.support.RequestContextUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class LoginHandlerInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (HttpMethod.resolve(request.getMethod()) == HttpMethod.GET) {
// todos podem executar buscas de informação
return true;
}
if ("/login".equals(request.getRequestURI())) {
// o endpoint de auteneticação também deve ser liberado
return true;
}
ApplicationContext ctx = RequestContextUtils.findWebApplicationContext(request);
UsuarioLogadoDTO usuarioLogado = ctx.getBean(UsuarioLogadoDTO.class);
if (usuarioLogado.getId() == null) {
response.sendError(HttpStatus.UNAUTHORIZED.value());
return false;
}
return true;
}
}
Agora sim, via Postman, veremos que os endpoints de administração de filmes só estarão
disponíveis após uma autenticação com sucesso no endpoint POST /login
. Ainda dentro do Postman
clique no botão Cookies
que está localizado perto do botão Send
de uma aba de requisição,
verifique o valor, remova o cookie, dê uma praticada para ficar confortável com essa solução.
Nossa solução funciona, mas não é nada padronizada. A biblioteca Spring Security
já fornece
tudo isso pra nós, então por que não usar? Para isso, vamos começar removendo a classe
LoginHandlerInterceptor
e a classe LoginAPI
. Depois de removê-las (não se esqueça do registro do interceptador na classe MvcConfiguration
) vamos ajustar a configuração do Spring Security na classe
SegurancaConfiguration
:
package com.opensanca.trilharest.filmes.seguranca;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.StandardPasswordEncoder;
@Configuration
public class SegurancaConfiguration extends WebSecurityConfigurerAdapter {
@Value("${autenticacao.secret}")
private String secret;
@Bean
public PasswordEncoder passwordEncoder() {
return new StandardPasswordEncoder(secret);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// em APIs de dados não usamos CSRF
http.csrf().disable();
http.formLogin();
http.authorizeRequests()
.antMatchers(HttpMethod.POST, "/**/*").hasAuthority("ADMIN")
.antMatchers(HttpMethod.PUT, "/**/*").hasAuthority("ADMIN")
.antMatchers(HttpMethod.DELETE, "/**/*").hasAuthority("ADMIN")
.antMatchers(HttpMethod.GET, "/**/*").permitAll()
.anyRequest().denyAll();
}
}
Na chamada ao formLogin
habilitados o endpoint /login
do Spring Security,
que será responsável por receber os dados de usuário e senha. Com a cadeia de chamadas
começando por authorizeRequests
estamos definindo a autorização necessária para cada
tipo de requisição, por exemplo apenas administradores podem executar requisições do tipo
POST enquanto qualquer usuário (mesmo não autenticado) pode executar requisições GET.
Se executarmos a aplicação do jeito que ela está e enviarmos um POST /login
veremos
que o endpoint nos retorna um HTML default de página de login (junto com mensagens de erro).
Isso não é nem de perto o que queremos, já que nossa API é uma API de dados, então vamos
começar mudando isso para que ele retorne um 401 sempre que a autenticação falha. Podemos
fazer isso através de um failureHandler
customizado na configuração de formLogin
:
[...]
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
[...]
http.formLogin()
.failureHandler(new SimpleUrlAuthenticationFailureHandler());
[...]
A classe SimpleUrlAuthenticationFailureHandler
recebe um parâmetro, que se nulo faz com que
o endpoint não cause um redirect, mas sim retorne o status code 401.
Agora e se enviarmos os parâmetros corretos? Note que esse endpoint não faz parse JSON da
requisição, então temos que enviar os dados de maneira convencional (no Postman isso significa
selecionar a opção form-data
na aba Body
ao invés de raw
> JSON
). Além disso, os nomes
reconhecidos por padrão são username
e password
ao invés de usuario
e senha
.
Note que precisamos de uma forma para indicar ao Spring Security como ele deve fazer para
validar credenciais. Isso é papel do authenticationProvider
que por sua vez faz parte
do authenticationManager
, e existe um método de configuração na superclassse da nossa
SegurancaConfiguration
especificamente para construí-lo:
@Configuration
public class SegurancaConfiguration extends WebSecurityConfigurerAdapter {
[...]
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("adm1").password("123456memoria").authorities("ADMIN")
.and()
.withUser("user2").password("123456memoria").authorities();
}
}
Note que obviamente essa autenticação não serve para muitos casos, pois estamos travando os
usuários no código fonte da aplicação. Ao invés de usar a inMemoryAuthentication
podemos
usar outras opções como a jdbcAuthentication
:
[...]
import javax.sql.DataSource;
[...]
@Configuration
public class SegurancaConfiguration extends WebSecurityConfigurerAdapter {
[...]
@Autowired
private DataSource dataSource;
[...]
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication()
.dataSource(dataSource)
.usersByUsernameQuery("select usuario as username, senha as password, true as enabled from usuario where usuario = ?")
.authoritiesByUsernameQuery("select ? as username, 'ADMIN' as authority")
.passwordEncoder(passwordEncoder());
}
[...]
Note que estamos usando uma query fixa para carregar os papéis, em uma situação real teríamos
uma tabela onde carregaríamos essa informação, por exemplo: select usuario as username, papel as authority from papel_usuario where usuario = ?
. Como não temos essa tabela no nosso
projeto, estamos considerando que todos os usuários logados são administradores (ouch).
Mas e se precisarmos de ainda mais flexibilidade? Neste caso podemos implementar nosso próprio
authenticationProvider
, com um bean gerenciado e capaz de usar todos os outros
componentes disponíveis. Vamos começar criando uma classe chamada CustomAuthenticationProvider
dentro do pacote de segurança:
package com.opensanca.trilharest.filmes.seguranca;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UsuariosRepository usuariosRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
UsernamePasswordAuthenticationToken token = (UsernamePasswordAuthenticationToken) authentication;
String nomeDeUsuario = token.getName();
String senha = token.getCredentials().toString();
Usuario usuario = usuariosRepository.findByUsuario(nomeDeUsuario)
.orElseThrow(() -> new BadCredentialsException("Credenciais inválidas"));
if (!passwordEncoder.matches(senha, usuario.getSenha())) {
throw new BadCredentialsException("Credenciais inválidas");
}
return new UsernamePasswordAuthenticationToken(nomeDeUsuario, senha,
Arrays.asList(new SimpleGrantedAuthority("ADMIN")));
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.isAssignableFrom(UsernamePasswordAuthenticationToken.class);
}
}
Para finalizar temos que substituir a configuração de jdbcAuthentication
por uma chamada
ao método de configuração authenticationProvider
:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider);
}
Estamos quase lá! Temos mais dois cenários a tratar: o primeiro é o que fazer no sucesso de
autenticação (hoje o Spring redireciona para a raíz da aplicação, o que não é desejado em
uma API de dados) e o segundo é o redirecionamento automático que está ocorrendo quando
tentamos acessar uma URL sem estar autenticado. Com relação ao primeiro, temos que definir
um novo authenticationSuccessHandler
com o comportamento desejado. Para isso crie uma classe
chamada CustomAuthenticationSuccessHandler
no pacote de segurança:
package com.opensanca.trilharest.filmes.seguranca;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setStatus(HttpStatus.NO_CONTENT.value());
}
}
E registre-a na classe SegurancaConfiguration
:
[...]
http.formLogin()
.failureHandler(new SimpleUrlAuthenticationFailureHandler())
.successHandler(new CustomAuthenticationSuccessHandler());
[...]
E para resolver o segundo caso, precisamos de um authenticationEntryPoint
customizado. Crie
uma classe chamada CustomAuthenticationEntryPoint
no pacote de segurança:
package com.opensanca.trilharest.filmes.seguranca;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
}
}
E registre-a no SegurancaConfiguration
:
[...]
@Configuration
public class SegurancaConfiguration extends WebSecurityConfigurerAdapter {
[...]
@Autowired
private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
[...]
@Override
protected void configure(HttpSecurity http) throws Exception {
// em APIs de dados não usamos CSRF
http.csrf().disable();
http.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint);
http.formLogin()
.failureHandler(new SimpleUrlAuthenticationFailureHandler())
.successHandler(new CustomAuthenticationSuccessHandler());
http.authorizeRequests()
.antMatchers(HttpMethod.POST, "/**").hasAuthority("ADMIN")
.antMatchers(HttpMethod.PUT, "/**").hasAuthority("ADMIN")
.antMatchers(HttpMethod.DELETE, "/**").hasAuthority("ADMIN")
.antMatchers(HttpMethod.GET, "/**").permitAll()
.anyRequest().denyAll();
}
[...]
E assim finalizamos a implementação de segurança feita de maneira manual e com Spring Security
,
baseada em sessão.
Essa implementação tem um problema, no entanto: por ser baseada em sessão, ela impede que escalemos
horizontalmente nosso servidor (se o motivo disso não estiver claro para o leitor, sugiro a
pesquisa sobre o conceito de sticky sessions). Para fugir disso temos que de alguma forma
não usar a sessão para autenticação, mas isso complica um pouco nossa vida. O primeiro passo
nesse nosso objetivo é desligar a sessão de vez, vamos fazer isso na classe
SegurancaConfiguration
:
@Override
protected void configure(HttpSecurity http) throws Exception {
// em APIs de dados não usamos CSRF
http.csrf().disable();
http.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint);
http.sessionManagement()
.disable();
/*http.formLogin()
.failureHandler(new SimpleUrlAuthenticationFailureHandler())
.successHandler(new CustomAuthenticationSuccessHandler());*/
http.authorizeRequests()
.antMatchers(HttpMethod.POST, "/**").hasAuthority("ADMIN")
.antMatchers(HttpMethod.PUT, "/**").hasAuthority("ADMIN")
.antMatchers(HttpMethod.DELETE, "/**").hasAuthority("ADMIN")
.antMatchers(HttpMethod.GET, "/**").permitAll()
.anyRequest().denyAll();
}
Repare que também desativamos o formLogin
, já que ele não nos ajuda mais pois está acoplado
ao armazenamento de usuário na sessão. Mas e agora o que colocaremos no lugar? Utilizaremos
Basic Authentication
. Essa autenticação consiste basicamente na informação de credenciais
em todas as requisições, através de um header especial que é o Authorization
. Vamos ver
como isso funciona:
@Override
protected void configure(HttpSecurity http) throws Exception {
// em APIs de dados não usamos CSRF
http.csrf().disable();
http.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint);
http.sessionManagement()
.disable();
/*http.formLogin()
.failureHandler(new SimpleUrlAuthenticationFailureHandler())
.successHandler(new CustomAuthenticationSuccessHandler());*/
http.httpBasic()
.authenticationEntryPoint(customAuthenticationEntryPoint);
http.authorizeRequests()
.antMatchers(HttpMethod.POST, "/**").hasAuthority("ADMIN")
.antMatchers(HttpMethod.PUT, "/**").hasAuthority("ADMIN")
.antMatchers(HttpMethod.DELETE, "/**").hasAuthority("ADMIN")
.antMatchers(HttpMethod.GET, "/**").permitAll()
.anyRequest().denyAll();
}
Suba a aplicação e teste agora uma requisição protegida enviando o header Authorization
com as credenciais (usuário e senha, isso é possível usando o tipo Basic
). No Postman existe
uma aba chamada Authorization
que fornece uma maneira fácil de fazer isso.
Já ficou bem melhor, mas não me parece muito prático e seguro armazenar o login e senha do usuário durante todo o tempo em que ele estiver usando a aplicação, certo? Nessa hora que entram os chamados tokens de autenticação, que são chaves geradas pelo servidor que permitem aferir posteriormente que uma autenticação com sucesso ocorreu no passado.
Vamos começar criando uma entidade chamada TokenAutenticacao
no pacote de segurança:
package com.opensanca.trilharest.filmes.seguranca;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import java.time.LocalDateTime;
import java.util.UUID;
@Entity
public class TokenAutenticacao {
@Id
private UUID id;
@ManyToOne
private Usuario usuario;
private LocalDateTime validoAte;
// getters e setters omitidos
}
E adicionar o changeset liquibase responsável pela criação da tabela. Primeiro no arquivo
db.changelog-master.yml
:
- changeSet:
id: 4
author: samuelgrigolato
changes:
- sqlFile:
dbms: postgresql
encoding: utf8
path: sqls/0004-cria-tabela-tokens-autenticacao.sql
relativeToChangelogFile: true
E depois o SQL correspondente:
create table token_autenticacao (
id uuid not null,
usuario_id uuid not null,
valido_ate timestamp with time zone,
constraint pk_tokenautenticacao primary key (id),
constraint fk_tokenautenticacao_usuario foreign key (usuario_id)
references usuario (id)
);
Agora que temos a entidade, vamos criar um repositório para ela (interface
TokensAutenticacaoRepository
no pacote de segurança):
package com.opensanca.trilharest.filmes.seguranca;
import org.springframework.data.repository.CrudRepository;
import java.util.UUID;
public interface TokensAutenticacaoRepository extends CrudRepository<TokenAutenticacao, UUID> {
}
A ideia é não usarmos mais o tipo de autenticação Basic
, mas o Bearer
, cujo intuito é
justamente receber chaves que o servidor consegue processar e aceitar ou não, autenticando
o interessado.
Um problema que vamos notar ao tentar usar um header de autorização do tipo Bearer
é que
o Spring Security não possui suporte para ele (diferentemente do tipo Basic
como
vimos), então para adicionar esse suporte temos que implementar um filtro customizado.
Vamos começar criando a classe BearerTokenAuthenticationFilter
no pacote de segurança:
package com.opensanca.trilharest.filmes.seguranca;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.UUID;
@Component
public class BearerTokenAuthenticationFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
String authorization = ((HttpServletRequest) request).getHeader("Authorization");
if (authorization != null && authorization.startsWith("Bearer ")) {
String tokenStr = authorization.substring("Bearer ".length());
UUID token = UUID.fromString(tokenStr);
SecurityContextHolder.getContext().setAuthentication(new BearerTokenAuthentication(token));
}
}
chain.doFilter(request, response);
}
}
E também a classe BearerTokenAuthentication
no mesmo pacote:
package com.opensanca.trilharest.filmes.seguranca;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.List;
import java.util.UUID;
public class BearerTokenAuthentication extends AbstractAuthenticationToken {
private UUID token;
public BearerTokenAuthentication(UUID token) {
super(null);
this.token = token;
}
public BearerTokenAuthentication(UUID token, List<GrantedAuthority> authorities) {
super(authorities);
this.token = token;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return token;
}
@Override
public Object getPrincipal() {
return token;
}
}
Por fim vamos registrar este filtro na classe SegurancaConfiguration
:
/*http.formLogin()
.failureHandler(new SimpleUrlAuthenticationFailureHandler())
.successHandler(new CustomAuthenticationSuccessHandler());*/
/*http.httpBasic()
.authenticationEntryPoint(customAuthenticationEntryPoint);*/
http.addFilterBefore(new BearerTokenAuthenticationFilter(), BasicAuthenticationFilter.class);
Agora podemos adaptar a classe CustomAuthenticationProvider
de modo que ela suporte tokens
do tipo BearerTokenAuthentication
, e tente buscar na base de dados um token existente:
package com.opensanca.trilharest.filmes.seguranca;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.UUID;
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private TokensAutenticacaoRepository tokensAutenticacaoRepository;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
BearerTokenAuthentication bearerToken = (BearerTokenAuthentication) authentication;
UUID token = (UUID)bearerToken.getCredentials();
TokenAutenticacao tokenAutenticacao = tokensAutenticacaoRepository.findOne(token);
if (tokenAutenticacao == null) {
throw new BadCredentialsException("Token inválido");
}
if (tokenAutenticacao.getValidoAte().isBefore(LocalDateTime.now())) {
throw new BadCredentialsException("Token expirado");
}
return new BearerTokenAuthentication(token,
Arrays.asList(new SimpleGrantedAuthority("ADMIN")));
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.isAssignableFrom(BearerTokenAuthentication.class);
}
}
Agora podemos testar, para isso vamos inserir na mão um token (ajuste os IDs se necessário):
insert into token_autenticacao (id, usuario_id, valido_ate)
values ('8ace3001-05f3-4a25-a776-5f9eacd0b518', 'f8fcc35b-5515-4e06-9c47-7332c9494918', '2018-01-01');
E agora a única coisa que falta é fornecermos um endpoint que permite a obtenção desses tokens!
Para isso, vamos voltar com nossa classe LoginAPI
no pacote de segurança, mas agora com uma
cara diferente:
package com.opensanca.trilharest.filmes.seguranca;
import com.opensanca.trilharest.filmes.comum.BindingException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
import java.time.LocalDateTime;
import java.util.UUID;
@RestController
@RequestMapping("/login")
public class LoginAPI {
@Autowired
private UsuariosRepository usuariosRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private TokensAutenticacaoRepository tokensAutenticacaoRepository;
@PostMapping
public UUID login(@Valid @RequestBody CredenciaisDTO credenciaisDTO, BindingResult results) {
if (results.hasErrors()) {
throw new BindingException(results);
}
Usuario usuario = usuariosRepository.findByUsuario(credenciaisDTO.getUsuario())
.orElseThrow(CredencialInvalidaException::new);
if (!passwordEncoder.matches(credenciaisDTO.getSenha(), usuario.getSenha())) {
throw new CredencialInvalidaException();
}
UUID tokenId = UUID.randomUUID();
TokenAutenticacao token = new TokenAutenticacao();
token.setId(tokenId);
token.setUsuario(usuario);
token.setValidoAte(LocalDateTime.now().plusMinutes(30));
tokensAutenticacaoRepository.save(token);
return tokenId;
}
}
Por fim temos que liberar o uso desse endpoint para todos os usuários independente de estarem
ou não autenticados. Fazemos isso no SegurancaConfiguration
:
http.authorizeRequests()
.antMatchers(HttpMethod.POST, "/login").permitAll()
.antMatchers(HttpMethod.POST, "/**").hasAuthority("ADMIN")
.antMatchers(HttpMethod.PUT, "/**").hasAuthority("ADMIN")
.antMatchers(HttpMethod.DELETE, "/**").hasAuthority("ADMIN")
.antMatchers(HttpMethod.GET, "/**").permitAll()
.anyRequest().denyAll();
Agora conseguimos obter tokens através do endpoint POST /login
e usá-los como header
Authentication
do tipo Bearer
. Essa estratégia é bem fácil de ser integrada com frameworks
de frontend como Angular, React etc.
Antes de passarmos para o próximo tópico, como saber dentro de uma action qual o usuário logado
(por exemplo para vinculá-lo como usuário que cadastrou ou alterou um registro)? Isso é bem
simples, basta anotarmos um parâmetro de action com a anotação @AuthenticationPrincipal
, se
esse parâmetro for compatível com o objeto retornado pelo método getPrincipal()
do nosso
BearerTokenAuthentication
(no nosso caso o parâmetro deve ser do tipo UUID) o Spring vai
injetar o parâmetro na action sozinho, veja:
[...]
@RestController
@RequestMapping("/filmes")
@Api("API de filmes em cartaz")
public class FilmesAPI {
[...]
@PostMapping
public UUID cadastrar(@Valid @RequestBody FilmeFormDTO dados, BindingResult results, @AuthenticationPrincipal UUID token) {
System.out.println("Cadastro solicitado pelo token " + token);
validarNomeDuplicado(dados, null, results);
if (results.hasErrors()) {
throw new BindingException(results);
}
Filme entidade = dados.construir();
filmesRepository.save(entidade);
return entidade.getId();
}
[...]
E agora para o último tópico antes de encerrar o assunto de autenticação/autorização: JSON Web Tokens
. Não seria interessante não termos que ter uma tabela de tokens na nossa base de dados?
Ela só vai crescer com o tempo, gera várias consultas a cada requisição, dentre outros problemas.
Será que existe alguma solução? Ocorre que sim, existe, e essa solução é o uso de JWT. JWT é
basicamente uma maneira de entregar um token criptografado de uma maneira que apenas o
servidor consiga descriptografá-lo (isso é diferente de espalhamento, com esses algoritmos
há sim uma maneira de se chegar na informação original). Esse conceito é fortemente baseado em
criptografia de chaves assimétricas, caso o leitor não conheça sobre o assunto sugiro que adicione
na lista de coisas a estudar.
A ideia é, no endpoint POST /login
, gerar um JWT ao invés de salvar um registro no banco, sendo
que neste JWT estarão contidos o ID do usuário que se autenticou com sucess, bem como uma data
de validade. Já no nosso CustomAuthenticationProvider
, ao invés de ir buscar um token no banco
de dados, iremos descriptografar o JWT recebido e confiar na informação (o que vai nos dar
muita performance e simplicidade).
Vamos começar então adicionando uma biblioteca para manipulação de JWTs, no nosso build.gradle
:
compile group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.0'
E então adaptar a classe LoginAPI
para gerar um JWT ao invés de um registro no banco:
package com.opensanca.trilharest.filmes.seguranca;
import com.opensanca.trilharest.filmes.comum.BindingException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
@RequestMapping("/login")
public class LoginAPI {
@Autowired
private UsuariosRepository usuariosRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private TokensAutenticacaoRepository tokensAutenticacaoRepository;
@Value("${autenticacao.secret}")
private String secret;
@Value("${autenticacao.jwtSecret}")
private String jwtSecret;
@PostMapping
public String login(@Valid @RequestBody CredenciaisDTO credenciaisDTO, BindingResult results) {
if (results.hasErrors()) {
throw new BindingException(results);
}
Usuario usuario = usuariosRepository.findByUsuario(credenciaisDTO.getUsuario())
.orElseThrow(CredencialInvalidaException::new);
if (!passwordEncoder.matches(credenciaisDTO.getSenha(), usuario.getSenha())) {
throw new CredencialInvalidaException();
}
/*UUID tokenId = UUID.randomUUID();
TokenAutenticacao token = new TokenAutenticacao();
token.setId(tokenId);
token.setUsuario(usuario);
token.setValidoAte(LocalDateTime.now().plusMinutes(30));
tokensAutenticacaoRepository.save(token);*/
return Jwts.builder()
.setSubject(usuario.getUsuario())
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
}
E agora para a parte de validação, primeiro na classe BearerAuthenticationToken
:
package com.opensanca.trilharest.filmes.seguranca;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import java.util.List;
public class BearerTokenAuthentication extends AbstractAuthenticationToken {
private String jwt;
public BearerTokenAuthentication(String jwt) {
super(null);
this.jwt = jwt;
}
public BearerTokenAuthentication(String jwt, List<GrantedAuthority> authorities) {
super(authorities);
this.jwt = jwt;
setAuthenticated(true);
}
@Override
public Object getCredentials() {
return jwt;
}
@Override
public Object getPrincipal() {
return jwt;
}
}
Na classe BearerTokenAuthenticationFilter
:
package com.opensanca.trilharest.filmes.seguranca;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@Component
public class BearerTokenAuthenticationFilter extends GenericFilterBean {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (request instanceof HttpServletRequest) {
String authorization = ((HttpServletRequest) request).getHeader("Authorization");
if (authorization != null && authorization.startsWith("Bearer ")) {
String jwt = authorization.substring("Bearer ".length());
SecurityContextHolder.getContext().setAuthentication(new BearerTokenAuthentication(jwt));
}
}
chain.doFilter(request, response);
}
}
E por fim na classe CustomAuthenticationProvider
:
package com.opensanca.trilharest.filmes.seguranca;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import java.util.Arrays;
@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
@Autowired
private TokensAutenticacaoRepository tokensAutenticacaoRepository;
@Value("${autenticacao.jwtSecret}")
private String jwtSecret;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
BearerTokenAuthentication bearerToken = (BearerTokenAuthentication) authentication;
String jwt = (String)bearerToken.getCredentials();
try {
String usuario = Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(jwt)
.getBody()
.getSubject();
System.out.println(usuario + " autenticado com sucesso!");
return new BearerTokenAuthentication(jwt,
Arrays.asList(new SimpleGrantedAuthority("ADMIN")));
} catch (SignatureException ex) {
throw new BadCredentialsException("JWT inválido");
}
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.isAssignableFrom(BearerTokenAuthentication.class);
}
}
Antes de testar, temos que adicionar a propriedade autenticacao.jwtSecret
no arquivo
application.properties
colocando uma representação Base64 de uma chave que pode ser
gerada com o seguinte código (isso pode ser executado em uma sessão de debug ou em um
main):
java.util.Base64.getEncoder().encodeToString(
io.jsonwebtoken.impl.crypto.MacProvider.generateKey().getEncoded())
autenticacao.jwtSecret=/74cfVXgvlbGnsuClrFSfyOemI9t2wz32ZDRjuJ0p0AU5VjwRJqgOIUOvabPsBELhp9ENFHGEoNNuXxhlRvAeg==
Pronto, agora temos uma solução usando Spring Security junto com JSON Web Tokens, depois de passar por soluções convencionais usando Cookies e sessão bem como tokens armazenados no banco de dados.