Spring Security 6 e Spring Boot 3

Spring Boot 3

Caso você tenha uma aplicação configurada com o Spring Boot 2.x.x e queira migrá-la para o Spring Boot 3, sua aplicação provavelmente passará por algumas mudanças. No artigo Spring Boot 3 citei algumas particularidades que você precisa ficar atento ao realizar tal migração.

O ponto é, o Spring Boot 3 sofreu tais mudanças porque ele foi desenvolvido sobre a nova versão do Spring Framework, que é 6. E seguindo essa mesma lógica, o Spring Security 6, lançado recentemente, também foi desenvolvido com base no Spring Framework 6.

Isso implica que, caso sua aplicação Spring Boot 2 seja migrada para o Spring Boot 3 e ela contenha um sistema de segurança baseado no Spring Security 5, terá que migrar para o Spring Security 6. E por conta disso, algumas mudanças deverão ser realizadas.

Neste artigo vamos ver alguns pontos que precisei alterar em meus projetos em relação a mudança de versão do Spring Security.

Do Spring Security 5 para Spring Security 6

Assim como na versão 2.7 do Spring Boot, que surgiu com a necessidade de algumas mudanças na configuração do Spring Security, na versão Spring Boot 3 essas mudanças se estenderam um pouco mais.

Já sabemos que a configuração por herança da classe WebSecurityConfigurerAdapter foi descontinuada a partir da versão 2.7 do Spring Boot. Mas algo importante agora foi modificado e, está diretamente ligado a anotação @EnableWebSecurity.

A anotação @Configuration

Antes da versão 3 do Spring Boot, o starter referente ao Spring Security trazia o seguinte código fonte para a anotação @EnableWebSecurity.

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class, HttpSecurityConfiguration.class})
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
    boolean debug() default false;
}

Observe que entre as anotações presentes temos a @Configuration. Ou seja, ao declarar @EnableWebSecurity na sua classe de configuração do Spring Security, automaticamente ela se transformava em uma classe de configuração. Dessa forma, poderíamos apenas ter um código semelhante a este:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
   // métodos de configuração
}

Porém, o Spring Security 6, deixou de fora a anotação @Configuration no código fonte da anotação @EnableWebSecurity, como podemos ver aqui:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, 
    OAuth2ImportSelector.class,	HttpSecurityConfiguration.class })
@EnableGlobalAuthentication
public @interface EnableWebSecurity {
	boolean debug() default false;
}

Por conta disso, agora é necessário usar a @Configuration na classe de configuração do Spring Security:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {
   // métodos de configuração
}

Eu sei que muitos de você já faziam isso, talvez porque nunca deram uma olhada no código fonte da interface EnableWebSecurity e nem notariam tal mudança. Mas, fica o aviso para aqueles que assim como eu, sabiam que não era necessário tal uso. Por fim, é necessário remover a declaração de herança da classe WebSecurityConfigurerAdapter, já que foi descontinuada.

Outra alteração entre as versões está na anotação @EnableGlobalMethodSecurity, usada para habilitar o uso das anotações com as regras de segurança, como a @PreAuthorize. A partir da versão 6 do Spring Security use a substitua por @EnableMethodSecurity.

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class WebSecurityConfig {
   // métodos de configuração
}

Adeus ao authorizeRequests() e antMatchers()

Sim, é isso mesmo. O método authorizeRequests() foi descontinuado. Em substituição a ele devemos agora usar o método authorizeHttpRequests(). Além disso, o novo método deve ser usado com uma declaração no formato lambda. Pelo menos, foi apenas dessa forma que consegui usá-lo. Logo mais vou exemplificar, por que antes, temos que falar sobre o método antMatchers().

Sim, ele também não fará mais parte das configurações. O antMatchers() foi substituído pelo método requestMatchers(). Dessa vez, até achei que o nome ficou mais adequado para sua serventia. Mas é importante destacar que apenas o nome foi alterado, suas declarações não precisarão ser alteradas além da troca do nome.

Agora vamos ver um exemplo dessa nova forma de criar a configuração:

@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests( (authorize) -> authorize
           .requestMatchers("/").permitAll()
           .requestMatchers("/user/cadastro").hasAuthority(ADMIN)
           .anyRequest().authenticated()
     )
    .formLogin()
       .loginPage("/login")
       .defaultSuccessUrl("/", true)
       .failureUrl("/login-error")
       .permitAll()
    .and()
       .logout()
       .logoutSuccessUrl("/")
       .deleteCookies("JSESSIONID")
    .and()
       .exceptionHandling()
       .accessDeniedPage("/negado");

    return http.build();
}

Declaração de FormLogin, Logout e ExceptionHandling

Como visto no código anterior, parece que nada mudou em relação as declarações das instruções baseadas nos métodos formLogin(), logout() e exceptionHandling(). Mas não é verdade, as declarações apresentadas continuam validas, embora agora possamos declará-las usando lambda, como feito na parte do authorizeHttpRequests().

Veja um exemplo de como ficaria o código com essa atualização:

@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
    http.authorizeHttpRequests( (authorize) -> authorize
           .requestMatchers("/").permitAll()
           .requestMatchers("/user/cadastro").hasAuthority(ADMIN)
           .anyRequest().authenticated()
     ).formLogin( (form) -> form
           .loginPage("/login")
           .defaultSuccessUrl("/", true)
           .failureUrl("/login-error")
           .permitAll()
      ).logout( (logout) -> logout
           .logoutSuccessUrl("/")
           .deleteCookies("JSESSIONID")
      ).exceptionHandling( (ex) -> ex
           .accessDeniedPage("/negado")
      );
      return http.build();
}

Além disso, caso use instruções do tipo rememberMe() ou sessionManagement(), elas também suportam a declaração por meio de expressão lambda. É bom ficar atento a isso, porque assim como as instruções de autorizações aceitam apenas o uso de lambda, pode ser que no futuro, todas as demais instruções sejam permitidas apenas neste formato.

Autenticação com PasswordEnconder

Por muito tempo eu usei em minhas aplicações a seguinte configuração para dizer ao Spring Security a classe que implementava UserDetailsService e também qual o tipo de criptografia deveria ser utilizada:

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {		
		auth.userDetailsService(userDetailsService)
        .passwordEncoder(new BCryptPasswordEncoder());
	}

Porém, essa forma também não é mais aceitar ou reconhecida pelo Spring Security. Agora precisamos criar dois métodos. Um que retorna a interface PasswordEncoder para criar o bean referente ao objeto de criptografia:

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

Na sequencia, você deverá escolher a forma como vai registrar a autenticação. Conforme a documentação, do Spring Security 6, temos duas maneiras de fazer isso. Eu vou exemplificar aquela que achei mais pratica e você poderá mais tarde consultar na documentação a outra forma:

@Bean
public AuthenticationManager authenticationManager(HttpSecurity http,
                    PasswordEncoder passwordEncoder,
                    UsuarioService userDetailsService) throws Exception {
    return http.getSharedObject(AuthenticationManagerBuilder.class)
               .userDetailsService(userDetailsService)
               .passwordEncoder(passwordEncoder)
               .and()
               .build();
}

Veja que agora o retorno do método é um AuthenticationManager, obtido pelo método build() ao final de instrução. Como argumentos do método, passamos o HttpSecurity, o qual vai nos dar acesso aos métodos da configuração do recurso, passamos o PassworEncoder e por fim o UserDetailsService. Todos os três argumentos serão automaticamente injetados no método.

No corpo do método, tirando o uso de getSharedObject() para registrar a classe AuthenticationManagerBuilder, o que me pareceu nada trivial, não tem nada de muito difícil para entender o que foi realizado.

Uma observação importante sobre a configuração do AuthenticationManager, ela não é necessária. Apenas declarando o bean PasswordEncoder a sua classe do tipo UserDetailsService será capaz de realizar o processo de autenticação. Essa dica me foi dada por um seguidor no canal do Youtube e depois de alguns testes eu pude comprovar que realmente funciona. Sendo assim, fica a seu critério decidir se prefere declarar o bean AuthenticationManager ou não.

Thymeleaf Extras Spring Security

Para quem desenvolve aplicações com o tempalte Thymeleaf, sabe que nas página devemos usar alguns atributos para acessar recursos do Spring Security. Até o Spring Boot 2 era necessário incluir no gerenciador de dependências o artefato thymeleaf-extras-springsecurity5.

Quando você migrar sua aplicação para o Spring Boot 3 vai precisar alterar a dependência referente para o artefato thymeleaf-extras-springsecurity6:

<dependency>
		 <groupId>org.thymeleaf.extras</groupId>
		 <artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>

Vídeo Migração Spring Security 5 para o Spring Security 6

Conclusão

Até o momento foram essas a modificações, referentes ao Spring Security, que precisei realizar nas minhas aplicações ao migrá-las para o Spring Boot 3. Caso você ainda não leu nada a respeito sobre o básico na migração do Spring Boot 2 para o Spring Boot 3, não deixe de ler o meu artigo anterior sobre a migração, onde cito alguns itens que sofreram alterações.

Então, boa sorte com sua migração e primeiro teste bem antes de envia-la para produção.

Referencias

* Spring Security

* Spring Security – Migrating to 6.0

* Spring Security 5.8 and 6.0 are now GA

Ballem

Marcio Ballem é bacharel em Sistemas de Informação pelo Centro Universitário Franciscano em Santa Maria/RS. Tem experiência com desenvolvimento Delphi e Java em projetos para gestão pública e acadêmica. Possui certificação em Java, OCJP 6.

Você pode gostar...