Here's a complete example of a Spring Boot 3 application with Spring Security 6 and Thymeleaf for user registration and login functionality.
Final Project Directory Structure

Complete pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Dependency List
Here are all the dependencies used in the project:
- Spring Boot Starters:
spring-boot-starter-data-jpa
(for JPA and Hibernate)spring-boot-starter-security
(for Spring Security 6)spring-boot-starter-thymeleaf
(for Thymeleaf templating)spring-boot-starter-web
(for web MVC)spring-boot-starter-validation
(for bean validation)
- Thymeleaf Extras:
thymeleaf-extras-springsecurity6
(for Thymeleaf + Spring Security integration)
- Database:
h2
(embedded database)
- Development Tools:
lombok
(for reducing boilerplate code)
- Testing:
spring-boot-starter-test
(for general testing)spring-security-test
(for security testing)
Key Configuration Files
- application.yaml:
spring:
datasource:
url: jdbc:h2:mem:testdb
driverClassName: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true
path: /h2-console
jpa:
show-sql: true
hibernate:
ddl-auto: update
properties:
hibernate:
format_sql: true
thymeleaf:
cache: false
- data.sql (for initial data):
INSERT INTO roles(name) VALUES('ROLE_USER');
INSERT INTO roles(name) VALUES('ROLE_ADMIN');
Implementation Steps
1. Entity Classes
User.java
package com.example.demo.model;
import jakarta.persistence.*;
import lombok.Data;
import java.util.Collection;
@Data
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String firstName;
private String lastName;
private String email;
private String password;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(
name = "users_roles",
joinColumns = @JoinColumn(name = "user_id", referencedColumnName = "id"),
inverseJoinColumns = @JoinColumn(name = "role_id", referencedColumnName = "id")
)
private Collection<Role> roles;
}
Role.java
package com.example.demo.model;
import jakarta.persistence.*;
import lombok.Data;
@Data
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String name;
}
2. DTO Classes
UserRegistrationDto.java
package com.example.demo.dto;
import lombok.Data;
import java.util.Collection;
@Data
public class UserRegistrationDto {
private String firstName;
private String lastName;
private String email;
private String password;
private Collection<String> roles;
}
UserDto.java
package com.example.demo.dto;
import lombok.Data;
import java.util.Collection;
@Data
public class UserDto {
private String email;
private String password;
}
3. Repository Interfaces
UserRepository.java
package com.example.demo.repository;
import com.example.demo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<User, Long> {
User findByEmail(String email);
}
RoleRepository.java
package com.example.demo.repository;
import com.example.demo.model.Role;
import org.springframework.data.jpa.repository.JpaRepository;
public interface RoleRepository extends JpaRepository<Role, Long> {
Role findByName(String name);
}
4. Service Layer
UserService.java
package com.example.demo.service;
import com.example.demo.dto.UserRegistrationDto;
import com.example.demo.model.User;
import org.springframework.security.core.userdetails.UserDetailsService;
public interface UserService extends UserDetailsService {
User save(UserRegistrationDto registrationDto);
}
UserServiceImpl.java
package com.example.demo.service;
import com.example.demo.dto.UserRegistrationDto;
import com.example.demo.model.Role;
import com.example.demo.model.User;
import com.example.demo.repository.RoleRepository;
import com.example.demo.repository.UserRepository;
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.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
@Service
public class UserServiceImpl implements UserService {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final BCryptPasswordEncoder passwordEncoder;
public UserServiceImpl(UserRepository userRepository,
RoleRepository roleRepository,
BCryptPasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.passwordEncoder = passwordEncoder;
}
@Override
public User save(UserRegistrationDto registrationDto) {
User user = new User();
user.setFirstName(registrationDto.getFirstName());
user.setLastName(registrationDto.getLastName());
user.setEmail(registrationDto.getEmail());
user.setPassword(passwordEncoder.encode(registrationDto.getPassword()));
user.setRoles(Arrays.asList(roleRepository.findByName("ROLE_USER")));
return userRepository.save(user);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByEmail(username);
if (user == null) {
throw new UsernameNotFoundException("Invalid username or password.");
}
return new org.springframework.security.core.userdetails.User(
user.getEmail(),
user.getPassword(),
mapRolesToAuthorities(user.getRoles())
);
}
private Collection<? extends GrantedAuthority> mapRolesToAuthorities(Collection<Role> roles) {
return roles.stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
}
}
5. Security Configuration
SecurityConfig.java
package com.example.demo.config;
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 SecurityConfig {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
return new UserServiceImpl();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService());
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/register", "/register/**", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/home")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/login?logout")
.permitAll()
);
return http.build();
}
}
6. Web MVC Configuration
WebMvcConfig.java
package com.example.demo.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/login").setViewName("login");
}
}
7. Controller Classes
AuthController.java
package com.example.demo.controller;
import com.example.demo.dto.UserRegistrationDto;
import com.example.demo.service.UserService;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping("/register")
public class AuthController {
private final UserService userService;
public AuthController(UserService userService) {
this.userService = userService;
}
@ModelAttribute("user")
public UserRegistrationDto userRegistrationDto() {
return new UserRegistrationDto();
}
@GetMapping
public String showRegistrationForm() {
return "register";
}
@PostMapping
public String registerUserAccount(@ModelAttribute("user") UserRegistrationDto registrationDto) {
userService.save(registrationDto);
return "redirect:/register?success";
}
}
HomeController.java
package com.example.demo.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class HomeController {
@GetMapping("/home")
public String home() {
return "home";
}
}
8. Thymeleaf Templates
src/main/resources/templates/login.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Login</title>
<link rel="stylesheet" th:href="@{/css/style.css}"/>
</head>
<body>
<div class="container">
<h2>Login</h2>
<form th:action="@{/login}" method="post">
<div th:if="${param.error}" class="alert alert-danger">
Invalid username or password.
</div>
<div th:if="${param.logout}" class="alert alert-success">
You have been logged out.
</div>
<div>
<label>Email:</label>
<input type="email" name="username" required/>
</div>
<div>
<label>Password:</label>
<input type="password" name="password" required/>
</div>
<div>
<button type="submit">Login</button>
</div>
</form>
<p>Don't have an account? <a th:href="@{/register}">Register here</a></p>
</div>
</body>
</html>
src/main/resources/templates/register.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Registration</title>
<link rel="stylesheet" th:href="@{/css/style.css}"/>
</head>
<body>
<div class="container">
<h2>Registration</h2>
<form th:action="@{/register}" th:object="${user}" method="post">
<div th:if="${param.success}" class="alert alert-success">
Registration successful! Please login.
</div>
<div>
<label>First Name:</label>
<input type="text" th:field="*{firstName}" required/>
</div>
<div>
<label>Last Name:</label>
<input type="text" th:field="*{lastName}" required/>
</div>
<div>
<label>Email:</label>
<input type="email" th:field="*{email}" required/>
</div>
<div>
<label>Password:</label>
<input type="password" th:field="*{password}" required/>
</div>
<div>
<button type="submit">Register</button>
</div>
</form>
<p>Already have an account? <a th:href="@{/login}">Login here</a></p>
</div>
</body>
</html>
src/main/resources/templates/home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
<meta charset="UTF-8">
<title>Home</title>
<link rel="stylesheet" th:href="@{/css/style.css}"/>
</head>
<body>
<div class="container">
<h2>Welcome <span sec:authentication="name"></span>!</h2>
<p>This is the home page of our application.</p>
<form th:action="@{/logout}" method="post">
<button type="submit">Logout</button>
</form>
</div>
</body>
</html>
9. CSS (optional)
src/main/resources/static/css/style.css
body {
font-family: Arial, sans-serif;
margin: 0;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: #f4f4f4;
}
.container {
background: #fff;
padding: 20px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
width: 300px;
}
h2 {
text-align: center;
margin-bottom: 20px;
}
div {
margin-bottom: 15px;
}
label {
display: block;
margin-bottom: 5px;
}
input {
width: 100%;
padding: 8px;
box-sizing: border-box;
}
button {
width: 100%;
padding: 10px;
background-color: #007bff;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
}
button:hover {
background-color: #0056b3;
}
.alert {
padding: 10px;
margin-bottom: 15px;
border-radius: 5px;
}
.alert-danger {
background-color: #f8d7da;
color: #721c24;
}
.alert-success {
background-color: #d4edda;
color: #155724;
}
a {
color: #007bff;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
Running the Application
- Start the Spring Boot application
- Access the application at
http://localhost:8080
- Register a new user at
http://localhost:8080/register
- Login at
http://localhost:8080/login
- Access the secured home page at
http://localhost:8080/home
Key Features
- User registration with form validation
- Secure password storage with BCrypt
- Role-based authentication
- Custom login page
- Thymeleaf integration with Spring Security
- H2 database for development
This example provides a complete foundation that you can extend with additional features like email verification, password reset, and more advanced role management.