Spring Security com JWT para API REST

Publicados: 2022-03-11

Spring é considerado um framework confiável no ecossistema Java e é amplamente utilizado. Não é mais válido se referir ao Spring como um framework, pois é mais um termo abrangente que abrange vários frameworks. Uma dessas estruturas é o Spring Security, que é uma estrutura de autenticação e autorização poderosa e personalizável. É considerado o padrão de fato para proteger aplicativos baseados em Spring.

Apesar de sua popularidade, devo admitir que, quando se trata de aplicativos de página única, não é simples e direto de configurar. Eu suspeito que o motivo é que ele começou mais como uma estrutura orientada a aplicativos MVC, onde a renderização de páginas da Web acontece no lado do servidor e a comunicação é baseada em sessão.

Se o back-end for baseado em Java e Spring, faz sentido usar o Spring Security para autenticação/autorização e configurá-lo para comunicação sem estado. Embora existam muitos artigos explicando como isso é feito, para mim ainda era frustrante configurá-lo pela primeira vez e tive que ler e resumir informações de várias fontes. É por isso que decidi escrever este artigo, onde tentarei resumir e cobrir todos os detalhes sutis e pontos fracos necessários que você pode encontrar durante o processo de configuração.

Definição de Terminologia

Antes de mergulhar nos detalhes técnicos, quero definir explicitamente a terminologia usada no contexto do Spring Security apenas para ter certeza de que todos falamos a mesma língua.

Estes são os termos que precisamos abordar:

  • A autenticação refere-se ao processo de verificação da identidade de um usuário, com base nas credenciais fornecidas. Um exemplo comum é inserir um nome de usuário e uma senha ao fazer login em um site. Você pode pensar nisso como uma resposta à pergunta Quem é você? .
  • A autorização refere-se ao processo de determinar se um usuário tem permissão adequada para executar uma ação específica ou ler dados específicos, assumindo que o usuário foi autenticado com sucesso. Você pode pensar nisso como uma resposta à pergunta Um usuário pode fazer/ler isso? .
  • Princípio refere-se ao usuário atualmente autenticado.
  • A autoridade concedida refere-se à permissão do usuário autenticado.
  • Função refere-se a um grupo de permissões do usuário autenticado.

Criando um aplicativo Spring básico

Antes de passar para a configuração do framework Spring Security, vamos criar um aplicativo web básico do Spring. Para isso, podemos usar um Spring Initializr e gerar um projeto modelo. Para um aplicativo web simples, apenas uma dependência do framework web Spring é suficiente:

 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies>

Depois de criar o projeto, podemos adicionar um controlador REST simples a ele da seguinte maneira:

 @RestController @RequestMapping("hello") public class HelloRestController { @GetMapping("user") public String helloUser() { return "Hello User"; } @GetMapping("admin") public String helloAdmin() { return "Hello Admin"; } }

Depois disso, se construirmos e executarmos o projeto, podemos acessar as seguintes URLs no navegador da web:

  • http://localhost:8080/hello/user retornará a string Hello User .
  • http://localhost:8080/hello/admin retornará a string Hello Admin .

Agora, podemos adicionar a estrutura Spring Security ao nosso projeto, e podemos fazer isso adicionando a seguinte dependência ao nosso arquivo pom.xml :

 <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies>

Adicionar outras dependências da estrutura Spring normalmente não tem um efeito imediato em um aplicativo até que forneçamos a configuração correspondente, mas o Spring Security é diferente porque tem um efeito imediato, e isso geralmente confunde os novos usuários. Após adicioná-lo, se reconstruirmos e executarmos o projeto e tentarmos acessar uma das URLs mencionadas em vez de visualizar o resultado, seremos redirecionados para http://localhost:8080/login . Este é o comportamento padrão porque a estrutura Spring Security requer autenticação pronta para uso para todas as URLs.

Para passar a autenticação, podemos usar o nome de user padrão e encontrar uma senha gerada automaticamente em nosso console:

 Using generated security password: 1fc15145-dfee-4bec-a009-e32ca21c77ce

Lembre-se de que a senha muda cada vez que executamos novamente o aplicativo. Se quisermos alterar esse comportamento e tornar a senha estática, podemos adicionar a seguinte configuração ao nosso arquivo application.properties :

 spring.security.user.password=Test12345_

Agora, se inserirmos as credenciais no formulário de login, seremos redirecionados de volta ao nosso URL e veremos o resultado correto. Observe que o processo de autenticação pronto para uso é baseado em sessão e, se quisermos sair, podemos acessar o seguinte URL: http://localhost:8080/logout

Esse comportamento pronto para uso pode ser útil para aplicativos da Web MVC clássicos em que temos autenticação baseada em sessão, mas no caso de aplicativos de página única, geralmente não é útil porque, na maioria dos casos de uso, temos renderização e autenticação sem estado baseada em JWT. Nesse caso, teremos que customizar fortemente o framework Spring Security, o que faremos no restante do artigo.

Como exemplo, implementaremos um aplicativo web de livraria clássico e criaremos um back-end que fornecerá APIs CRUD para criar autores e livros, além de APIs para gerenciamento e autenticação de usuários.

Visão geral da arquitetura de segurança Spring

Antes de começarmos a personalizar a configuração, vamos primeiro discutir como a autenticação do Spring Security funciona nos bastidores.

O diagrama a seguir apresenta o fluxo e mostra como as solicitações de autenticação são processadas:

Arquitetura de segurança Spring

Arquitetura de segurança Spring

Agora, vamos dividir este diagrama em componentes e discutir cada um deles separadamente.

Cadeia de Filtros de Segurança Spring

Quando você adiciona a estrutura Spring Security ao seu aplicativo, ela registra automaticamente uma cadeia de filtros que intercepta todas as solicitações recebidas. Essa cadeia consiste em vários filtros e cada um deles lida com um caso de uso específico.

Por exemplo:

  • Verifique se a URL solicitada é acessível publicamente, com base na configuração.
  • No caso de autenticação baseada em sessão, verifique se o usuário já está autenticado na sessão atual.
  • Verifique se o usuário está autorizado a realizar a ação solicitada e assim por diante.

Um detalhe importante que quero mencionar é que os filtros Spring Security são registrados com a ordem mais baixa e são os primeiros filtros invocados. Para alguns casos de uso, se você quiser colocar seu filtro personalizado na frente deles, precisará adicionar preenchimento ao pedido. Isso pode ser feito com a seguinte configuração:

 spring.security.filter.order=10

Assim que adicionarmos essa configuração ao nosso arquivo application.properties , teremos espaço para 10 filtros personalizados na frente dos filtros Spring Security.

Gerenciador de Autenticação

Você pode pensar no AuthenticationManager como um coordenador onde você pode registrar vários provedores e, com base no tipo de solicitação, ele entregará uma solicitação de autenticação ao provedor correto.

Provedor de autenticação

AuthenticationProvider processa tipos específicos de autenticação. Sua interface expõe apenas duas funções:

  • authenticate executa a autenticação com a solicitação.
  • supports verifica se este provedor oferece suporte ao tipo de autenticação indicado.

Uma implementação importante da interface que estamos usando em nosso projeto de exemplo é DaoAuthenticationProvider , que recupera detalhes do usuário de um UserDetailsService .

UserDetailsService

UserDetailsService é descrito como uma interface central que carrega dados específicos do usuário na documentação do Spring.

Na maioria dos casos de uso, os provedores de autenticação extraem informações de identidade do usuário com base nas credenciais de um banco de dados e, em seguida, realizam a validação. Como esse caso de uso é tão comum, os desenvolvedores do Spring decidiram extraí-lo como uma interface separada, que expõe a função única:

  • loadUserByUsername aceita o nome de usuário como parâmetro e retorna o objeto de identidade do usuário.

Autenticação usando JWT com Spring Security

Depois de discutir os aspectos internos da estrutura Spring Security, vamos configurá-la para autenticação sem estado com um token JWT.

Para personalizar o Spring Security, precisamos de uma classe de configuração anotada com a anotação @EnableWebSecurity em nosso classpath. Além disso, para simplificar o processo de personalização, a estrutura expõe uma classe WebSecurityConfigurerAdapter . Vamos estender este adaptador e substituir ambas as suas funções para:

  1. Configure o gerenciador de autenticação com o provedor correto
  2. Configure a segurança da web (URLs públicos, URLs privados, autorização, etc.)
 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { // TODO configure authentication manager } @Override protected void configure(HttpSecurity http) throws Exception { // TODO configure web security } }

Em nosso aplicativo de exemplo, armazenamos identidades de usuários em um banco de dados MongoDB, na coleção de users . Essas identidades são mapeadas pela entidade User e suas operações CRUD são definidas pelo repositório UserRepo Spring Data.

Agora, quando aceitamos a solicitação de autenticação, precisamos recuperar a identidade correta do banco de dados usando as credenciais fornecidas e depois verificá-la. Para isso, precisamos da implementação da interface UserDetailsService , que é definida da seguinte forma:

 public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }

Aqui, podemos ver que é necessário retornar o objeto que implementa a interface UserDetails , e nossa entidade User o implementa (para detalhes de implementação, consulte o repositório do projeto de exemplo). Considerando o fato de que ele expõe apenas o protótipo de função única, podemos tratá-lo como uma interface funcional e fornecer a implementação como uma expressão lambda.

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; public SecurityConfig(UserRepo userRepo) { this.userRepo = userRepo; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(username -> userRepo .findByUsername(username) .orElseThrow( () -> new UsernameNotFoundException( format("User: %s, not found", username) ) )); } // Details omitted for brevity }

Aqui, a chamada de função auth.userDetailsService iniciará a instância DaoAuthenticationProvider usando nossa implementação da interface UserDetailsService e a registrará no gerenciador de autenticação.

Junto com o provedor de autenticação, precisamos configurar um gerenciador de autenticação com o esquema de codificação de senha correto que será usado para verificação de credenciais. Para isso, precisamos expor a implementação preferencial da interface PasswordEncoder como um bean.

Em nosso projeto de exemplo, usaremos o algoritmo de hash de senha bcrypt.

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; public SecurityConfig(UserRepo userRepo) { this.userRepo = userRepo; } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(username -> userRepo .findByUsername(username) .orElseThrow( () -> new UsernameNotFoundException( format("User: %s, not found", username) ) )); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } // Details omitted for brevity }

Tendo configurado o gerenciador de autenticação, agora precisamos configurar a segurança da web. Estamos implementando uma API REST e precisamos de autenticação sem estado com um token JWT; portanto, precisamos definir as seguintes opções:

  • Habilite o CORS e desabilite o CSRF.
  • Defina o gerenciamento de sessão como sem estado.
  • Defina o manipulador de exceção de solicitações não autorizadas.
  • Defina permissões em endpoints.
  • Adicionar filtro de token JWT.

Esta configuração é implementada da seguinte forma:

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { private final UserRepo userRepo; private final JwtTokenFilter jwtTokenFilter; public SecurityConfig(UserRepo userRepo, JwtTokenFilter jwtTokenFilter) { this.userRepo = userRepo; this.jwtTokenFilter = jwtTokenFilter; } // Details omitted for brevity @Override protected void configure(HttpSecurity http) throws Exception { // Enable CORS and disable CSRF http = http.cors().and().csrf().disable(); // Set session management to stateless http = http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and(); // Set unauthorized requests exception handler http = http .exceptionHandling() .authenticationEntryPoint( (request, response, ex) -> { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage() ); } ) .and(); // Set permissions on endpoints http.authorizeRequests() // Our public endpoints .antMatchers("/api/public/**").permitAll() .antMatchers(HttpMethod.GET, "/api/author/**").permitAll() .antMatchers(HttpMethod.POST, "/api/author/search").permitAll() .antMatchers(HttpMethod.GET, "/api/book/**").permitAll() .antMatchers(HttpMethod.POST, "/api/book/search").permitAll() // Our private endpoints .anyRequest().authenticated(); // Add JWT token filter http.addFilterBefore( jwtTokenFilter, UsernamePasswordAuthenticationFilter.class ); } // Used by spring security if CORS is enabled. @Bean public CorsFilter corsFilter() { UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration config = new CorsConfiguration(); config.setAllowCredentials(true); config.addAllowedOrigin("*"); config.addAllowedHeader("*"); config.addAllowedMethod("*"); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

Observe que adicionamos o JwtTokenFilter antes do UsernamePasswordAuthenticationFilter interno do Spring Security. Estamos fazendo isso porque precisamos de acesso à identidade do usuário neste momento para realizar autenticação/autorização, e sua extração acontece dentro do filtro de token JWT com base no token JWT fornecido. Isso é implementado da seguinte forma:

 @Component public class JwtTokenFilter extends OncePerRequestFilter { private final JwtTokenUtil jwtTokenUtil; private final UserRepo userRepo; public JwtTokenFilter(JwtTokenUtil jwtTokenUtil, UserRepo userRepo) { this.jwtTokenUtil = jwtTokenUtil; this.userRepo = userRepo; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { // Get authorization header and validate final String header = request.getHeader(HttpHeaders.AUTHORIZATION); if (isEmpty(header) || !header.startsWith("Bearer ")) { chain.doFilter(request, response); return; } // Get jwt token and validate final String token = header.split(" ")[1].trim(); if (!jwtTokenUtil.validate(token)) { chain.doFilter(request, response); return; } // Get user identity and set it on the spring security context UserDetails userDetails = userRepo .findByUsername(jwtTokenUtil.getUsername(token)) .orElse(null); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails == null ? List.of() : userDetails.getAuthorities() ); authentication.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(request, response); } }

Antes de implementar nossa função de API de login, precisamos cuidar de mais uma etapa - precisamos acessar o gerenciador de autenticação. Por padrão, não é acessível publicamente e precisamos expô-lo explicitamente como um bean em nossa classe de configuração.

Isso pode ser feito da seguinte forma:

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }

E agora, estamos prontos para implementar nossa função de API de login:

 @Api(tags = "Authentication") @RestController @RequestMapping(path = "api/public") public class AuthApi { private final AuthenticationManager authenticationManager; private final JwtTokenUtil jwtTokenUtil; private final UserViewMapper userViewMapper; public AuthApi(AuthenticationManager authenticationManager, JwtTokenUtil jwtTokenUtil, UserViewMapper userViewMapper) { this.authenticationManager = authenticationManager; this.jwtTokenUtil = jwtTokenUtil; this.userViewMapper = userViewMapper; } @PostMapping("login") public ResponseEntity<UserView> login(@RequestBody @Valid AuthRequest request) { try { Authentication authenticate = authenticationManager .authenticate( new UsernamePasswordAuthenticationToken( request.getUsername(), request.getPassword() ) ); User user = (User) authenticate.getPrincipal(); return ResponseEntity.ok() .header( HttpHeaders.AUTHORIZATION, jwtTokenUtil.generateAccessToken(user) ) .body(userViewMapper.toUserView(user)); } catch (BadCredentialsException ex) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); } } }

Aqui, verificamos as credenciais fornecidas usando o gerenciador de autenticação e, em caso de sucesso, geramos o token JWT e o retornamos como um cabeçalho de resposta junto com as informações de identidade do usuário no corpo da resposta.

Autorização com Spring Security

Na seção anterior, configuramos um processo de autenticação e configuramos URLs públicos/privados. Isso pode ser suficiente para aplicativos simples, mas para a maioria dos casos de uso do mundo real, sempre precisamos de políticas de acesso baseadas em função para nossos usuários. Neste capítulo, abordaremos esse problema e configuraremos um esquema de autorização baseado em função usando a estrutura Spring Security.

Em nosso aplicativo de exemplo, definimos as três funções a seguir:

  • USER_ADMIN nos permite gerenciar os usuários do aplicativo.
  • AUTHOR_ADMIN nos permite gerenciar autores.
  • BOOK_ADMIN nos permite gerenciar livros.

Agora, precisamos aplicá-los aos URLs correspondentes:

  • api/public é acessível publicamente.
  • api/admin/user pode acessar usuários com a função USER_ADMIN .
  • api/author pode acessar usuários com a função AUTHOR_ADMIN .
  • api/book pode acessar usuários com a função BOOK_ADMIN .

A estrutura Spring Security nos fornece duas opções para configurar o esquema de autorização:

  • Configuração baseada em URL
  • Configuração baseada em anotações

Primeiro, vamos ver como funciona a configuração baseada em URL. Ele pode ser aplicado à configuração de segurança da Web da seguinte maneira:

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Override protected void configure(HttpSecurity http) throws Exception { // Enable CORS and disable CSRF http = http.cors().and().csrf().disable(); // Set session management to stateless http = http .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and(); // Set unauthorized requests exception handler http = http .exceptionHandling() .authenticationEntryPoint( (request, response, ex) -> { response.sendError( HttpServletResponse.SC_UNAUTHORIZED, ex.getMessage() ); } ) .and(); // Set permissions on endpoints http.authorizeRequests() // Our public endpoints .antMatchers("/api/public/**").permitAll() .antMatchers(HttpMethod.GET, "/api/author/**").permitAll() .antMatchers(HttpMethod.POST, "/api/author/search").permitAll() .antMatchers(HttpMethod.GET, "/api/book/**").permitAll() .antMatchers(HttpMethod.POST, "/api/book/search").permitAll() // Our private endpoints .antMatchers("/api/admin/user/**").hasRole(Role.USER_ADMIN) .antMatchers("/api/author/**").hasRole(Role.AUTHOR_ADMIN) .antMatchers("/api/book/**").hasRole(Role.BOOK_ADMIN) .anyRequest().authenticated(); // Add JWT token filter http.addFilterBefore( jwtTokenFilter, UsernamePasswordAuthenticationFilter.class ); } // Details omitted for brevity }

Como você pode ver, essa abordagem é simples e direta, mas tem uma desvantagem. O esquema de autorização em nosso aplicativo pode ser complexo e, se definirmos todas as regras em um único local, ele se tornará muito grande, complexo e difícil de ler. Por isso, geralmente prefiro usar a configuração baseada em anotação.

A estrutura Spring Security define as seguintes anotações para segurança da web:

  • @PreAuthorize suporta Spring Expression Language e é usado para fornecer controle de acesso baseado em expressão antes de executar o método.
  • @PostAuthorize suporta Spring Expression Language e é usado para fornecer controle de acesso baseado em expressão após a execução do método (fornece a capacidade de acessar o resultado do método).
  • @PreFilter suporta Spring Expression Language e é usado para filtrar a coleção ou matrizes antes de executar o método com base nas regras de segurança personalizadas que definimos.
  • @PostFilter suporta Spring Expression Language e é usado para filtrar a coleção ou matrizes retornadas após a execução do método com base nas regras de segurança personalizadas que definimos (fornece a capacidade de acessar o resultado do método).
  • @Secured não oferece suporte ao Spring Expression Language e é usado para especificar uma lista de funções em um método.
  • @RolesAllowed não oferece suporte ao Spring Expression Language e é a anotação equivalente do JSR 250 da anotação @Secured .

Essas anotações são desabilitadas por padrão e podem ser habilitadas em nosso aplicativo da seguinte forma:

 @EnableWebSecurity @EnableGlobalMethodSecurity( securedEnabled = true, jsr250Enabled = true, prePostEnabled = true ) public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity }


securedEnabled = true habilita a anotação @Secured .
jsr250Enabled = true habilita a anotação @RolesAllowed .
prePostEnabled = true habilita as @PreAuthorize , @PostAuthorize , @PreFilter , @PostFilter .

Depois de habilitá-los, podemos aplicar políticas de acesso baseadas em função em nossos endpoints de API como este:

 @Api(tags = "UserAdmin") @RestController @RequestMapping(path = "api/admin/user") @RolesAllowed(Role.USER_ADMIN) public class UserAdminApi { // Details omitted for brevity } @Api(tags = "Author") @RestController @RequestMapping(path = "api/author") public class AuthorApi { // Details omitted for brevity @RolesAllowed(Role.AUTHOR_ADMIN) @PostMapping public void create() { } @RolesAllowed(Role.AUTHOR_ADMIN) @PutMapping("{id}") public void edit() { } @RolesAllowed(Role.AUTHOR_ADMIN) @DeleteMapping("{id}") public void delete() { } @GetMapping("{id}") public void get() { } @GetMapping("{id}/book") public void getBooks() { } @PostMapping("search") public void search() { } } @Api(tags = "Book") @RestController @RequestMapping(path = "api/book") public class BookApi { // Details omitted for brevity @RolesAllowed(Role.BOOK_ADMIN) @PostMapping public BookView create() { } @RolesAllowed(Role.BOOK_ADMIN) @PutMapping("{id}") public void edit() { } @RolesAllowed(Role.BOOK_ADMIN) @DeleteMapping("{id}") public void delete() { } @GetMapping("{id}") public void get() { } @GetMapping("{id}/author") public void getAuthors() { } @PostMapping("search") public void search() { } }

Observe que as anotações de segurança podem ser fornecidas tanto no nível da classe quanto no nível do método.

Os exemplos demonstrados são simples e não representam cenários do mundo real, mas o Spring Security fornece um rico conjunto de anotações e você pode manipular um esquema de autorização complexo se optar por usá-los.

Prefixo padrão do nome da função

Nesta subseção separada, quero enfatizar mais um detalhe sutil que confunde muitos novos usuários.

A estrutura Spring Security diferencia dois termos:

  • Authority representa uma permissão individual.
  • Role representa um grupo de permissões.

Ambos podem ser representados com uma única interface chamada GrantedAuthority e posteriormente verificados com Spring Expression Language dentro das anotações do Spring Security da seguinte forma:

  • Authority : @PreAuthorize(“hasAuthority('EDIT_BOOK')”)
  • Role : @PreAuthorize(“hasRole('BOOK_ADMIN')”)

Para tornar a diferença entre esses dois termos mais explícita, a estrutura Spring Security adiciona um prefixo ROLE_ ao nome da função por padrão. Portanto, em vez de verificar um papel chamado BOOK_ADMIN , ele verificará ROLE_BOOK_ADMIN .

Pessoalmente, acho esse comportamento confuso e prefiro desativá-lo em meus aplicativos. Ele pode ser desabilitado dentro da configuração do Spring Security da seguinte forma:

 @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { // Details omitted for brevity @Bean GrantedAuthorityDefaults grantedAuthorityDefaults() { return new GrantedAuthorityDefaults(""); // Remove the ROLE_ prefix } }

Testando com Spring Security

Para testar nossos endpoints com testes de unidade ou integração ao usar a estrutura Spring Security, precisamos adicionar a dependência spring-security-test junto com o spring-boot-starter-test . Nosso arquivo de compilação pom.xml ficará assim:

 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency>

Essa dependência nos dá acesso a algumas anotações que podem ser usadas para adicionar contexto de segurança às nossas funções de teste.

Essas anotações são:

  • @WithMockUser pode ser adicionado a um método de teste para emular a execução com um usuário simulado.
  • @WithUserDetails pode ser adicionado a um método de teste para emular a execução com UserDetails retornado do UserDetailsService .
  • @WithAnonymousUser pode ser adicionado a um método de teste para emular a execução com um usuário anônimo. Isso é útil quando um usuário deseja executar a maioria dos testes como um usuário específico e substituir alguns métodos para ser anônimo.
  • @WithSecurityContext determina qual SecurityContext usar e todas as três anotações descritas acima são baseadas nele. Se tivermos um caso de uso específico, podemos criar nossa própria anotação que usa @WithSecurityContext para criar qualquer SecurityContext que desejarmos. Sua discussão está fora do escopo do nosso artigo e consulte a documentação do Spring Security para obter mais detalhes.

A maneira mais fácil de executar os testes com um usuário específico é usar a anotação @WithMockUser . Podemos criar um usuário simulado com ele e executar o teste da seguinte forma:

 @Test @WithMockUser(username="[email protected]", roles={"USER_ADMIN"}) public void test() { // Details omitted for brevity }

Essa abordagem tem algumas desvantagens, no entanto. Primeiro, o usuário simulado não existe e, se você executar o teste de integração, que posteriormente consulta as informações do usuário no banco de dados, o teste falhará. Segundo, o usuário simulado é a instância da classe org.springframework.security.core.userdetails.User , que é a implementação interna da interface UserDetails do framework Spring, e se tivermos nossa própria implementação, isso pode causar conflitos mais tarde, durante execução do teste.

Se as desvantagens anteriores são bloqueadoras para nosso aplicativo, a anotação @WithUserDetails é o caminho a seguir. Ele é usado quando temos implementações personalizadas de UserDetails e UserDetailsService . Ele assume que o usuário existe, então temos que criar a linha real no banco de dados ou fornecer a instância simulada UserDetailsService antes de executar os testes.

É assim que podemos usar esta anotação:

 @Test @WithUserDetails("[email protected]") public void test() { // Details omitted for brevity }

Esta é uma anotação preferencial nos testes de integração do nosso projeto de amostra porque temos implementações personalizadas das interfaces mencionadas.

Usar @WithAnonymousUser permite executar como usuário anônimo. Isso é especialmente conveniente quando você deseja executar a maioria dos testes com um usuário específico, mas alguns testes como um usuário anônimo. Por exemplo, o seguinte executará os casos de teste test1 e test2 com um usuário simulado e test3 com um usuário anônimo:

 @SpringBootTest @AutoConfigureMockMvc @WithMockUser public class WithUserClassLevelAuthenticationTests { @Test public void test1() { // Details omitted for brevity } @Test public void test2() { // Details omitted for brevity } @Test @WithAnonymousUser public void test3() throws Exception { // Details omitted for brevity } }

Empacotando

No final, gostaria de mencionar que o framework Spring Security provavelmente não ganhará nenhum concurso de beleza e definitivamente tem uma curva de aprendizado íngreme. Eu encontrei muitas situações em que ele foi substituído por alguma solução caseira devido à sua complexidade de configuração inicial. Mas uma vez que os desenvolvedores entendem seus aspectos internos e conseguem configurar a configuração inicial, torna-se relativamente simples de usar.

Neste artigo, tentei demonstrar todos os detalhes sutis da configuração e espero que você ache os exemplos úteis. Para exemplos de código completos, consulte o repositório Git do meu projeto Spring Security de amostra.