O Spring Security é o módulo que fornece suporte para autenticação, autorização e proteção contra ataques comuns em aplicações Web. A autenticação é como verificamos a identidade de usuários para termos acesso às funcionalidades de um sistema. Uma maneira comum de autenticação é a verificação de nome e senha fornecidos pelo usuário.
Depois que a autenticação é realizada, podemos executar a autorização, que é a permissão de acesso a recursos conforme um determinado papel que o usuário autenticado desempenha no sistema.
Neste Codelab você aprenderá como adicionar autenticação e autorização a uma aplicação Java Web utilizando o framework Spring Boot com Thymeleaf e acesso a dados com JdbcTemplate.
A aplicação consiste de uma página web para gerenciar um condomínio. (consulte este documento para mais detalhes). Teremos três papéis que os usuários poderão desempenhar na aplicação:
Papel | Autorização |
USUARIO | permissão para visualizar os relatórios de apartamentos e proprietários. |
MORADOR | permissão para visualizar os relatórios de apartamentos e proprietários e criar novos registros. |
ADMIN | todas as permissões. |
Para representar as informações de usuários e suas permissões vamos usar o seguinte modelo de dados:

A tabela de usuários armazena suas credenciais para autenticação e a tabela de papeis armazena suas autorizações (permissões). O relacionamento de entidade entre usuários e papeis é muitos-para-muitos porque um usuário pode ter um ou mais papeis e um papel pode ser atribuído a um ou mais usuários. É por isso que precisamos ter a tabela intermediária usuarios_papeis para representar esse relacionamento.
Script de criação do banco de dados:
CREATE TABLE IF NOT EXISTS `papeis` (
`papel_id` INT(11) NOT NULL AUTO_INCREMENT,
`nome` VARCHAR(45) NOT NULL,
PRIMARY KEY (`papel_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
CREATE TABLE IF NOT EXISTS `usuarios` (
`usuario_id` INT(11) NOT NULL AUTO_INCREMENT,
`email` VARCHAR(45) NOT NULL,
`senha` VARCHAR(64) NOT NULL,
PRIMARY KEY (`usuario_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
CREATE TABLE IF NOT EXISTS `usuarios_papeis` (
`usuario_id` INT(11) NOT NULL,
`papel_id` INT(11) NOT NULL,
CONSTRAINT `papel_fk`
FOREIGN KEY (`papel_id`)
REFERENCES `papeis` (`papel_id`),
CONSTRAINT `user_fk`
FOREIGN KEY (`usuario_id`)
REFERENCES `usuarios` (`usuario_id`))
ENGINE = InnoDB
DEFAULT CHARACTER SET = utf8;
-- Script de inserção de dados
INSERT INTO `papeis` (`papel_id`, `nome`) VALUES (1, 'USUARIO');
INSERT INTO `papeis` (`papel_id`, `nome`) VALUES (2, 'MORADOR');
INSERT INTO `papeis` (`papel_id`, `nome`) VALUES (3, 'ADMIN');
INSERT INTO `usuarios` (`usuario_id`, `email`, `senha`) VALUES (1, 'mercurio@teste.com', '$2a$10$wSa39/yk/UTovsqPt817X.c0I8xlS2s76YQy4ViDxag0mlxUoYUq2');
INSERT INTO `usuarios` (`usuario_id`, `email`, `senha`) VALUES (2, 'venus@teste.com', '$2a$10$v8Wr0mf6HgmIG0ANimKJOuOIt/09qIkXIF7wCwzq8.U/LTqTs9ovq');
INSERT INTO `usuarios` (`usuario_id`, `email`, `senha`) VALUES (3, 'terra@teste.com', '$2a$10$5sci59bfdcED4XxxuN9gx.SJBPsdNknirJSkLbTCouf2mFzLmX/Gi');
INSERT INTO `usuarios` (`usuario_id`, `email`, `senha`) VALUES (4, 'marte@teste.com', '$2a$10$Wl1gojjJgFhXztvHIULT3e0hiEMrDbCWCys0p6LnfrqxcxYkgh9OW');
INSERT INTO `usuarios` (`usuario_id`, `email`, `senha`) VALUES (5, 'jupiter@teste.com', '$2a$10$5sci59bfdcED4XxxuN9gx.SJBPsdNknirJSkLbTCouf2mFzLmX/Gi');
INSERT INTO `usuarios_papeis` (`usuario_id`, `papel_id`) VALUES (1, 1); -- usuário mercurio tem papel USUARIO
INSERT INTO `usuarios_papeis` (`usuario_id`, `papel_id`) VALUES (2, 2); -- usuário venus tem papel MORADOR
INSERT INTO `usuarios_papeis` (`usuario_id`, `papel_id`) VALUES (3, 2); -- usuário terra tem papel MORADOR
INSERT INTO `usuarios_papeis` (`usuario_id`, `papel_id`) VALUES (3, 1); -- usuário terra tem papel USUARIO
INSERT INTO `usuarios_papeis` (`usuario_id`, `papel_id`) VALUES (4, 1); -- usuário marte tem papel USUARIO
INSERT INTO `usuarios_papeis` (`usuario_id`, `papel_id`) VALUES (5, 3); -- usuário jupiter tem papel ADMIN
Observe que as senhas dos usuários estão criptografadas. A criptografia de senha é necessário para aumentar a segurança da aplicação. Vamos usar o algoritmo bcrypt para criptografar as senhas. Note que cada usuário tem um código diferente para a senha, no entanto, todas as senhas têm o mesmo valor (123456). Para gerar este códigos você pode usar este website. O Spring Security fornece uma implementação deste algoritmo para codificar as senhas cadastradas pelo usuário. Como não teremos a implementação de CRUD para usuários neste codelab, vamos inserir manualmente os dados de usuários usando o script mostrado acima.
Para adicionar segurança à aplicação vamos adicionar as seguintes dependências ao arquivo pom.xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
</dependency>
Observação: Em um caso de uso de cadastro de usuário, deve-se adicionar uma etapa de criptografia na senha para adicionar segurança, veja o exemplo abaixo:
@PostMapping(value = "novo-usuario")
public String cadastraNovoUsuario(Usuario usuario) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String encodedPassword = passwordEncoder.encode(usuario.getSenha());
usuario.setSenha(encodedPassword);
usuarioRepository.gravaUsuario(usuario);
return "redirect:/usuarios";
}
Vamos usar a autenticação de nome de usuário / senha, que utiliza um formulário HTML para obter do usuário um nome e uma senha para autenticação.

Fonte: spring.io
Quando a página de login é especificada na configuração do Spring Security, você é responsável por renderizar a página.
Existem alguns pontos-chave sobre o formulário HTML padrão:
/login usando o método post; th:action="@{/login}" username password Primeiro vamos adicionar uma nova pasta (security) à nossa estrutura do projeto:

Antes de iniciar a codificação da autenticação, precisamos adicionar duas classes na camada model e uma classe na camada repository
As seguintes classes representam os dados persistentes (camada model):
Arquivo model/Papel.java
package com.professorangoti.condominio.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Papel {
private Long id;
private String nome;
}
Arquivo model/Usuario.java
package com.professorangoti.condominio.model;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Usuario {
private Long id;
private String email, senha, papeis;
}
e a classe de acesso a dados do usuario repository/UsuarioRepository.java:
package com.professorangoti.condominio.repository;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;
import com.professorangoti.condominio.model.Usuario;
@Repository
public class UsuarioRepository {
@Autowired
JdbcTemplate jdbc;
public Usuario buscaPorEmail(String email) {
String sql = "SELECT usuarios.*, group_concat(distinct papeis.nome separator ', ') papeis FROM usuarios "
+ "inner join usuarios_papeis ON usuarios.usuario_id = usuarios_papeis.usuario_id "
+ "inner JOIN papeis ON usuarios_papeis.papel_id = papeis.papel_id "
+ "where email = ? "
+ "group by usuarios.usuario_id, usuarios.email;";
return jdbc.queryForObject(sql, this::mapper, email);
}
private Usuario mapper(ResultSet registro, int linha) throws SQLException {
return new Usuario(registro.getLong("usuario_id"),
registro.getString("email"),
registro.getString("senha"),
registro.getString("papeis"));
}
}
Como as credenciais do usuário estão armazenadas em banco de dados com a senha criptografada, precisamos configurar um Bean DaoAuthenticationProvider que usa um UserDetailsService e PasswordEncoder para autenticar um nome de usuário e senha.
Veja em DaoAuthenticationProvider :: Spring Security

Fonte: spring.io
Arquivo security/CondominioSecurity.java
package com.professorangoti.condominio.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class CondominioSecurity {
@Bean
public UserDetailsService userDetailsService() {
return new CondominioUserDetailsService();
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.antMatchers("/","/home").permitAll()
.anyRequest().authenticated())
.formLogin(form -> form.loginPage("/login").permitAll())
.logout().logoutSuccessUrl("/");
return http.build();
}
}
Arquivo security/CondominioUserDetails.java
package com.professorangoti.condominio.security;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.professorangoti.condominio.model.Usuario;
public class CondominioUserDetails implements UserDetails {
private Usuario usuario;
public CondominioUserDetails(Usuario usuario) {
this.usuario = usuario;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
String[] papeis = usuario.getPapeis().split(",");
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (String papel : papeis) {
authorities.add(new SimpleGrantedAuthority(papel));
}
return authorities;
}
@Override
public String getPassword() {
return usuario.getSenha();
}
@Override
public String getUsername() {
return usuario.getEmail();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Arquivo security/CondominioUserDetailsService.java
package com.professorangoti.condominio.security;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import com.professorangoti.condominio.model.Usuario;
import com.professorangoti.condominio.repository.UsuarioRepository;
public class CondominioUserDetailsService implements UserDetailsService {
@Autowired
private UsuarioRepository usuarioRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Usuario usuario = usuarioRepository.buscaPorEmail(username);
if (usuario == null) {
throw new UsernameNotFoundException("Usuário não autenticado!");
}
System.out.println(usuario.getPapeis());
return new CondominioUserDetails(usuario);
}
}
O método SecurityFilterChain filterChain(HttpSecurity http) estabelece as regras de segurança da aplicação. O objeto http é usado para escrever as regras da autorização e autenticação. Usamos o método authorizeHttpRequests para restringir o acesso com base na URL da requisição HTTP. As regras são aplicadas na ordem apresentada.
Regra | Autorização |
| autoriza o acesso livre aos recursos mapeados com a URL "/" ou "/home" |
| qualquer outra requisição precisa de autenticação |
| configura a URL da página de login a ser exibida. Necessita definir um controle mapeado com GET /login. |
| configura a página a ser exibida após o logout. |
Inserir este método no arquivo HomeController.java
@GetMapping("/login")
public String login() {
return "login";
}
Arquivo resources/templates/login.html
<!DOCTYPE html>
<html th:include="template :: modelo">
<div th:fragment="conteudo">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="/home">Home</a></li>
<li class="breadcrumb-item active" aria-current="page">Autenticação (login)</li>
</ol>
</nav>
<div th:if="${param.error}">
<h6 class="alert alert-danger">Usuario/senha inválidos</h6>
</div>
<form th:action="@{/login}" method="post">
<div class="mb-3">
<label class="form-label">Email</label>
<input class="form-control" type="text" name="username" placeholder="E-mail" />
</div>
<div class="mb-3">
<label class="form-label">Senha</label>
<input class="form-control" type="password" name="password" placeholder="Senha" />
</div>
<input class="btn btn-primary" type="submit" value="Log in" />
</form>
</div>
</html>
Arquivo resources/templates/template.html
<!-- Alteração do fragmento da barra de navegação para adicionar botão de logout e saudação do usuário logado -->
<div class="d-flex col-2" sec:authorize="isAuthenticated()">
<span class="text-white small" sec:authentication="name">opa</span>
</div>
<div class="align-self-center">
<form th:action="@{/logout}" method="post" sec:authorize="isAuthenticated()">
<input class="btn btn-secondary btn-sm p-1 small" type="submit" value="Sair" />
</form>
<a href="/login" sec:authorize="!isAuthenticated()" class="btn btn-secondary btn-sm p-1 small">Entrar</a>
</div>
<!-- Alteração do fragmento da barra de navegação para adicionar botão de logout e saudação do usuário logado -->
A autorização consiste em estabelecer regras relacionando usuários com permissões de acesso a recursos da aplicação. Os recursos de uma aplicação Spring Web MVC consistem em mapear URLs e métodos de uma classe da camada controller.
# | Endpoint | Perfil permitido |
1 | /upload | MORADOR, ADMIN |
1 | /cad_apto | MORADOR, ADMIN |
1 | /cad_prop | MORADOR, ADMIN |
2 | /fotos_apto | USUARIO, MORADOR, ADMIN |
2 | /rel_apto | USUARIO, MORADOR, ADMIN |
2 | /rel_prop | USUARIO, MORADOR, ADMIN |
3 | /excluir_foto | ADMIN |
3 | /excluir_prop | ADMIN |
4 | /home ou / | aberto |
4 | /login | aberto |
Papel | Usuário |
MORADOR | venus@teste.com terra@teste.com |
USUARIO | mercurio@teste.com marte@teste.com |
ADMIN | jupiter@teste.com |
As autorizações também serão feitas dentro do arquivo CondominioSecurity.java. Para cada conjunto de endpoints teremos um filtro que declara qual perfil tem autorização de acesso.
O primeiro conjunto (#1) composto pelos endpoints: /upload, /cad_apto e /cad_apto permite o acesso aos papeis MORADOR e ADMIN e a regra fica assim:
antMatchers("/upload", "/cad_apto", "/cad_prop").hasAnyAuthority("MORADOR", "ADMIN")
O segundo conjunto (#2) composto pelos endpoints: /fotos_apto, /rel_apto, /rel_prop permite o acesso aos papeis USUARIO, MORADOR, ADMIN e a regra fica assim:
antMatchers("/fotos_apto", "/rel_apto", "/rel_prop").hasAnyAuthority("USUARIO", "MORADOR", "ADMIN")
O terceiro conjunto (#3) composto pelos endpoints: /excluir_foto, /excluir_prop permite o acesso somente ao ADMIN e a regra fica assim:
antMatchers("/excluir_foto", "/excluir_prop").hasAnyAuthority("ADMIN")
O quarto conjunto (#4) composto pelos endpoints: /home, /, /login permite o acesso aberto (sem necessidade de autenticação) e a regra fica assim:
antMatchers("/", "/home").permitAll()
formLogin(form -> form.loginPage("/login").permitAll())
e o método fica assim no final da configuração:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(authorize -> authorize
.antMatchers("/upload", "/cad_apto", "/cad_prop").hasAnyAuthority("MORADOR", "ADMIN") // permissão para criar
.antMatchers("/fotos_apto", "/rel_apto", "/rel_prop").hasAnyAuthority("USUARIO", "MORADOR", "ADMIN") // permissão para visualizar
.antMatchers("/excluir_foto", "/excluir_prop").hasAnyAuthority("ADMIN") // permissão para editar/excluir
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated())
.formLogin(form -> form.loginPage("/login").permitAll())
.logout().logoutSuccessUrl("/");
return http.build();
}
https://github.com/thymeleaf/thymeleaf-extras-springsecurity
Este módulo oferece alguns atributos que usaremos nas páginas da aplicação para exibir as funções do sistema conforme o papel do usuário autenticado.
Lista dos atributos:
Renderiza o elemento se o usuário autenticado estiver autorizado a ver a URL especificada. Exemplo:
<a class="dropdown-item" href="/rel_prop" sec:authorize-url="/rel_prop">Listagem</a>
Renderiza seu conteúdo quando a expressão do atributo é avaliada como verdadeira. A expressão isAuthenticated() retorna true se o usuário realizou a autenticação. Exemplo:
<a href="/login" sec:authorize="!isAuthenticated()" class="btn btn-secondary btn-sm p-1 small">Entrar</a>
Este atributo oferece acesso às propriedades do objeto de autenticação. Neste exemplo, queremos exibir na barra de navegação o nome do usuário e seu perfil (authorities).
<div class="col-3 text-white small" sec:authorize="isAuthenticated()">
<span sec:authentication="name">opa</span>
<span sec:authentication="principal.authorities"></span>
</div>
