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 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>
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.