Springboot

Spring Boot 2x Security

Spring Boot 2x Security

Em memória

Esta configuração permite criar mais de usuário e perfis de acesso.

É necessário criar uma classe que estenda WebSecurityConfigurerAdapter conforme abaixo:

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;


@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user")
                .password("{noop}user123")
                .roles("USERS")
                .and()
                .withUser("admin")
                .password("{noop}master123")
                .roles("MANAGERS");
    }
}
  • Definimos que está é uma classe de configuração;
  • Aplica a configuração de segurança padrão do Spring ao nosso aplicativo;
  • Exige que os usuários tenham um perfil (role) apropriado usando anotações de segurança;
  • Gerenciador de credenciais em memória;
  • Determinam a criptografia aplicada na senha de cada usuário.

Existem algumas implementações de criptografias utilizadas pelo Spring Security

  • Use {bcrypt} for BCryptPasswordEncoder (mais comum);
  • Use {noop} for NoOpPasswordEncoder;
  • Use {pbkdf2} for Pbkdf2PasswordEncoder;
  • Use {scrypt} for SCryptPasswordEncoder;
  • Use {sha256} for StandardPasswordEncoder.
ℹ️ Observe que agora temos dois usuários com perfis diferentes!

Vamos imaginar que a nossa API possua 3 acessos, a nossa página de boas-vindas, uma página a nível de perfil users e uma página disponível somente para managers.

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class WelcomeController {
    @GetMapping
    public String welcome(){
        return "Welcome to My Spring Boot Web API";
    }
    @GetMapping("/users")
    @PreAuthorize("hasAnyRole('MANAGERS','USERS')")
    public String users() {
        return "Authorized user";
    }
    @GetMapping("/managers")
    @PreAuthorize("hasRole('MANAGERS')")
    public String managers() {
        return "Authorized manager";
    }
}
  • É realizada uma pré verificação de autorização considerando o perfil do usuário
  • Somente usuários com perfil MANAGERS poderá acessar este recurso.
Pontos PositivosPontos Negativos
Configuração simplesSenha estática e exposta na aplicação 30
Configurações adicionais em nossos controllers
✔️ Realize alguns testes nos recursos disponíveis, realizando a autenticação com usuário da aplicação.

Este será o resultado ao tentar acessar a área manager com perfil user

Configure Adapter

Nos exemplos acima já podemos considerar um nível de segurança em nossa aplicação, mas se percebermos o esforço de configuração para as novas funcionalidades, poderá não ser uma abordagem tão satisfatória. Para isso, vamos tentar deixar a parte de configuração, centralizada na nossa WebSecurityConfig, removendo configurações adicionais em nossos controllers.

Na classe WebSecurityConfig.java do nosso projeto adicione o método abaixo:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/login").permitAll()
            .antMatchers("/managers").hasAnyRole("MANAGERS")
            .antMatchers("/users").hasAnyRole("USERS","MANAGERS")
            .anyRequest().authenticated().and().formLogin();
}
  • Determinamos acesso público os nossos recursos de login e boas-vindas;
  • Somente usuários com perfil managers poderão acessar este recurso;
  • Somente logins com perfil users ou managers poderão acessar este recurso;
  • Determinamos a forma de solicitar a requisição como Form Login

Como toda a configuração de segurança está na classe WebSecurityConfig.java, agora podemos deixar nosso controller mais clean:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class WelcomeController {
    @GetMapping
    public String welcome(){
        return "Welcome to My Spring Boot Web API";
    }
    //@PreAuthorize("hasAnyRole('MANAGERS','USERS')")
    @GetMapping("/users")
    public String users() {
        return "Authorized user";
    }
    //@PreAuthorize("hasRole('MANAGERS')")
    @GetMapping("/managers")
    public String managers() {
        return "Authorized manager";
    }
}
ℹ️ Faça alguns testes de login e acesso aos recursos, com cada perfil de acesso.

HTTP Basic

Na maioria das nossas APIs, utilizamos o tipo de autenticação Basic Autentication para que nossos clientes possam enviar sua credencial, sem a necessidade de visualizar uma tela de login.

Para habilitar este recurso no projeto, substitua a linha onde contenha o código .anyRequest().authenticated().and().formLogin() para .anyRequest().authenticated().and().httpBasic();

Você pode usar o postman para realizar dos testes em sua API.

Auth Database

Depois de uma contextualizada simples sobre segurança, hora de explorar novos recursos como interação com banco de dados.

Adicionando a dependência do Spring Data JPA:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

Por fim, precisamos adicionar a dependência do banco de dados escolhido.

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
ℹ️ Você pode habilitar tudo que acontece no banco adicionando a propriedade spring.jpa.show-sql=true no arquivo applications.properties.

Vamos criar a classe User.java:

import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;

@Entity
@Table(name = "tab_user")
public class UserEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id_user")
    private Integer id;
    @Column(length = 50, nullable = false)
    private String name;
    @Column(length = 20, nullable = false)
    private String username;
    @Column(length = 100, nullable = false)
    private String password;
    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "tab_user_roles", joinColumns = @JoinColumn(name = "user_id"))
    @Column(name = "role_id") 
    private List<String> roles = new ArrayList<>();

    public UserEntity(){

    }
    public UserEntity(String username){
        this.username = username;
    }
    //getters e setters
}
ℹ️ Recomendamos explorar as anotações de mapeamento disponíveis no JPA.

Agora criaremos um repositório para interagir com a nossa entidade User.java e realizar as operações de CRUD necessárias.

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

public interface UserRepository extends JpaRepository<UserEntity, Integer> {
    @Query("SELECT e FROM UserEntity e JOIN FETCH e.roles WHERE e.username= (:username)")
    public UserEntity findByUsername(@Param("username") String username);
}
✔️ Explorando o Spring Data JPA temos uma query override, que busca um usuário sua respectiva lista de perfis (roles).

UserDetailService

A interface UserDetailsService é usada para recuperar dados relacionados ao usuário. Ele possui um método denominado loadUserByUsername () que pode ser substituído para personalizar o processo de localização do usuário.

Vamos criar uma classe SecurityDatabaseService.java que implementará a UserDetailService para retornar um usuário para contexto de segurança conforme nosso banco de dados.


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.HashSet;
import java.util.Set;
@Service
public class SecurityDatabaseService  implements UserDetailsService {
    @Autowired
    private UserRepository userRepository;
    @Override
    public UserDetails loadUserByUsername(String username) {
        UserEntity userEntity = userRepository.findByUsername(username);
        if (userEntity == null) {
            throw new UsernameNotFoundException(username);
        }
        Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
        userEntity.getRoles().forEach(role -> {
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
        });

        System.out.println("Acionando a verificação de credencial na base de dados");

        //aqui é onde a mágica acontece
        UserDetails user = new org.springframework.security.core.userdetails.User(userEntity.getUsername(),
                userEntity.getPassword(),
                authorities);
        
        return user;
    }
}
  • Injeta o repositório para a localização do usuário no banco de dados;
  • Converte os perfis do usuário (roles) em Authoritys;
  • Retorna um usuário para o contexto Spring Security.

WebSecurityConfig

Em um contexto de banco de dados a classe WebSecurityConfig.java, não contém mais responsabilidade de criar e localizar os usuários, esta ação é delegada ao UserDetailService.


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.method.configuration.EnableGlobalMethodSecurity;
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 org.springframework.security.crypto.password.NoOpPasswordEncoder;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private SecurityDatabaseService securityService;
    @Autowired
    public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
        //NoOpPasswordEncoder -> é um encode que salva literalmente a senha na base de dados
        //sem realizar qualquer criptogragia
        auth.userDetailsService(securityService).passwordEncoder(NoOpPasswordEncoder.getInstance());
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/").permitAll()
                .antMatchers("/login").permitAll()
                .antMatchers("/managers").hasAnyRole("MANAGERS")
                .antMatchers("/users").hasAnyRole("USERS","MANAGERS")

                //evitar pedir tela de login, pois nosso projeto agora é uma api
                //.anyRequest().authenticated().and().formLogin();

                //autenticacao basic http
                .anyRequest().authenticated().and().httpBasic();
    }
    // deixamos de ter os usuários em memória
    /*
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user")
                .password("{noop}user123")
                .roles("USERS")
                .and()
                .withUser("admin")
                .password("{noop}master123")
                .roles("MANAGERS");
    }
     */
}

Veja que agora teremos um globalUserDetails requisitando a um SecurityDatabaseService.

Sign up

Agora é hora de dar uma carga inicial de usuários em nossa aplicação.

Crie a classe StartApplication.java que implementa a interface CommandLineRunner para executar ao iniciar a aplicação.

🔔 Atenção
Este código simula a insercão dos usuários em nossa base de dados, lembrando que este etapa deverá ter uma funcionalidade de gestão de usuários

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;

@Component
public class StartApplication implements CommandLineRunner {
    @Autowired
    private UserRepository repository;
    @Transactional
    @Override
    public void run(String... args) throws Exception {
        UserEntity user = repository.findByUsername("admin");
        if(user==null){
            user = new UserEntity();
            user.setName("ADMIN");
            user.setUsername("admin");
            user.setPassword("master123");
            user.getRoles().add("MANAGERS");
            repository.save(user);
        }
        user = repository.findByUsername("user");
        if(user ==null){
            user = new UserEntity();
            user.setName("USER");
            user.setUsername("user");
            user.setPassword("user123");
            user.getRoles().add("USERS");
            repository.save(user);
        }
    }
}
ℹ️ Se preferir, insira via Script SQL a inserção de seus usuários conforme ilustração abaixo:
  1. Crie o arquivo data-h2.sql dentro de /src/main/resources e insira os scripts sql abaixo:
create table tab_user (id_user integer generated by default as identity, name varchar(50) not null, password varchar(100) not null, username varchar(20) not null, primary key (id_user))
create table tab_user_roles (user_id integer not null, role_id varchar(255))
alter table tab_user_roles add constraint fk_user_roles foreign key (user_id) references tab_user

insert into tab_user (name,username, password) values ( 'Gleyson', 'glysns', 'iza123')
insert into tab_user_roles (user_id, role_id) values (1, 'MANAGERS')

insert into tab_user (name,username, password) values ( 'Admin', 'admin', 'master123')
insert into tab_user_roles (user_id, role_id) values (2, 'MANAGERS')

insert into tab_user (name,username, password) values ( 'User', 'user', 'user123')
insert into tab_user_roles (user_id, role_id) values (3, 'USERS')
  1. No arquivo application.properties adicione as configurações abaixo:
spring.jpa.hibernate.ddl-auto=none
spring.sql.init.platform=h2

Testando credenciais

Segue ilustração de realizar uma autenticação com base nos usuários existentes no banco de dados.