Spring Boot + Security + Data, customizando um Authentication Provider

Quando o assunto é o desenvolvimento de aplicações complexas autocontidas, o Spring Boot inegavelmente é um framework para linguagem de programação Java à frente dos demais.

Este tutorial irá mostrar como configurar um provedor de autenticação utilizando o Spring Security, permitindo uma flexibilidade adicional de customização em comparação com o cenário padrão utilizando o UserDetailsService (uma interface do core que carrega dados específicos de usuário administrativo) de maneira simplificada e objetiva.

Anotações como: @EnableAutoConfiguration, @SpringBootApplication, @ComponentScan simplificam a configuração, em sua grande maioria já pré estabelecida pelo Common application properties, definidos por default pelo Spring Boot.

Várias dessas propriedades são facilmente alteradas/personalizadas utilizando os arquivos: application.properties ou application.yml (dependendo do gerenciador de dependências que estiver utilizando para seu projeto).

Utilizar o Spring Boot + Spring Security + Spring Data no desenvolvimento de projetos de software adiciona uma camada extra de segurança, além de oferecer integração com os demais projetos do ecosistema do Spring Framework.

Spring Framework Ecosystem

Features do Spring Security 4

  • Authentication and Authorization.
  • Supports BASIC, Digest and Form-Based Authentication.
  • Supports LDAP Authentication.
  • Supports WebSocket Security.
  • CSRF Token Argument Resolver.
  • Supports OpenID Authentication.
  • Supports SSO (Single Sign-On) Implementation.
  • Suuports Cross-Site Request Forgery (CSRF) Implementation.
  • Suuports “Remember-Me” Feature through HTTP Cookies.
  • Supports Implementation of ACLs
  • Supports “Channel Secruity” that means automatically switching between HTTP and HTTPS.
  • Supports I18N (Internationalization).
  • Supports JAAS (Java Authentication and Authorization Service).
  • Supports Flow Authorization using Spring WebFlow Framework.
  • Supports WS-Security using Spring Web Services.

O Provedor de Autenticação

O Spring Security fornece uma variedade de opções para realizar autenticação, os pedidos são processados pelo AuthenticationProvider que retorna um objeto com as credenciais do usuário.
Ele é usado por toda a estrutura como um DAO de usuário, que inclusive, é a estratégia utilizada pelo DaoAuthenticationProvider.

package br.com.coderi.base.services;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

import br.com.coderi.base.model.Usuario;

@Component
public class AuthProviderService implements AuthenticationProvider {

	@Autowired
	private UsuarioService usuarioService;

	@Override
	public Authentication authenticate(Authentication auth) throws AuthenticationException {
		String login = auth.getName();
		String senha = auth.getCredentials().toString();

		// Defina suas regras para realizar a autenticação

		if (usuarioBd != null) {
			if (usuarioAtivo(usuarioBd)) {
				Collection<? extends GrantedAuthority> authorities = usuarioBd.getPapeis();
				return new UsernamePasswordAuthenticationToken(login, senha, authorities);
			} else {
				throw new BadCredentialsException("Este usuário está desativado.");
			}
		}

		throw new UsernameNotFoundException("Login e/ou Senha inválidos.");
	}

	@Override
	public boolean supports(Class<?> auth) {
		return auth.equals(UsernamePasswordAuthenticationToken.class);
	}

	private boolean usuarioAtivo(Usuario usuario) {
		if (usuario != null) {
			if (usuario.getStatus() == true) {
				return true;
			}
		}
		return false;
	}
}

Registrando o provedor de autenticação

Se somente um provedor de autenticação estiver configurado, o Spring Boot automaticamente o utiliza como default para realizar o processo de autorização (sem necessidade de injeção @inject).

Abaixo, em código, mostro como configurar um provedor de autenticação via Java (recomendado para o Spring Boot) e como utilizar a mesma configuração via arquivo .xml

Via .xml

Configuração .XML

Via .java

package br.com.coderi.base.configuration;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

import br.com.coderi.base.services.AuthProviderService;

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
	
	@Autowired
	private AuthProviderService authProvider;
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth
			.authenticationProvider(authProvider);
	}

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http
			.authorizeRequests()
				.antMatchers("/").permitAll()
				.antMatchers("/dashboard").hasRole("DASHBOARD")
				.antMatchers("/usuario").hasRole("USUARIO")
				.anyRequest().authenticated()
				.and()
			.exceptionHandling()
				.accessDeniedPage("/negado")
				.and()
			.formLogin()
				.loginPage("/login")
				.usernameParameter("login")
				.passwordParameter("senha")
				.failureUrl("/login?error=1")
				.permitAll()
				.and()
			.logout()
				.logoutUrl("/logout")
				.logoutSuccessUrl("/login?logout")
				.invalidateHttpSession(true)
				.permitAll();
	}
}

Inclusive, o builder do AuthenticationManager é tão rico, que é possível também customizar o nome dos campos que irá receber das requisições enviadas pelo formulário de login.

Criando as entidades

UserDetails

Implemente a interface UserDetails, ela possibilita a customização da sua entidade de usuários do sistema.
Essa interface requer um único método de leitura, o que simplifica o suporte para quaisquer estratégias de acesso a dados.

package br.com.coderi.base.model;

import java.util.Collection;
import java.util.Date;
import java.util.List;

import javax.persistence.CascadeType;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Transient;

import org.hibernate.validator.constraints.NotEmpty;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import com.fasterxml.jackson.annotation.JsonIgnore;

@Entity
public class Usuario implements UserDetails {

	private static final long serialVersionUID = 1L;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;

	@NotEmpty
	private String nome;

	@NotEmpty
	private String login;

	@NotEmpty
	private String senha;

	@Transient
	private String confirmaSenha;

	@NotEmpty
	private String email;

	private Boolean status;

	@DateTimeFormat(pattern = "dd/MM/yyyy")
	private Date cadastro;

	@JsonIgnore
	@OneToMany(mappedBy = "usuario", fetch = FetchType.EAGER, cascade = CascadeType.ALL)
	private List papeis;

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		return papeis;
	}

	@Override
	public String getPassword() {
		return senha;
	}

	@Override
	public String getUsername() {
		return login;
	}

	@Override
	public boolean isAccountNonExpired() {
		return false;
	}

	@Override
	public boolean isAccountNonLocked() {
		return false;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		return false;
	}

	@Override
	public boolean isEnabled() {
		return status;
	}

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getNome() {
		return nome;
	}

	public void setNome(String nome) {
		this.nome = nome;
	}

	public String getLogin() {
		return login;
	}

	public void setLogin(String login) {
		this.login = login;
	}

	public String getSenha() {
		return senha;
	}

	public void setSenha(String senha) {
		this.senha = senha;
	}

	public String getConfirmaSenha() {
		return confirmaSenha;
	}

	public void setConfirmaSenha(String confirmaSenha) {
		this.confirmaSenha = confirmaSenha;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public Boolean getStatus() {
		return status;
	}

	public void setStatus(Boolean status) {
		this.status = status;
	}

	public Date getCadastro() {
		return cadastro;
	}

	public void setCadastro(Date cadastro) {
		this.cadastro = cadastro;
	}

	public List getPapeis() {
		return papeis;
	}

	public void setPapeis(List papeis) {
		this.papeis = papeis;
	}

	@Override
	public String toString() {
		return "Usuario [id=" + id + ", nome=" + nome + ", login=" + login + ", senha=" + senha + ", email=" + email
				+ ", status=" + status + ", cadastro=" + cadastro + "]";
	}
}

GrantedAuthority

Implemente a interface GrantedAuthority, ela permitirá definir os papeis dos usuários do sistema.
Essa interface representa a autoridade concedida a um objeto de autenticação especificamente apoiado por um AccessDecisionManager.
Já a interface publica AccessDecisionManager faz a decisão final de controle de acesso (conhecido com autorização).

package br.com.coderi.base.model;

import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

import org.springframework.security.core.GrantedAuthority;

import com.fasterxml.jackson.annotation.JsonIgnore;

@Entity
public class Papel implements GrantedAuthority {

	private static final long serialVersionUID = 1L;

	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id;

	@Enumerated(EnumType.STRING)
	private Modulo nome;

	@ManyToOne
	@JsonIgnore
	private Usuario usuario;

	@Override
	public String getAuthority() {
		return nome.toString();
	}

	public enum Modulo {
		DASHBOARD("ROLE_DASHBOARD"), USUARIO("ROLE_USUARIO");
		private String modulo;

		private Modulo(String modulo) {
			this.modulo = modulo;
		}

		@Override
		public String toString() {
			return this.modulo;
		}
	}

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public Modulo getNome() {
		return nome;
	}

	public void setNome(Modulo nome) {
		this.nome = nome;
	}

	public Usuario getUsuario() {
		return usuario;
	}

	public void setUsuario(Usuario usuario) {
		this.usuario = usuario;
	}

	@Override
	public String toString() {
		return "Papel [id=" + id + ", nome=" + nome + "]";
	}
}

A implementação default mais comum é customizar o AuthenticationProvider, que recupera os detalhes do usuário a partir de uma consulta a base de dados. O objeto permite acesso somente ao nome de usuário, a fim de recuperar a entidade completa, em um grande número de cenários, isso é o suficiente.

O Spring Security possui mais cenários personalizados, mas será necessário acessar a solicitação de autenticação completa para ser capaz de realizar o processo de autenticação, por exemplo, é possível utilizar serviços externos de terceiros para realizar o mesmo processo de autenticação e autorização.

Boa sorte com sua customização!