Spring Security

  O Spring Security é um framework do ecossistema Spring que ajuda na autenticação e autorização de usuários.

 Importação da dependência

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

Por que usar um framework de segurança na sua aplicação web?

 Utilizar um framework de segurança é fundamental para qualquer aplicação web hoje em dia, tanto para aplicações expostas na internet para qualquer usuário, quanto para aplicações corporativas fechadas.
 As principais vulnerabilidades em sistemas web hoje em dia são:
Para saber quais são as 10 maiores falhas, você pode acessar o OWASP Top Ten.

Autenticação x Autorização

 Uma dúvida comum é não saber a diferença entre autenticação e autorização. É muito simples:
  • autenticação: processo de identificação do usuário (login)
  • autorização: processo de verificação de acesso do usuário autenticado à um recurso específico (exemplo: o João pode atualizar os dados de um produto?)
 Quando um usuário tenta acessar um recurso sem estar autenticado, devemos retornar o erro 401 Unauthorized. Por outro lado, uma vez que o usuário esteja autenticado e tenta acessar um recurso que não tem autorização, devemos retornar o erro 403 Forbidden.

Formas de autenticação

 As duas principais formas de autenticação são:
  • Basic Auth: forma mais simples, onde o usuário e a senha são enviadas no cabeçalho da requisição (Authorization: Basic {credenciais em base 64 no formato usuário:senha})
  • Bearer: forma mais prática, onde o usuário recebe um token e pode enviá-lo em diversas requisições, também no cabeçalho (Authorization: Bearer <token>)

Configurando o Spring Security

WebSecurityConfigurerAdapter

 A forma clássica de configurar o SS é criando uma classe de configuração que estenda a classe WebSecurityConfigurerAdapter:
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... }

Permissões de acesso

 Para definir as permissões da aplicação, basta criar um método que sobrescreva o método configure(HttpSecurity http):

@Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic()
.and()
.authorizeHttpRequests()
.antMatchers(HttpMethod.GET, "/parking-spot/**").permitAll()
.antMatchers(HttpMethod.POST, "/parking-spot").hasRole("USER")
.antMatchers(HttpMethod.DELETE, "/parking-spot/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.csrf().disable();
}
 No código acima, podemos verificar os seguintes pontos:
  • httpBasic(): define que a forma de autenticação é o basic auth;
  • and(): concatena definições;
  • authorizeHttpRequest(): informa que iremos tratar das permissões das requisições http
  • antMatchers(método, URL): informa de qual URL/método iremos tratar
  • permitAll(): permite o acesso sem autenticação e autorização
  • authenticated(): exige autenticação
  • hasRole("role"): exige a autenticação e autorização
  • anyRequest(): informa que a definição se aplica aos demais endpoints
  • csrf(): informa que iremos falar sobre o CSRF
  • disable(): desabilita a obrigatoriedade do CSRF do SS. 

@EnableWebSecurity

 Usar a anotação @EnableWebSecurity faz com que o SS desconsidere as configurações padrão e use somente as definidas naquela classe.

CSRF

 Geralmente, o CSRF é desabilitado em APIs, já que esse tipo de ataque não pode ser feito dessa forma, só em aplicações que recebem e retornam HTML. Caso ele não seja desabilitado, precisamos inseri-lo nos formulários para que seja possível executar as requisições:
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

Autenticação com Spring Security

 Por padrão, o SS irá gerar um hash toda vez que a aplicação for inicializada. Esse hash é a senha do user que deverá ser usada para endpoints que exigirem autenticação (ou seja, requisições que precisam de autenticação devem ter basic auth passando como usuário "user" e como senha o hash gerado).

Configurando usuários em memória

 Caso você não queira usar o user default do SS, é possível configurar um usuário em memória sobreescrevendo o método configure(AuthenticationManagerBuilder auth):
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("username")
.password(passwordEncoder().encode("senha"))
.roles("ADMIN");
}
 Note que é necessário definir a senha usando o método passwordEncoder, que define como será feita a criptografia da senha:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

Usuários no banco de dados

 A forma mais comum, no entanto, é utilizar dados de usuários gravados em um banco de dados. Para isso, precisamos primeiro criar uma entidade que irá representar a tabela. Essa entidade precisa implementar a interface UserDetails. Além disso, é necessário ter uma tabela intermediária que irá representar as roles dos usuários:

@Entity
@Table(name="TB_USERS")
public class UserModel implements Serializable, UserDetails {

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="ID")
private Integer id;

@Column(name="USERNAME", nullable = false, unique = true)
private String username;

@Column(name="PASSWORD", nullable = false)
private String password;

@ManyToMany
@JoinTable(
name="TB_USERS_ROLES",
joinColumns = @JoinColumn(name="USER_ID"),
inverseJoinColumns = @JoinColumn(name="ROLE_ID")
)
private List<RoleModel> roles;

public UserModel() {}

public UserModel(Integer id, String username, String password, List<RoleModel> roles) {
this.id = id;
this.username = username;
this.password = password;
this.roles = roles;
}


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

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

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

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

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roles;
}
    // Getters e Setters
}
 A classe que representa as roles precisa implementar GrantedAuthority. Também é importante que o atributo roleName seja uma enum com os valores começando com "ROLE_" (ex: ROLE_USER, ROLE_ADMIN, etc.).
@Entity
@Table(name="TB_ROLES")
public class RoleModel implements Serializable, GrantedAuthority {

private static final long serialVersionUID = 1L;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name="ID")
private Integer roleId;

@Enumerated(EnumType.STRING)
@Column(name="NAME", nullable = false, unique = true)
private RoleName roleName;


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

public Integer getRoleId() {
return roleId;
}

public void setRoleId(Integer roleId) {
this.roleId = roleId;
}

public RoleName getRoleName() {
return roleName;
}

public void setRoleName(RoleName roleName) {
this.roleName = roleName;
}

}
 Após criar as classes de modelo, precisamos criar uma classe que implemente UserDetailsService:
@Service
@Transactional
public class UserDetailsServiceImpl implements UserDetailsService {

final private UserRepository userRepository;

public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserModel userModel =
userRepository.findByUsername(username).orElseThrow(
() -> new UsernameNotFoundException("User not found with username: " + username)
);

return new User(
userModel.getUsername(), userModel.getPassword(),
true, true, true, true,
userModel.getAuthorities()
);
}

}
 E também a classe UserRepository:

@Repository
public interface UserRepository extends JpaRepository<UserModel, Integer> {
Optional<UserModel> findByUsername(String username);
}
 Após criar as classes necessárias, precisamos ajustar o WebSecurityConfig para considerar essa forma de autenticação, alterando o método configure(AuthenticationManagerBuilder auth) para usar userDetailsService() passando a classe de serviço criada:
final private UserDetailsServiceImpl userDetailsService;

public WebSecurityConfig(UserDetailsServiceImpl userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}

Nova forma de configuração do Spring Security

 Agora a classe de configuração não precisa mais estender WebSecurityConfigurerAdapter. Basta ter a anotação @Configuration e cada método retornar o objeto certo. 
 Por exemplo, para configurar as permissões por endpoint, basta criar um bean que receba HttpSecurity e retorne SecurityFilterChain:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http.httpBasic()
.and()
.authorizeHttpRequests()
.antMatchers(HttpMethod.GET, "/parking-spot/**").permitAll()
.antMatchers(HttpMethod.POST, "/parking-spot").hasRole("USER")
.antMatchers(HttpMethod.DELETE, "/parking-spot/**").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.csrf().disable()
.build();
}
 Além disso, não é mais necessário ter um método que define a forma de login (configure(AuthenticationManagerBuilder auth()). O SS irá verificar se existe um bean responsável por isso e considerá-lo automicaticamente.

Permissões de acesso por método

 Ao invés de definir as permissões de acesso em um único método, podemos defini-las em cada método. Para isso, anotamos a classe de configuração com @EnableGlobalMethodSecurity(prePostEnabled = true):
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfigV2 { ... }
 E então em cada método usar a anotação @PreAuthorize("permissões necessárias"):
@PreAuthorize("permitAll")
@PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_ADMIN')")
@PreAuthorize("hasRole('ADMIN')")

Referências

Comentários

Postagens mais visitadas deste blog

Thymeleaf