Spring Boot 3 + Spring Security 6 + Thymeleaf - Registration and Login Example

In this section we will learn how to create user registration and login using Spring boot 3, Spring security 6, Thymeleaf, JPA, and PostgreSQL. The GitHub repository link is provided at the end of this tutorial. You can download the source code.


What’s New in Spring Security 6?

  • WebSecurityConfigurerAdapter Removed: The WebSecurityConfigurerAdapter class was deprecated and then removed in Spring Security 6. Instead, you should now take a more component-based approach and create a bean of type SecurityFilterChain. Here's an example:
@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/registration**").permitAll()
.anyRequest().authenticated()
)
.formLogin((form) -> form
.loginPage("/login")
.permitAll()
)
.logout((logout) -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.invalidateHttpSession(true));

return http.build();
}
}


  • Method authorizeRequests() has been deprecated and shouldn’t be used anymore: The authorizeRequests() method of the WebSecurityConfigurerAdapter class has been deprecated and shouldn’t be used anymore. This method was previously used to configure the authorization rules for securing web applications. Instead, you should use the HttpSecurity class and its authorizeHttpRequests() method instead. Here's an example:
   @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.
authorizeHttpRequests((requests) -> requests
.requestMatchers(
"/registration**").permitAll()
.anyRequest().authenticated()
)
.formLogin((form) -> form
.loginPage(
"/login")
.permitAll()
)
.logout((logout) -> logout
.logoutUrl(
"/logout")
.logoutSuccessUrl(
"/")
.invalidateHttpSession(
true));

return http.build();
}
}


  • Requestmatchers Replacing Antmatcher, Mvcmatcher, and Regexmatcher:In Spring Security 6.0, antMatchers() as well as other configuration methods for securing requests (namely mvcMatchers() and regexMatchers()) have been removed from the API. An overloaded method requestMatchers() was introduced as a uniform mean for securing requests. The requestMatchers() facilitate all the ways of restricting requests that were supported by the removed methods. Here's an example that permits access to the /registration endpoint without authentication.
   @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.
authorizeHttpRequests((requests) -> requests
.requestMatchers("/registration").permitAll()
.anyRequest().authenticated()
)
.formLogin((form) -> form
.loginPage(
"/login")
.permitAll()
)
.logout((logout) -> logout
.logoutUrl(
"/logout")
.logoutSuccessUrl(
"/")
.invalidateHttpSession(
true));

return http.build();
}
}


  • Regarding deprecated annotation @EnableGlobalMethodSecurity it was replaced with @EnableMethodSecurity: Note that you can avoid using prePostEnabled = true, because by default is true.
boolean prePostEnabled() default true;
boolean jsr250Enabled() default false;
boolean proxyTargetClass() default false;

Technologies and Tools Used:

  • Java 17 or Later
  • Spring Boot 3+
  • Spring Framework 6+
  • Spring Security 6.1.4
  • Hibernate 6+
  • JPA
  • Maven
  • Thymeleaf
  • PostgreSQL 

User Interface

User Login


User Registration


Login Successful

Project Structure:


Maven Dependencies - pom.xml file

<?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.1.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.knf.dev.demo</groupId>
<artifactId>registration-login-spring-boot-security6-thymeleaf</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>registration-login-spring-boot-security6-thymeleaf</name>
<description>Demo project for Spring Boot + Spring Security 6 + Thymeleaf</description>
<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-web</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</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.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

Configure PostgreSQL Database
Let’s configure Spring Boot to use PostgreSQL as our data source. We are simply adding PostgreSQL database URL, username, and password in the src/main/resources/application.yaml file -
spring:
datasource:
url: jdbc:postgresql://localhost:5432/mydb
username: postgres
password: root
jpa:
hibernate:
ddl-auto: update # Hibernate ddl auto (create, create-drop, validate, update),production set to none or comment it
show-sql: true
database-platform: org.hibernate.dialect.PostgreSQLDialect
open-in-view: false
generate-ddl: true

Create JPA Entity - User.java
package com.knf.dev.demo.entity;

import jakarta.persistence.*;
import java.util.Collection;

@Entity
@Table(name = "my_user", uniqueConstraints =
@UniqueConstraint(columnNames = "email"))
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(name = "first_name")
private String firstName;

@Column(name = "last_name")
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;

public User() {

}

public User(String firstName, String lastName,
String email, String password,
Collection<Role> roles) {

this.firstName = firstName;
this.lastName = lastName;
this.email = email;
this.password = password;
this.roles = roles;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public Collection<Role> getRoles() {
return roles;
}

public void setRoles(Collection<Role> roles) {
this.roles = roles;
}
}
The @Entity annotation specifies that the class is an entity and is mapped to a database table. The @Table annotation specifies the name of the database table to be used for mapping. The @Id annotation specifies the primary key of an entity and the @GeneratedValue provides for the specification of generation strategies for the values of primary keys. The @Column annotation is used to specify the mapped column for a persistent property or field. If no Column annotation is specified, the default value will be applied. We use @ManyToMany annotation to create a many-to-many relationship between two entities.

Create JPA Entity - Role.java
package com.knf.dev.demo.entity;

import jakarta.persistence.*;

@Entity
@Table(name = "role")
public class Role {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;

public Role() {

}

public Role(String name) {
super();
this.name = name;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}
The @Entity annotation specifies that the class is an entity and is mapped to a database table. The @Table annotation specifies the name of the database table to be used for mapping. The @Id annotation specifies the primary key of an entity and the @GeneratedValue provides for the specification of generation strategies for the values of primary keys.

Create User Repository
package com.knf.dev.demo.repository;

import com.knf.dev.demo.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User, Long> {

User findByEmail(String email);
}
JpaRepository is a JPA-specific extension of Repository. It contains an API for basic CRUD operations and also API for pagination and sorting.

Create User Service
package com.knf.dev.demo.service;

import com.knf.dev.demo.dto.UserRegistrationDto;
import com.knf.dev.demo.entity.User;
import org.springframework.security.core.userdetails.UserDetailsService;

import java.util.List;

public interface UserService extends UserDetailsService {

User save(UserRegistrationDto registrationDto);
List<User> getAll();
}
The UserDetailsService interface is used to retrieve user-related data. It has one method named loadUserByUsername().

Create User Service Implementation
package com.knf.dev.demo.service.impl;


import com.knf.dev.demo.dto.UserRegistrationDto;
import com.knf.dev.demo.entity.Role;
import com.knf.dev.demo.entity.User;
import com.knf.dev.demo.repository.UserRepository;
import com.knf.dev.demo.service.UserService;
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.List;
import java.util.stream.Collectors;

@Service
public class UserServiceImpl implements UserService {

private UserRepository userRepository;
private BCryptPasswordEncoder passwordEncoder;

public UserServiceImpl(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder) {
super();
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}

@Override
public User save(UserRegistrationDto registrationDto) {

var user = new User(registrationDto.getFirstName(),
registrationDto.getLastName(),
registrationDto.getEmail(),
passwordEncoder.encode(registrationDto
.getPassword()),
Arrays.asList(new Role("ROLE_ADMIN")));

return userRepository.save(user);
}

@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {

var user = userRepository.findByEmail(username);
if (user == null) {
throw new UsernameNotFoundException
("Invalid username or password.");
}
return new org.springframework.security
.core.userdetails.User(user.getFirstName(),
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());
}

@Override
public List<User> getAll() {

return userRepository.findAll();
}
}

Create User Registration DTO
package com.knf.dev.demo.dto;

public class UserRegistrationDto {

private String firstName;
private String lastName;
private String email;
private String password;

public UserRegistrationDto() {

}

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}
}

Create Security Configuration
package com.knf.dev.demo.config;

import com.knf.dev.demo.service.UserService;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfiguration {

@Bean
public BCryptPasswordEncoder passwordEncoder() {

return new BCryptPasswordEncoder();
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((requests) -> requests
.requestMatchers("/registration").permitAll()
.anyRequest().authenticated()
)
.formLogin((form) -> form
.loginPage("/login")
.permitAll()
)
.logout((logout) -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.invalidateHttpSession(true));

return http.build();
}
}

Create Home Controller
package com.knf.dev.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class HomeController {

@GetMapping("/login")
public String login() {
return "login";
}

@GetMapping("/")
public String home() {
return "index";
}
}

Create Registration Controller
package com.knf.dev.demo.controller;

import com.knf.dev.demo.dto.UserRegistrationDto;
import com.knf.dev.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("/registration")
public class RegistrationController {

private UserService userService;

public RegistrationController(UserService userService) {
super();
this.userService = userService;
}

@ModelAttribute("user")
public UserRegistrationDto userRegistrationDto() {
return new UserRegistrationDto();
}

@GetMapping
public String showRegistrationForm() {
return "registration";
}

@PostMapping
public String registerUserAccount(@ModelAttribute("user")
UserRegistrationDto registrationDto) {

try {
userService.save(registrationDto);
}catch(Exception e)
{
System.out.println(e);
return "redirect:/registration?email_invalid";
}
return "redirect:/registration?success";
}
}

Spring Boot Main Driver
package com.knf.dev.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}

}

View Layer
index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="ISO-8859-1">
<title>Spring Security 6 + Thymeleaf Demo</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
</head>

<body>
<!-- create navigation bar ( header) -->
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed"
data-toggle="collapse" data-target="#navbar"
aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span> <span class="icon-bar">
</span>
</button>
<a class="navbar-brand">
Registration and Login with Spring Boot 3 + Spring Security 6 + Thymeleaf</a>
</div>
<div id="navbar" class="collapse navbar-collapse">
<ul class="nav navbar-nav navbar-right">
<li sec:authorize="isAuthenticated()">
<a href="javascript: document.logoutForm.submit()" class="dropdown-toggle">Sign out</a>
<form name="logoutForm" th:action="@{/logout}" method="post" th:hidden="true">
<input hidden type="submit" value="Sign Out" />
</form>
</li>
</ul>
</div>
</div>
</nav>

<br><br><br><br>
<div class="container">
<div class="alert alert-success">
Hello <strong><span sec:authentication="principal.username"></span></strong>
Login Successful.
</div>
</div>
<input type="hidden" th:name="${_csrf.parameterName}"
th:value="${_csrf.token}" />
</body>
</html>

login.html
<!DOCTYPE html>
<html>
<head>
<meta charset="ISO-8859-1">
<title>Spring Security 6 + Thymeleaf Demo</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
</head>

<body>
<!-- create navigation bar ( header) -->
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed"
data-toggle="collapse" data-target="#navbar"
aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span> <span
class="icon-bar"></span> <span class="icon-bar"></span>
</button>
<a class="navbar-brand">
Registration and Login with Spring Boot 3 + Spring Security 6 + Thymeleaf</a>
</div>
</div>
</nav>

<br><br><br><br><br><br><br>
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3">

<h1>User Login Page</h1>
<form th:action="@{/login}" method="post">

<!-- error message -->
<div th:if="${param.error}">
<div class="alert alert-danger">Invalid username or
password.</div>
</div>

<!-- logout message -->
<div th:if="${param.logout}">
<div class="alert alert-info">
You have been logged out.</div>
</div>

<div class="form-group">
<label for="username"> Email </label> :
<input type="text" class="form-control" id="username"
name="username" placeholder="Enter Email ID"
autofocus="autofocus">
</div>

<div class="form-group">
<label for="password">Password</label>:
<input type="password" id="password" name="password"
class="form-control" placeholder="Enter Password" />
</div>

<div class="form-group">
<div class="row">
<div class="col-sm-6 col-sm-offset-3">
<input type="submit" name="login-submit"
id="login-submit"
class="form-control btn btn-primary"
value="Log In" />
</div>
</div>
</div>
</form>
<div class="form-group">
<span>New user? <a href="/" th:href="@{/registration}">
Register
here</a></span>
</div>
</div>
</div>
</div>
</body>
</html>

registration.html
<!DOCTYPE html>
<html>
<head>
<meta charset="ISO-8859-1">
<title>Spring Security 6 + Thymeleaf Demo</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css">
</head>

<body>
<!-- create navigation bar ( header) -->
<nav class="navbar navbar-inverse navbar-fixed-top">
<div class="container">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed"
data-toggle="collapse" data-target="#navbar"
aria-expanded="false" aria-controls="navbar">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span> <span
class="icon-bar"></span> <span class="icon-bar"></span>
</button>
<a class="navbar-brand">
Registration and Login with Spring Boot 3 + Spring Security 6 + Thymeleaf</a>
</div>
</div>
</nav>

<br><br><br><br><br><br><br>
<!-- Create HTML registration form -->
<div class="container">
<div class="row">
<div class="col-md-6 col-md-offset-3">

<!-- success message -->
<div th:if="${param.success}">
<div class="alert alert-info">You've successfully registered
to our awesome app!</div>
</div>

<div th:if="${param.email_invalid}">
<div class="alert alert-danger">
Email is Already Registered!</div>
</div>

<h1>Registration</h1>

<form th:action="@{/registration}" method="post"
th:object="${user}">
<div class="form-group">
<label class="control-label" for="firstName">
First Name </label>
<input id="firstName" class="form-control"
th:field="*{firstName}" required
autofocus="autofocus" />
</div>

<div class="form-group">
<label class="control-label" for="lastName">
Last Name </label> <input id="lastName"
class="form-control" th:field="*{lastName}"
required autofocus="autofocus" />
</div>

<div class="form-group">
<label class="control-label" for="email"> Email
</label> <input id="email" class="form-control"
th:field="*{email}" required autofocus="autofocus"
/>
</div>

<div class="form-group">
<label class="control-label" for="password">
Password </label> <input id="password"
class="form-control" type="password"
th:field="*{password}" required
autofocus="autofocus" />
</div>

<div class="form-group">
<button type="submit" class="btn btn-primary">
Register</button>
<span>Already registered? <a href="/"
th:href="@{/login}">Login
here</a></span>
</div>
</form>
</div>
</div>
</div>
</body>
</html>

Local Setup and Run the application

Step 1: Download or clone the source code from GitHub to a local machine - Click here


Step 2: mvn clean install


Step 3: Run the Spring Boot application - mvn spring-boot:run

From the browser call the endpoint http://localhost:8080/

Popular posts from this blog

Learn Java 8 streams with an example - print odd/even numbers from Array and List

Java Stream API - How to convert List of objects to another List of objects using Java streams?

Registration and Login with Spring Boot + Spring Security + Thymeleaf

Java, Spring Boot Mini Project - Library Management System - Download

ReactJS, Spring Boot JWT Authentication Example

Spring Boot + Mockito simple application with 100% code coverage

Top 5 Java ORM tools - 2024

Java - Blowfish Encryption and decryption Example

Spring boot video streaming example-HTML5

Google Cloud Storage + Spring Boot - File Upload, Download, and Delete