📚 Pré-requisitos Teóricos: este projeto aplica conceitos ensinados em Especialização em Backend com Java e Spring Boot. Recomendado revisar antes de começar.
Trilha de Aprendizado — Projeto 1 de 4
- ➡️ 01 (este): Login/cadastro com role única (enum) · Bootstrap 5 · H2
- 02: Refatoração SOLID/DRY — camada de Service, DTOs, Bean Validation
- 03: RBAC muitos-para-muitos · painel admin · relatórios
- 04: Levantamento de Requisitos · Backlog & Sprints · Git Flow · testes · ADRs
🎓 Nível profissional simulado: Estagiário. Você segue um passo a passo guiado para ligar Spring Security, Thymeleaf e JPA pela primeira vez: registo público, login, BCrypt, e uma distinção simples de papéis (
ADMIN/USERcomoenumdireto na entidade). Sem ainda pensar em separar camadas de serviço, DTOs ou testes automatizados — é o primeiro contato com o ecossistema.🗺️ Mapa da trilha: veja a tabela comparativa completa na listagem de projetos ou na conclusão do Login de Usuários 04.
Este documento detalha a construção de uma aplicação web monolítica completa para registo, autenticação e gestão de utilizadores, com distinção entre perfis de Administrador e Utilizador comum.`
Mantemos a arquitetura monolítica com Spring Boot e Thymeleaf, mas com melhorias cruciais:
ADMIN e USER.
admin é criado na primeira execução para garantir o acesso inicial ao sistema.graph TD
User((Utilizador)) -->|Acessa URL| Security[Spring Security / Filter Chain]
Security -->|Autentica/Verifica Perfil| Controller{Controllers Web}
subgraph "Camada de Lógica e Acesso"
Controller -->|Solicita Dados| Service[CustomUserDetailsService]
Service -->|Busca por Email| Repo[UserRepository]
Repo -->|JPA| DB[("Base de Dados H2")]
end
subgraph "Respostas (Páginas)"
Controller -->|Retorna Dados| View["Thymeleaf Templates"]
View -->|Renderiza HTML| UI["Páginas: Login, Registo, Usuarios"]
UI -->|Exibe ao| User
end
subgraph "Regras de Acesso"
RoleAdmin["Perfil ADMIN"] -->|Permite| Delete["Apagar Utilizadores"]
RoleUser["Perfil USER"] -->|Bloqueia| Delete
end
Começamos por gerar o projeto com a configuração especificada.
MavenJavabr.com.cursologinusuarios17Spring Web, Thymeleaf, Spring Data JPA, H2 Database, Spring Security.Após gerar e abrir o projeto na sua IDE, vamos ao código.
RolePara gerir os perfis, a melhor prática é usar um Enum. Crie o pacote model e, dentro dele, o ficheiro Role.java.
package br.com.curso.loginusuarios.model;
public enum Role {
USER,
ADMIN
}
User AtualizadaAgora, vamos adicionar o campo role à nossa entidade User.
Ficheiro: src/main/java/br/com/curso/loginusuarios/model/User.java
package br.com.curso.loginusuarios.model;
import jakarta.persistence.*;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String nome;
@Column(unique = true, nullable = false)
private String email;
private String senha;
@Enumerated(EnumType.STRING) // Diz ao JPA para guardar o nome do enum (ex: "ADMIN")
@Column(nullable = false)
private Role role;
// --- Getters e Setters ---
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getNome() { return nome; }
public void setNome(String nome) { this.nome = nome; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getSenha() { return senha; }
public void setSenha(String senha) { this.senha = senha; }
public Role getRole() { return role; }
public void setRole(Role role) { this.role = role; }
}
UserRepositoryO repositório não precisa de alterações. Ele continua a ser a nossa ponte com a base de dados.
Ficheiro: src/main/java/br/com/curso/loginusuarios/repository/UserRepository.java
package br.com.curso.loginusuarios.repository;
import br.com.curso.loginusuarios.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
SecurityConfig AtualizadaA configuração de segurança é agora mais específica, definindo que apenas utilizadores com o perfil ADMIN podem apagar outros utilizadores.
Ficheiro: src/main/java/br/com/curso/loginusuarios/config/SecurityConfig.java
package br.com.curso.loginusuarios.config;
import br.com.curso.loginusuarios.model.Role;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
// Permite acesso público a estas páginas e recursos estáticos
.requestMatchers("/registar", "/login", "/css/**", "/js/**").permitAll()
// Apenas ADMINS podem aceder a URLs que começam com /usuarios/apagar
.requestMatchers(HttpMethod.POST, "/usuarios/apagar/**").hasAuthority(Role.ADMIN.name())
// Qualquer outro pedido exige autenticação
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
.defaultSuccessUrl("/usuarios", true)
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
);
return http.build();
}
}
CustomUserDetailsServiceO serviço agora precisa de carregar os perfis (authorities) do utilizador para que o Spring Security possa tomar decisões.
Ficheiro: src/main/java/br/com/curso/loginusuarios/service/CustomUserDetailsService.java
package br.com.curso.loginusuarios.service;
import br.com.curso.loginusuarios.model.User;
import br.com.curso.loginusuarios.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
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.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Collections;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("Utilizador não encontrado com o email: " + email));
// Constrói o UserDetails, incluindo a autoridade (perfil) do utilizador
return new org.springframework.security.core.userdetails.User(
user.getEmail(),
user.getSenha(),
Collections.singletonList(new SimpleGrantedAuthority(user.getRole().name()))
);
}
public void save(User user) {
user.setSenha(passwordEncoder.encode(user.getSenha()));
userRepository.save(user);
}
}
DataInitializer (Criador do Utilizador Admin)Para garantir o acesso inicial, esta classe cria um utilizador admin com a senha admin123 na primeira vez que a aplicação é executada.
Crie o pacote util e, dentro dele, a classe DataInitializer.java:
package br.com.curso.loginusuarios.util;
import br.com.curso.loginusuarios.model.Role;
import br.com.curso.loginusuarios.model.User;
import br.com.curso.loginusuarios.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
@Component
public class DataInitializer implements CommandLineRunner {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void run(String... args) throws Exception {
// Se não houver nenhum utilizador com o perfil ADMIN, cria um
if (userRepository.findAll().stream().noneMatch(user -> user.getRole() == Role.ADMIN)) {
User admin = new User();
admin.setNome("Administrador");
admin.setEmail("admin@email.com");
admin.setSenha(passwordEncoder.encode("admin123")); // Senha inicial
admin.setRole(Role.ADMIN);
userRepository.save(admin);
System.out.println(">>> Utilizador ADMIN padrão criado com sucesso!");
}
}
}
AuthController (Login e Registo)Este controlador agora atribui o perfil USER a todos os novos registos.
Ficheiro: src/main/java/br/com/curso/loginusuarios/controller/AuthController.java
package br.com.curso.loginusuarios.controller;
import br.com.curso.loginusuarios.model.Role;
import br.com.curso.loginusuarios.model.User;
import br.com.curso.loginusuarios.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class AuthController {
@Autowired
private CustomUserDetailsService userService;
@GetMapping("/login")
public String showLoginPage() {
return "login";
}
@GetMapping("/registar")
public String showRegistrationForm(Model model) {
model.addAttribute("user", new User());
return "registar";
}
@PostMapping("/registar")
public String processRegistration(@ModelAttribute User user) {
user.setRole(Role.USER); // Todos os novos utilizadores são do tipo USER
userService.save(user);
return "redirect:/login?success";
}
}
UserController (Gestão de Utilizadores)Este controlador foi expandido para incluir a funcionalidade de apagar utilizadores, que só será acessível a um ADMIN.
Ficheiro: src/main/java/br/com/curso/loginusuarios/controller/UserController.java
package br.com.curso.loginusuarios.controller;
import br.com.curso.loginusuarios.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/usuarios")
public class UserController {
@Autowired
private UserRepository userRepository;
@GetMapping
public String listUsers(Model model) {
model.addAttribute("users", userRepository.findAll());
return "usuarios";
}
// Endpoint para apagar um utilizador, acessível via POST para segurança
@PostMapping("/apagar/{id}")
public String deleteUser(@PathVariable("id") Long id) {
// Adicionar verificação para não se auto-apagar, se necessário
userRepository.deleteById(id);
return "redirect:/usuarios?deleted";
}
}
Todas as páginas foram redesenhadas para serem mais modernas e informativas.
login.html)Um design mais limpo e com feedback claro para o utilizador.
Ficheiro: src/main/resources/templates/login.html
<!DOCTYPE html>
<html lang="pt" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - Gestão de Utilizadores</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
</head>
<body class="bg-light">
<div class="container">
<div class="row justify-content-center align-items-center" style="height: 100vh;">
<div class="col-md-5 col-lg-4">
<div class="card shadow-sm border-0">
<div class="card-body p-4">
<h3 class="card-title text-center mb-4">
<i class="bi bi-box-arrow-in-right me-2"></i>Aceder ao Sistema
</h3>
<div th:if="${param.error}" class="alert alert-danger" role="alert"> Email ou senha inválidos. </div> <div th:if="${param.logout}" class="alert alert-info" role="alert">
Sessão terminada com sucesso.
</div>
<div th:if="${param.success}" class="alert alert-success" role="alert">
Registo efetuado! Faça o login para continuar.
</div>
<form th:action="@{/login}" method="post">
<div class="mb-3">
<label for="username" class="form-label">Email</label>
<input type="email" id="username" name="username" class="form-control" required autofocus>
</div>
<div class="mb-3">
<label for="password" class="form-label">Senha</label>
<input type="password" id="password" name="password" class="form-control" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-primary">Entrar</button>
</div>
</form>
<div class="text-center mt-3">
<small class="text-muted">Não tem uma conta? <a th:href="@{/registar}">Registe-se aqui</a></small>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
registar.html)Formulário de registo simples e direto.
Ficheiro: src/main/resources/templates/registar.html
<!DOCTYPE html>
<html lang="pt" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Registar - Gestão de Utilizadores</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
</head>
<body class="bg-light">
<div class="container">
<div class="row justify-content-center align-items-center" style="height: 100vh;">
<div class="col-md-5 col-lg-4">
<div class="card shadow-sm border-0">
<div class="card-body p-4">
<h3 class="card-title text-center mb-4">
<i class="bi bi-person-plus-fill me-2"></i>Criar Nova Conta
</h3>
<form th:action="@{/registar}" th:object="${user}" method="post">
<div class="mb-3">
<label for="nome" class="form-label">Nome Completo</label>
<input type="text" id="nome" th:field="*{nome}" class="form-control" required>
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input type="email" id="email" th:field="*{email}" class="form-control" required>
</div>
<div class="mb-3">
<label for="senha" class="form-label">Senha</label>
<input type="password" id="senha" th:field="*{senha}" class="form-control" required>
</div>
<div class="d-grid">
<button type="submit" class="btn btn-success">Registar</button>
</div>
</form>
<div class="text-center mt-3">
<small class="text-muted">Já tem uma conta? <a th:href="@{/login}">Faça o login</a></small>
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
usuarios.html)Esta página agora utiliza o dialeto de segurança do Thymeleaf (xmlns:sec) para exibir condicionalmente o botão de apagar. Apenas utilizadores com a autoridade ADMIN verão este botão.
Ficheiro: src/main/resources/templates/usuarios.html
<!DOCTYPE html>
<html lang="pt" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Painel de Utilizadores</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-dark shadow-sm">
<div class="container-fluid">
<a class="navbar-brand" href="#">
<i class="bi bi-shield-lock-fill me-2"></i>Painel de Gestão
</a>
<form class="d-flex" th:action="@{/logout}" method="post">
<button class="btn btn-outline-light" type="submit">
<i class="bi bi-box-arrow-right me-2"></i>Sair
</button>
</form>
</div>
</nav>
<div class="container mt-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3">Utilizadores Registados</h1>
</div>
<div th:if="${param.deleted}" class="alert alert-warning" role="alert"> Utilizador apagado com sucesso. </div> <div class="card border-0 shadow-sm"> <div class="card-body"> <div class="table-responsive"> <table class="table table-hover align-middle"> <thead class="table-light"> <tr> <th>ID</th> <th>Nome</th> <th>Email</th> <th>Perfil</th> <th sec:authorize="hasAuthority('ADMIN')" class="text-end">Ações</th> </tr> </thead> <tbody> <tr th:each="user :${users}">
<td th:text="${user.id}"></td> <td th:text="${user.nome}"></td>
<td th:text="${user.email}"></td> <td> <span th:if="${user.role.name() == 'ADMIN'}" class="badge bg-success" th:text="${user.role.name()}"></span> <span th:if="${user.role.name() == 'USER'}" class="badge bg-secondary" th:text="${user.role.name()}"></span> </td> <!-- Este formulário só será visível para utilizadores com o perfil ADMIN --> <td sec:authorize="hasAuthority('ADMIN')" class="text-end"> <form th:action="@{/usuarios/apagar/{id}(id=${user.id})}" method="post" onsubmit="return confirm('Tem a certeza que deseja apagar este utilizador?');">
<button type="submit" class="btn btn-sm btn-outline-danger" th:disabled="${#authentication.name == user.email}">
<i class="bi bi-trash-fill"></i> Apagar
</button>
</form>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</body>
</html>
LoginusuariosApplication.java.admin@email.com e admin123.Para permitir que o H2 Console seja acedido pelo navegador, é necessário adicionar algumas configurações extra no SecurityConfig.java, pois o Spring Security, por padrão, bloqueia o uso de iframes e exige CSRF para o console.
Ficheiro Final Sugerido: src/main/java/br/com/curso/loginusuarios/config/SecurityConfig.java
package br.com.curso.loginusuarios.config;
import br.com.curso.loginusuarios.model.Role;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// Configurações específicas para o H2 Console (Iframe e CSRF)
.csrf(csrf -> csrf.ignoringRequestMatchers("/h2-console/**"))
.headers(headers -> headers.frameOptions(frameOptions -> frameOptions.sameOrigin()))
.authorizeHttpRequests(authorize -> authorize
// Acesso livre ao login, registo, recursos estáticos e H2 Console
.requestMatchers("/registar", "/login", "/css/**", "/js/**", "/h2-console/**").permitAll()
// Apenas ADMINS podem apagar outros utilizadores
.requestMatchers(HttpMethod.POST, "/usuarios/apagar/**").hasAuthority(Role.ADMIN.name())
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
.defaultSuccessUrl("/usuarios", true)
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
);
return http.build();
}
}
—`
No seu editor/IDE, abra a pasta deste projeto (File > Open Folder) ou navegue via terminal:
cd javaweb_login_01_form
./mvnw spring-boot:run
./mvnw test
[!TIP] Dica para execução a partir da raiz do repositório: Se você abriu o repositório completo no VS Code, basta navegar até a pasta antes de executar: cd proj_aplicacoes_full_stack/projetos/javaweb_login_01_form