User Registration Module using Spring Boot + Spring MVC + Spring Security + Hibernate 5 + Thymeleaf + MySQL

In this article, we discuss how to create a user registration form with Spring Boot, Spring Security, Hibernate and Thymeleaf. We validate the user registration fields with hibernate validator annotations and a custom field matching validator to validate if the email and/or password fields match. We will create Role-based Spring security with a MySQL database. 
Basically, we will develop a simple User Registration Module using Role-based Spring security which can use in any spring MVC based projects.
We create User and Role tables with many to many relationships between them that is one user can have multiple roles and one role can be assigned to multiple users.
In the previous article, we have learned Spring Boot + Spring MVC + Role Based Spring Security + JPA + Thymeleaf + MySQL Tutorial
Let's get started with our objective what we'll build?

What we’ll build?

We will develop a simple User Registration Module using Spring Boot 2, Spring MVC, Spring Security, Hibernate 5, Thymeleaf and MySQL. We validate the user registration fields with hibernate validator annotations and a custom field matching validator to validate if the email and/or password fields match. We will create Role-based Spring security with a MySQL database.
We will develop two main functionalities:
1. Register user (stored data into MySQL database).
2. Login Authentication - validate user login credentials with database email and password.

Tools and Technologies Used

  • Spring Boot - 2.0.4.RELEASE
  • JDK - 1.8 or later
  • Spring Framework - 5.0.8 RELEASE
  • Hibernate - 5.2.17. Final
  • Maven - 3.2+
  • IDE - Eclipse or Spring Tool Suite (STS)
  • Tomcat - 8.5+
  • Thymeleaf - 3.0.9 RELEASE
  • Bootstrap - 3.3.7
  • JQuery - 3.2.1
  • MySQL - 5.1.46

Creating and Importing a Project

There are many ways to create a Spring Boot application. The simplest way is to use Spring Initializr at http://start.spring.io/, which is an online Spring Boot application generator.

Look at the above diagram, we have specified the following details:
  • Generate: Maven Project
  • Java Version: 1.8 (Default)
  • Spring Boot:2.0.4
  • Group: net.guides.springboot
  • Artifact: registration-login-springboot-security-thymeleaf
  • Name: registration-login-springboot-security-thymeleaf
  • Package Name : net.javaguides.springboot.springsecurity
  • Packaging: jar (This is the default value)
  • Dependencies: Web, JPA, MySQL,Thymeleaf,Security
Click on the Generate Project button. Now you can extract the downloaded ZIP file and import it into your favorite IDE.

Project Structure

Let’s start by looking at the project structure.

The pom.xml File - Maven Dependencies

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>net.guides.springboot</groupId>
    <artifactId>registration-login-springboot-security-thymeleaf</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>registration-login-springboot-security-thymeleaf</name>
    <description>Demo project for Spring Boot</description>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</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-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity4</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- https://mvnrepository.com/artifact/commons-beanutils/commons-beanutils -->
        <dependency>
           <groupId>commons-beanutils</groupId>
           <artifactId>commons-beanutils</artifactId>
            <version>1.9.3</version>
       </dependency>


        <!-- bootstrap and jquery -->
        <dependency>
            <groupId>org.webjars</groupId>
            <artifactId>bootstrap</artifactId>
            <version>3.3.7</version>
        </dependency>
        <dependency>
             <groupId>org.webjars</groupId>
             <artifactId>jquery</artifactId>
             <version>3.2.1</version>
        </dependency>

        <!-- mysql connector -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

        <!-- testing -->
        <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>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>test</scope>
       </dependency>
    </dependencies>

    <build>
       <plugins>
          <plugin>
              <groupId>org.springframework.boot</groupId>
              <artifactId>spring-boot-maven-plugin</artifactId>
          </plugin>
      </plugins>
    </build>
</project>

Spring Security Configuration

We secure our application using Spring Security Form Authentication using the following configuration. Make sure you permit all access to the /registration page and your static resources.
package net.javaguides.springboot.springsecurity.config;

import org.springframework.beans.factory.annotation.Autowired;
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.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import net.javaguides.springboot.springsecurity.service.UserService;

@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserService userService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
            .antMatchers(
                "/registration**",
                "/js/**",
                "/css/**",
                "/img/**",
                "/webjars/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin()
            .loginPage("/login")
            .permitAll()
            .and()
            .logout()
            .invalidateHttpSession(true)
            .clearAuthentication(true)
            .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
            .logoutSuccessUrl("/login?logout")
            .permitAll();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider auth = new DaoAuthenticationProvider();
        auth.setUserDetailsService(userService);
        auth.setPasswordEncoder(passwordEncoder());
        return auth;
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(authenticationProvider());
    }
}

Configure MySQL and View Resolver

We configure Hibernate using the application.properties file located in the src/main/resources/ folder. Note that we also configure thymeleaf view resolver.
## Spring view resolver set up
spring.mvc.view.prefix=/WEB-INF/jsp/
spring.mvc.view.suffix=.jsp

## Spring DATASOURCE (DataSourceAutoConfiguration & DataSourceProperties)
spring.datasource.url = jdbc:mysql://localhost:3306/registration_module?useSSL=false
spring.datasource.username = root
spring.datasource.password = root

## Hibernate Properties
# The SQL dialect makes Hibernate generate better SQL for the chosen database
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect

# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update

Database Create SQL Scripts

As we specified spring.jpa.hibernate.ddl-auto = update property will auto create tables in MySQL database. The below are create statements of three tables.
CREATE TABLE `role` (
  `id` bigint(20) NOT NULL,
  `name` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE `user` (
  `id` bigint(20) NOT NULL,
  `email` varchar(255) DEFAULT NULL,
  `first_name` varchar(255) DEFAULT NULL,
  `last_name` varchar(255) DEFAULT NULL,
  `password` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `UKob8kqyqqgmefl0aco34akdtpe` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;


CREATE TABLE `users_roles` (
  `user_id` bigint(20) NOT NULL,
  `role_id` bigint(20) NOT NULL,
  KEY `FKt4v0rrweyk393bdgt107vdx0x` (`role_id`),
  KEY `FKgd3iendaoyh04b95ykqise6qh` (`user_id`),
  CONSTRAINT `FKgd3iendaoyh04b95ykqise6qh` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`),
  CONSTRAINT `FKt4v0rrweyk393bdgt107vdx0x` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

Spring Rest Controller - UserRegistrationController.java

This controller is mapped to “/registration” URI. We use the UserRegistrationDto to process and validate the user registration form and inject it using the @ModelAttribute("user") annotation. When the form is submitted it’s automatically validated and errors are available in the BindingResult. Next, we check if a user doesn’t already exist with the submitted email. If the form has any errors, we return to the registration page. Otherwise, we redirect and inform the user the registration procedure is complete.
package net.javaguides.springboot.springsecurity.web;

import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
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;

import net.javaguides.springboot.springsecurity.model.User;
import net.javaguides.springboot.springsecurity.service.UserService;
import net.javaguides.springboot.springsecurity.web.dto.UserRegistrationDto;

@Controller
@RequestMapping("/registration")
public class UserRegistrationController {

    @Autowired
    private UserService userService;

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

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

    @PostMapping
    public String registerUserAccount(@ModelAttribute("user") @Valid UserRegistrationDto userDto,
        BindingResult result) {

        User existing = userService.findByEmail(userDto.getEmail());
        if (existing != null) {
            result.rejectValue("email", null, "There is already an account registered with that email");
        }

        if (result.hasErrors()) {
            return "registration";
        }

        userService.save(userDto);
        return "redirect:/registration?success";
    }
}

Spring Rest Controller - MainController.java

This is a simple controller for managing trivial thymeleaf pages.
package net.javaguides.springboot.springsecurity.web;

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

@Controller
public class MainController {

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

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

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

Data Transfer Object - UserRegistrationDto.java

We use the UserRegistrationDto to validate the user registration form. This DTO is annotated using Hibernate-Validation annotations which validate trivial fields on empty and our own custom @FieldMatch annotations which validates if the password is equal to the confirm password and the email address field is equal to the confirm email address field.
package net.javaguides.springboot.springsecurity.web.dto;

import javax.validation.constraints.AssertTrue;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotEmpty;

import net.javaguides.springboot.springsecurity.constraint.FieldMatch;

@FieldMatch.List({
    @FieldMatch(first = "password", second = "confirmPassword", message = "The password fields must match"),
    @FieldMatch(first = "email", second = "confirmEmail", message = "The email fields must match")
})
public class UserRegistrationDto {

    @NotEmpty
    private String firstName;

    @NotEmpty
    private String lastName;

    @NotEmpty
    private String password;

    @NotEmpty
    private String confirmPassword;

    @Email
    @NotEmpty
    private String email;

    @Email
    @NotEmpty
    private String confirmEmail;

    @AssertTrue
    private Boolean terms;

    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 getPassword() {
        return password;
    }

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

    public String getConfirmPassword() {
        return confirmPassword;
    }

    public void setConfirmPassword(String confirmPassword) {
        this.confirmPassword = confirmPassword;
    }

    public String getEmail() {
        return email;
    }

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

    public String getConfirmEmail() {
        return confirmEmail;
    }

    public void setConfirmEmail(String confirmEmail) {
        this.confirmEmail = confirmEmail;
    }

    public Boolean getTerms() {
        return terms;
    }

    public void setTerms(Boolean terms) {
        this.terms = terms;
    }
}

Creating Field Matching Validator

We created a special @FieldMatch annotation to support the validation process of comparing fields with each other if they match. We can input two fields first and second and an optional message.
package net.javaguides.springboot.springsecurity.constraint;

import javax.validation.Payload;
import javax.validation.Constraint;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

@Target({
    TYPE,
    ANNOTATION_TYPE
})
@Retention(RUNTIME)
@Constraint(validatedBy = FieldMatchValidator.class)
@Documented
public @interface FieldMatch {
    String message() default "{constraints.field-match}";
    Class << ? > [] groups() default {};
    Class << ? extends Payload > [] payload() default {};
    String first();
    String second();

    @Target({
        TYPE,
        ANNOTATION_TYPE
    })
    @Retention(RUNTIME)
    @Documented
    @interface List {
        FieldMatch[] value();
    }
}
Next, we create a custom validator by implementing the ConstraintValidator. Here we can validate if the given input fields match. If they do we return true if the fields don’t match we return false.
package net.javaguides.springboot.springsecurity.constraint;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.apache.commons.beanutils.BeanUtils;

public class FieldMatchValidator implements ConstraintValidator < FieldMatch, Object > {

    private String firstFieldName;
    private String secondFieldName;

    @Override
    public void initialize(final FieldMatch constraintAnnotation) {
        firstFieldName = constraintAnnotation.first();
        secondFieldName = constraintAnnotation.second();
    }

    @Override
    public boolean isValid(final Object value, final ConstraintValidatorContext context) {
        try {
            final Object firstObj = BeanUtils.getProperty(value, firstFieldName);
            final Object secondObj = BeanUtils.getProperty(value, secondFieldName);
            return firstObj == null && secondObj == null || firstObj != null && firstObj.equals(secondObj);
        } catch (final Exception ignore) {}
        return true;
    }
}

Service Layer - UserService.java

The UserService extends the UserDetailsService interface.
package net.javaguides.springboot.springsecurity.service;

import org.springframework.security.core.userdetails.UserDetailsService;

import net.javaguides.springboot.springsecurity.model.User;
import net.javaguides.springboot.springsecurity.web.dto.UserRegistrationDto;

public interface UserService extends UserDetailsService {

    User findByEmail(String email);

    User save(UserRegistrationDto registration);
}

Service Layer - UserServiceImpl.java

In the UserServiceImpl we implement the methods to lookup a user by email and to save the user registration using the UserRegistrationDto. Make sure when you save the user you’ll encode his password using the BCryptPasswordEncoder. Otherwise, a database administrator will be able to see his password in clear text.
package net.javaguides.springboot.springsecurity.service;

import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;

import org.springframework.beans.factory.annotation.Autowired;
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 net.javaguides.springboot.springsecurity.model.Role;
import net.javaguides.springboot.springsecurity.model.User;
import net.javaguides.springboot.springsecurity.repository.UserRepository;
import net.javaguides.springboot.springsecurity.web.dto.UserRegistrationDto;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    public User findByEmail(String email) {
        return userRepository.findByEmail(email);
    }

    public User save(UserRegistrationDto registration) {
        User user = new User();
        user.setFirstName(registration.getFirstName());
        user.setLastName(registration.getLastName());
        user.setEmail(registration.getEmail());
        user.setPassword(passwordEncoder.encode(registration.getPassword()));
        user.setRoles(Arrays.asList(new Role("ROLE_USER")));
        return userRepository.save(user);
    }

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(email);
        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());
    }
}

Spring Data Repository - UserRepository.java

We create the UserRepository by extending the JpaRepository interface. This is a Spring Data interface and gives us all the CRUD operations automatically.
package net.javaguides.springboot.springsecurity.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import net.javaguides.springboot.springsecurity.model.User;

@Repository
public interface UserRepository extends JpaRepository < User, Long > {
    User findByEmail(String email);
}

Create JPA Entities

PA entity - User.java

We have annotated our User and Role objects with Java Persistence API annotations. These annotations are used to map our pojo’s to the database.
package net.javaguides.springboot.springsecurity.model;

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

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

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    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;

    public User() {}

    public User(String firstName, String lastName, String email, String password) {
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
        this.password = password;
    }

    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;
    }

    @Override
    public String toString() {
        return "User{" +
            "id=" + id +
            ", firstName='" + firstName + '\'' +
            ", lastName='" + lastName + '\'' +
            ", email='" + email + '\'' +
            ", password='" + "*********" + '\'' +
            ", roles=" + roles +
            '}';
    }
}

JPA entity - Role.java

package net.javaguides.springboot.springsecurity.model;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Role {

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

    public Role() {}

    public Role(String name) {
        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;
    }

    @Override
    public String toString() {
        return "Role{" +
            "id=" + id +
            ", name='" + name + '\'' +
            '}';
    }
}

Spring Boot Application Running

We use spring boot to start our application.
package net.javaguides.springboot.springsecurity;

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);
    }
}

Thyemeleaf Templates

The thymeleaf pages are built with bootstrap and jquery. All templates are located in the src/main/resources/templates folder. Note that we have used webjars to manage client-side dependencies.

User Registration Page

In the user registration page, we have multiple input fields. For each input field, we have a corresponding error message. On the top of the form, we also have some global error messages.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

<link rel="stylesheet" type="text/css"
 th:href="@{/webjars/bootstrap/3.3.7/css/bootstrap.min.css}" />

<title>Registration</title>
</head>
<body>
 <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" href="#" th:href="@{/}">Registration and
     Login Module</a>
   </div>
  </div>
 </nav>

 <br>
 <br>

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

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

    <h1>Registration</h1>
    <form th:action="@{/registration}" th:object="${user}" method="post">

     <p class="error-message" th:if="${#fields.hasGlobalErrors()}"
      th:each="error : ${#fields.errors('global')}" th:text="${error}">Validation
      error</p>

     <div class="form-group"
      th:classappend="${#fields.hasErrors('firstName')}? 'has-error':''">
      <label for="firstName" class="control-label">First name</label> <input
       id="firstName" class="form-control" th:field="*{firstName}" />
      <p class="error-message"
       th:each="error: ${#fields.errors('firstName')}"
       th:text="${error}">Validation error</p>
     </div>
     <div class="form-group"
      th:classappend="${#fields.hasErrors('lastName')}? 'has-error':''">
      <label for="lastName" class="control-label">Last name</label> <input
       id="lastName" class="form-control" th:field="*{lastName}" />
      <p class="error-message"
       th:each="error : ${#fields.errors('lastName')}"
       th:text="${error}">Validation error</p>
     </div>
     <div class="form-group"
      th:classappend="${#fields.hasErrors('email')}? 'has-error':''">
      <label for="email" class="control-label">E-mail</label> <input
       id="email" class="form-control" th:field="*{email}" />
      <p class="error-message"
       th:each="error : ${#fields.errors('email')}" th:text="${error}">Validation
       error</p>
     </div>
     <div class="form-group"
      th:classappend="${#fields.hasErrors('confirmEmail')}? 'has-error':''">
      <label for="confirmEmail" class="control-label">Confirm
       e-mail</label> <input id="confirmEmail" class="form-control"
       th:field="*{confirmEmail}" />
      <p class="error-message"
       th:each="error : ${#fields.errors('confirmEmail')}"
       th:text="${error}">Validation error</p>
     </div>
     <div class="form-group"
      th:classappend="${#fields.hasErrors('password')}? 'has-error':''">
      <label for="password" class="control-label">Password</label> <input
       id="password" class="form-control" type="password"
       th:field="*{password}" />
      <p class="error-message"
       th:each="error : ${#fields.errors('password')}"
       th:text="${error}">Validation error</p>
     </div>
     <div class="form-group"
      th:classappend="${#fields.hasErrors('confirmPassword')}? 'has-error':''">
      <label for="confirmPassword" class="control-label">Confirm
       password</label> <input id="confirmPassword" class="form-control"
       type="password" th:field="*{confirmPassword}" />
      <p class="error-message"
       th:each="error : ${#fields.errors('confirmPassword')}"
       th:text="${error}">Validation error</p>
     </div>
     <div class="form-group"
      th:classappend="${#fields.hasErrors('terms')}? 'has-error':''">
      <input id="terms" type="checkbox" th:field="*{terms}" />   <label
       class="control-label" for="terms"> I agree with the <a
       href="#">terms and conditions</a> for Registration.
      </label>
      <p class="error-message"
       th:each="error : ${#fields.errors('terms')}" th:text="${error}">Validation
       error</p>
     </div>
     <div class="form-group">
      <button type="submit" class="btn btn-success">Register</button>
      <span>Already registered? <a href="/" th:href="@{/login}">Login
        here</a></span>
     </div>

    </form>
   </div>
  </div>
 </div>

 <script type="text/javascript"
  th:src="@{/webjars/jquery/3.2.1/jquery.min.js/}"></script>
 <script type="text/javascript"
  th:src="@{/webjars/bootstrap/3.3.7/js/bootstrap.min.js}"></script>
</body>
</html>

Index Page

The index page can be accessed via http://localhost:8080/ after a successful login.
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
 xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

<link rel="stylesheet" type="text/css"
 th:href="@{/webjars/bootstrap/3.3.7/css/bootstrap.min.css}" />

<title>Registration</title>
</head>
<body>
 <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" href="#" th:href="@{/}">Registration and
     Login Module</a>
   </div>
   <div id="navbar" class="collapse navbar-collapse">
    <ul class="nav navbar-nav">
     <li sec:authorize="isAuthenticated()"><a th:href="@{/logout}">Logout</a></li>
    </ul>
   </div>
  </div>
 </nav>

 <br>
 <br>
 <div class="container">
  <h2>User Registration and Login Module using Spring Boot, Spring
   MVC, Spring Security, JPA/Hibernate and Thymeleaf</h2>

  <p>
   Welcome <span sec:authentication="principal.username">User</span>
  </p>
 </div>

 <script type="text/javascript"
  th:src="@{/webjars/jquery/3.2.1/jquery.min.js/}"></script>
 <script type="text/javascript"
  th:src="@{/webjars/bootstrap/3.3.7/js/bootstrap.min.js}"></script>

</body>
</html>

Login Page

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

<link rel="stylesheet" type="text/css"
 th:href="@{/webjars/bootstrap/3.3.7/css/bootstrap.min.css}" />

<title>Registration</title>
</head>
<body>

 <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" href="#" th:href="@{/}">Registration and
     Login Module</a>
   </div>
  </div>
 </nav>

 <br>
 <br>

 <div class="container">

  <div class="row">
   <div class="col-md-6 col-md-offset-3">
    <h1>Login page</h1>
    <form th:action="@{/login}" method="post">
     <div th:if="${param.error}">
      <div class="alert alert-danger">Invalid username or
       password.</div>
     </div>
     <div th:if="${param.logout}">
      <div class="alert alert-info">You have been logged out.</div>
     </div>
     <div class="form-group">
      <label for="username">Username</label>: <input type="text"
       id="username" name="username" class="form-control"
       autofocus="autofocus" placeholder="Username" />
     </div>
     <div class="form-group">
      <label for="password">Password</label>: <input type="password"
       id="password" name="password" class="form-control"
       placeholder="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>
     <div class="form-group">
      <span>New user? <a href="/" th:href="@{/registration}">Register
        here</a></span>
     </div>
    </form>
   </div>
  </div>


 </div>

 <script type="text/javascript"
  th:src="@{/webjars/jquery/3.2.1/jquery.min.js/}"></script>
 <script type="text/javascript"
  th:src="@{/webjars/bootstrap/3.3.7/js/bootstrap.min.js}"></script>

</body>
</html>

Demo

Registration page demo

Access http://localhost:8080/registration and fill in some invalid fields.

After successful registration, the user is redirected to http://localhost:8080/registration?success

Login page demo

You can go to the login page at http://localhost:8080/login and log in with the registered user.
After successfully, login will navigate below page.

Reference

Learn complete Spring boot on Spring Boot Tutorial
The source code of this article available on my GitHub repository registration-login-springboot-security-thymeleaf 

Comments