Escrevendo Testes de Segurança com @WithMockUser e CSRF
Ao concluir esta lição, você será capaz de:
- Saber que tipo de massa de dados devemos usar ao escrever testes de segurança para controllers Spring MVC e REST.
- Compreender como adicionar um token CSRF válido em requisições HTTP mutáveis (
POST,PUT,DELETE,PATCH) noMockMvc. - Configurar o usuário autenticado (
@WithAnonymousUser,@WithMockUsere@WithUserDetails) utilizado durante o processamento da requisição HTTP pelo controller testado.
Esta lição assume que:
1. Introdução ao Sistema Sob Teste (SUT)
Os requisitos de segurança da nossa aplicação de exemplo são:
- Nossa aplicação possui dois perfis de usuário (roles):
ROLE_USEReROLE_ADMIN. - O perfil
ROLE_ADMINpossui todas as permissões de acesso concedidas ao perfilROLE_USER. - Apenas usuários autenticados que possuam pelo menos o perfil
ROLE_USERtêm permissão para acessar os endpoints da nossa aplicação.
Além disso, adicionamos uma nova tabela chamada user_accounts ao esquema do nosso banco de dados. Essa tabela armazena as contas dos usuários cadastrados na aplicação (como id, email_address, name, password, role e is_enabled).
A seguir, criaremos o dataset do DbUnit que inicializa as contas de usuário no nosso banco de dados.
2. Criando o Dataset de Usuários (users.xml)
Inseriremos os dados de usuários necessários no banco de dados criando um novo arquivo de dataset separado, pois a tabela user_accounts pertence a um domínio lógico diferente das tabelas tasks, tags e tasks_tags.
Como nossa aplicação possui dois perfis de usuário (ROLE_USER e ROLE_ADMIN), criamos um dataset que insere duas linhas na tabela user_accounts. O nome desse arquivo é users.xml, e seu conteúdo é o seguinte:
|
|
Nota: Como esse dataset será reutilizado por múltiplos testes de integração em pacotes distintos, devemos colocá-lo no pacote raiz de recursos
/com/testwithspring/intermediate/users.xml, junto com os demais datasets compartilhados.
A seguir, veremos como criar a classe de constantes que espelha os dados inseridos na tabela user_accounts.
3. Utilizando os Dados de Usuário no Código de Teste (Users)
Criamos o objeto de constantes Users seguindo estes três passos:
- Declarar um
object Usersem Kotlin (que garante uma única instância singleton sem permitir instanciação externa). - Declarar as constantes no sub-objeto
JohnDoecontendo as informações do usuário com perfilROLE_USER. - Declarar as constantes no sub-objeto
AnneAdmincontendo as informações da usuária com perfilROLE_ADMIN.
Após criarmos o objeto de constantes, seu código-fonte em Kotlin ficará assim:
|
|
Duas observações importantes sobre o design da classe
Users:
- Demos nomes de “pessoas reais” (
JohnDoeeAnneAdmin) aos nossos objetos internos porque personas reais são muito mais fáceis de ler e lembrar nos testes do que nomes burocráticos comoRegisteredUsereAdministrator.- Os objetos
JohnDoeeAnneAdmindeclaram constantes ligeiramente diferentes porque incluímos na classeUsersapenas as constantes que são efetivamente referenciadas pelos nossos testes de integração.
Agora estamos prontos para escrever testes de segurança para os nossos controllers Spring MVC!
4. Escrevendo Testes de Segurança para Controllers Spring MVC
Durante esta seção, modificaremos um teste de integração existente que verifica se o controller retorna o status HTTP 200 OK quando uma tarefa existente é excluída do banco de dados (DELETE /api/task/{taskId}). Nosso novo teste garantirá que o status 200 OK seja retornado quando John Doe exclui uma tarefa existente.
Vamos começar inicializando o banco de dados em um estado conhecido.
Passo 1: Inicializando o Banco de Dados com Múltiplos Datasets
Precisamos garantir que o conteúdo de users.xml seja inserido no banco de dados antes da execução dos métodos de teste, juntamente com tasks.xml. Fazemos isso passando ambos os datasets para a anotação @DatabaseSetup:
|
|
Se executarmos esse teste de integração agora, notaremos que ele falha porque o status HTTP 403 Forbidden é retornado! A razão para isso é que nossa requisição DELETE não contém um token CSRF (Cross-Site Request Forgery) válido.
Vamos descobrir como adicionar esse token à nossa requisição HTTP.
Passo 2: Adicionando um Token CSRF Válido à Requisição HTTP
Quando utilizamos a proteção CSRF habilitada por padrão no Spring Security para aplicações web baseadas em sessão/cookies, precisamos incluir um token CSRF válido sempre que enviarmos uma requisição mutável (DELETE, PATCH, POST ou PUT) ao método do controller testado.
Podemos injetar esse token na nossa requisição do MockMvc utilizando o RequestPostProcessor estático csrf() da classe SecurityMockMvcRequestPostProcessors:
|
|
Dica de Segurança: Você também pode testar se sua aplicação rejeita ataques CSRF enviando um token inválido com
.with(csrf().useInvalidToken())e verificando se a resposta retornastatus().isForbidden(403)!
Quando executamos nosso teste de integração após adicionar .with(csrf()), notamos que ele ainda falha, mas agora retorna o código de status HTTP 401 Unauthorized (ou redireciona para a página de login)! Isso acontece porque ainda não configuramos um usuário autenticado, e o Spring Security bloqueia usuários anônimos.
Vamos ver como configurar o usuário autenticado.
Passo 3: Configurando o Usuário Autenticado
O suporte de testes do Spring Security fornece três anotações principais para configurar o usuário autenticado no SecurityContext antes da execução de um método de teste:
@WithAnonymousUser: Define umAnonymousAuthenticationTokenno contexto de segurança. Raramente precisamos adicioná-la no método porque o comportamento padrão sem anotação já é anônimo, mas ela é muito útil quando anotamos a classe de teste com um usuário autenticado padrão e queremos sobrescrever apenas um método@Testespecífico para rodar como usuário anônimo.@WithMockUser: Permite configurar rapidamenteusername,roles(ex:roles = ["USER"]) epasswordem memória no contexto de segurança, sem consultar o banco de dados nem oUserDetailsService.@WithUserDetails: Permite configurar ousername(no nosso caso, o e-mailUsers.JohnDoe.EMAIL_ADDRESS) do usuário cujas informações completas serão carregadas invocando o bean realUserDetailsServiceda aplicação! Após carregar o objetoUserDetailsa partir do banco de dados, o Spring Security o registra noSecurityContextantes do método de teste rodar.
Quando usar
@WithUserDetailsvs@WithMockUser?Nos testes de integração (onde o banco de dados e o
UserDetailsServicereais estão ativos), prefira sempre usar@WithUserDetailsporque ele exercita toda a cadeia real de autenticação e é indispensável em duas situações comuns:
- Quando salvamos informações de auditoria no banco de dados (ex:
@CreatedBy/@LastModifiedBydo Spring Data Envers/Auditing baseados no ID do usuário logado) e queremos fazer asserções sobre essas colunas.- Quando criamos uma classe customizada que implementa
UserDetails(ex:LoggedInUsercontendo oidnumérico da tabelauser_accounts) e o controller ou service faz cast do@AuthenticationPrincipalpara essa classe customizada.
Portanto, para autenticar como John Doe, anotamos nosso método de teste com @WithUserDetails(Users.JohnDoe.EMAIL_ADDRESS).
Após configurarmos o usuário autenticado e o token CSRF, o código completo da nossa classe DeleteTaskAsUserWhenTaskIsFoundTest ficará assim:
|
|
Nota: Você pode adicionar qualquer uma dessas três anotações (
@WithAnonymousUser,@WithMockUser,@WithUserDetails) tanto no nível da classe de teste quanto no nível do método de teste. Quando executamos nosso teste de integração agora, ele passa com sucesso!
Vamos resumir o que aprendemos nesta lição.
Resumo
Esta lição nos ensinou quatro pontos essenciais:
- Devemos criar um dataset dedicado (
users.xml) que insira os diferentes perfis de usuários da aplicação no banco de dados antes da execução dos testes de integração. - Podemos adicionar um token CSRF válido em uma requisição HTTP utilizando o pós-processador
SecurityMockMvcRequestPostProcessors.csrf(). - Em testes de integração com banco de dados real e
UserDetailsServicecustomizado, devemos configurar o usuário autenticado usando a anotação@WithUserDetails. - Podemos declarar
@WithUserDetails(ou@WithMockUser) tanto no nível da classe de teste quanto em métodos de teste individuais.