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.
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 Positivos | Pontos Negativos |
---|---|
Configuração simples | Senha estática e exposta na aplicação 30 |
Configurações adicionais em nossos controllers |
- http://localhost:8080/login
- http://localhost:8080
- http://localhost:8080/users
- http://localhost:8080/managers
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";
}
}
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>
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
}
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);
}
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.
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);
}
}
}
- 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')
- 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.