Configurando o Spring Security através de código Java (sem XML)

Nesse artigo, iremos ver como adicionar a uma aplicação Spring existente as classes necessárias para criar uma camada de segurança, que permitirá definir quem pode acessar o sistema e o quê poderá ser acessado.

Começaremos aqui citando as duas classes básicas para adicionar a camada de segurança de uma aplicação Spring:

  • SecurityConfig
Define as configurações básicas da camada de segurança da aplicação. Dois métodos são necessários aqui, ambos chamado configure, e diferenciados pelo tipo de seu parâmetro:
configure(AuthenticationManagerBuilder auth) -> define como os dados de login serão processados. Algumas opções são processar esses dados através de uma classe que implemente a interface UserDetailsService (que deve ser um atributo da classe SecurityConfig anotada com @Autowired) ou uma classe que estenda AuthenticationManager (que poderá chamar um ou várias AuthenticationProvider).
Um exemplo de implementação para essa classe usando UserDetailsService seria:
   @Autowired
   private UserDetailsService usuario;
   protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
      .userDetailsService(usuario)
      .passwordEncoder(encoder());
   }
   private Md5PasswordEncoder encoder() {
      return new Md5PasswordEncoder();
   }
configure(HttpSecurity http) -> define quais páginas poderão ser acessadas sem necessidade de autenticação, página de login, páginas iniciais (acessadas após o login), páginas de erro, nomes de atributos, dentre outros.
Exemplo:
   protected void configure(HttpSecurity http) throws Exception {
http
      .csrf()
      .disable()
      .authorizeRequests()
      .antMatchers("/acesso/erro").permitAll()
      .antMatchers("/bootstrap/**", "/jquery-ui/**", "/extras/**").permitAll()
      .anyRequest().authenticated()
      .and()
      .formLogin()
      .loginPage("/acesso/login").permitAll()
      .loginProcessingUrl("/login").permitAll()
      .usernameParameter("login")
      .passwordParameter("senha")
      .successHandler(new CustomAuthenticationSuccessHandler())
      .failureHandler(new CustomAuthenticationFailureHandler())
      .and()
      .rememberMe()
      .key("remember-me")
      .useSecureCookie(true)
      .and()
      .logout()
      .logoutUrl("/logout")
      .logoutSuccessUrl("/acesso/login").permitAll();
   }
  • SecurityWebApplicationInitializer
Essa classe é necessária para registrar a classe SecurityConfig no pacote WAR que contém a aplicação. Uma implementação mínima para esse método apenar referenciaria a classe SecurityConfig existente:
   public class SecurityWebApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {
      public SecurityWebApplicationInitializer() {
         super(SecurityConfig.class);
      }
   }

AuthenticationManager e AuthenticationProvider

Como foi dito anteriormente, caso você queira implementar algum esquema mais complexo de autenticação, precisará implementar uma classe AuthenticationManager personalizada, além de diversas classes AuthenticationProvider, uma para cada tipo de autenticação que sua aplicação permitir.
Na classe SecurityConfig, você deve incluir o seguinte código para seu método configure():
   @Bean
   @Override
   public AuthenticationManager authenticationManagerBean() throws Exception {
      return new CustomAuthenticationManager();
   }
Não há necessidade de atributos com a anaotação @Autowired aqui.
Agora, você precisa adicionar ao seu projeto uma classe CustomAuthenticationManager. Essa classe deve identificar o método de autenticação do usuário e direciona-lo para o AuthenticationProvider apropriado.
Um exemplo mínimo de implementação para essa classe seria:
   public class CustomAuthenticationManager implements AuthenticationManager {
      @Autowired
      private CustomAuthenticationProvider authenticationProvider;
      @Override
      public Authentication authenticate(Authentication authentication) throws AuthenticationException {
         System.out.println("CustomAuthenticationManager.authenticate");
         return authenticationProvider.authenticate(authentication);
      }
   }
Você deve criar uma classe específica para cada método de autenticação permitido, que seguirá o seguinte modelo (a classe abaixo faz a autenticação por usuário e senha. Você deve adapta-la para o token de autenticação que quiser usar):
   @Component
   public class CustomAuthenticationProvider implements AuthenticationProvider {
   @Autowired
   private AuthenticationService usuario;
   @Autowired
   private BCryptPasswordEncoder encoder;
   @Override
   public Authentication authenticate(Authentication authentication) throws AuthenticationException {
      String name = authentication.getName();
      String password = authentication.getCredentials().toString();
      UserDetails user = usuario.loadUserByUsername(name);
      if(encoder.matches(user.getPassword(), password)) {
         Authentication auth = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), user.getAuthorities());
         return auth;
      }
      else {
         return null;
      }
   }
   @Override
   public boolean supports(Class<?> authentication) {
      return authentication.equals(UsernamePasswordAuthenticationToken.class);
   }

AuthenticationSuccessHandler e AuthenticationFailureHandler

O método authenticate de suas classes AuthenticationManager/AuthenticationProvider podem retornar tanto um objeto Authentication quando um valor nulo. Caso retorne um objeto Authentication válido,  sua aplicação será direcionada para uma página de sucesso de login. Caso retorno um valor nulo, sua aplicação será direcionada para uma página de erro.
Essas ações são implementadas através de classes derivadas de AuthenticationSuccessHandler e AuthenticationFailureHandler, que possuem os métodos onAuthenticationSuccess e onAuthenticationFailure, respectivamente. Esses métodos direcionam o usuário para a página inicial correta, podendo também definir atributos para a sessão que podem ser acessados por qualquer página que o usuário abrir enquanto estiver logado no sistema.

MethodSecurity

Com o usuário logado, você precisa agora definir quais métodos do controller ou das classes service ele pode acessar. Para conseguir isso, você deve anotar o método com a tag @PreAuthorize. Para isso, você precisa adicionar ao seu projeto uma classe de configuração derivada de GlobalMethodSecurityConfiguration.
Essa classe precisa implementar apenas um método, createExpression(), que referencia uma classe derivada de PermissionEvaluator, que processa as verificações de permissões. Assim, essa classe teria essa implementação:
   @Configuration
   @ComponentScan(value="com.spring.webapp.lojavirtual")
   @EnableGlobalMethodSecurity(prePostEnabled = true)
   public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
      protected MethodSecurityExpressionHandler createExpressionHandler() {
         System.out.println("MethodSecurityConfig.createExpressionHandler");
         DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(new CustomPermissionEvaluator());
         return expressionHandler;
      }
   }
A classe CustomPermissionEvaluator, referenciada no método createExpressionHandler() acima, deve ter um método hasPermission(), que retorna uma valor booleano que informa se o usuário possui ou não uma dada permissão.
Uma implementação para essa classe seria a seguinte:
   @Component
   public class CustomPermissionEvaluator implements PermissionEvaluator {
      public CustomPermissionEvaluator() {
      }
      public boolean hasPermission(Authentication arg0, Object arg1) {
         if (arg0 == null || !arg0.isAuthenticated()) {
            System.out.println("false");
            return false;
         }
         else {
            for(GrantedAuthority authority: arg0.getAuthorities()) {
               if(authority.getAuthority().equals(arg1))
               return true;
            }
            return false;
         }
      }
   }