Registration and Login with Kotlin + Spring Security + Thymeleaf

Hello everyone, I hope you all are well, today we will learn how to create user registration and login using Kotlin, Spring security, Thymeleaf, JPA, and H2DB.GitHub repository link is provided at the end of this tutorial. You can download the source code.

Technologies used:

  • Spring Boot makes developing the web applications and microservices with Spring Framework faster and easier through three core capabilities: 1.  Autoconfiguration 2. An opinionated approach to configuration 3. The ability to create standalone applications
  • Kotlin is an open-source statically typed programming language. It is object-oriented and supports functional programming features. It is designed for the JVM (Java Virtual Machine)
  • Spring Security is a Java/Java EE framework that provides authentication, authorization, and other security features for enterprise applications.
  • Thymeleaf is a modern server-side Java template engine for both web and standalone environments.
  • Maven is a build automation tool used primarily for Java projects. Maven can also be used to build and manage projects written in C#, Ruby, Scala, and other languages.

User Interface

User Registration


User Login



Authentication success page

Project Structure:



Maven[pom.xml] 

A Project Object Model or POM is the fundamental unit of work in Maven. It is an XML file that contains information about the project and configuration details utilized by Maven to build the project. It contains default values for most projects. Some of the configurations that can be designated in the POM is the project dependencies, the plugins or goals that can be executed, the build profiles, and so on. Other information such as the project version, description, developers, mailing lists, and such can withal be designated.

<?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>2.5.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.knf.dev.demo</groupId>
<artifactId>kotlin-spring-thymeleaf-login-signup</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>kotlin-spring-thymeleaf-login-signup</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>11</java.version>
<kotlin.version>1.5.31</kotlin.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-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-kotlin</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</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>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<configuration>
<args>
<arg>-Xjsr305=strict</arg>
</args>
<compilerPlugins>
<plugin>spring</plugin>
<plugin>jpa</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-allopen</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-noarg</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>

</project>


Create Entity Role

package com.knf.dev.demo.kotlinspringthymeleafloginsignup.model

import javax.persistence.*

@Entity
@Table(name = "role")
class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null
var name: String? = null

constructor() {}
constructor(name: String?) : super() {
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 Entity User

package com.knf.dev.demo.kotlinspringthymeleafloginsignup.model

import javax.persistence.*

@Entity
@Table(name = "user", uniqueConstraints = [UniqueConstraint(columnNames = ["email"])])
class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null

@Column(name = "first_name")
var firstName: String? = null

@Column(name = "last_name")
var lastName: String? = null
var email: String? = null
var password: String? = null

@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")]
)
var roles: Collection<Role>? = null

constructor() {}
constructor(
firstName: String?, lastName: String?, email: String?,
password: String?, roles: Collection<Role>?
) : super() {
this.firstName = firstName
this.lastName = lastName
this.email = email
this.password = password
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. 


Repository Layer[UserRepository.kt]

package com.knf.dev.demo.kotlinspringthymeleafloginsignup.repository

import com.knf.dev.demo.kotlinspringthymeleafloginsignup.model.User
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository

@Repository
interface UserRepository : JpaRepository<User?, Long?> {
fun findByEmail(email: String?): User?
}
Spring @Repository annotation is used to indicate that the class provides the mechanism for storage, retrieval, search, update and delete operation on objects. JpaRepository is a JPA specific extension of Repository. It contains API for basic CRUD operations and also API for pagination and sorting.


Service layer[UserService]

package com.knf.dev.demo.kotlinspringthymeleafloginsignup.service

import org.springframework.security.core.userdetails.UserDetailsService
import com.knf.dev.demo.kotlinspringthymeleafloginsignup.dto.UserRegistrationDto
import com.knf.dev.demo.kotlinspringthymeleafloginsignup.model.User

interface UserService : UserDetailsService {
fun save(registrationDto: UserRegistrationDto?): User?
val all: List<User?>?
}
The UserDetailsService interface is used to retrieve user-related data. It has one method named loadUserByUsername().


Implementation[UserServiceImpl.kt]

package com.knf.dev.demo.kotlinspringthymeleafloginsignup.service

import com.knf.dev.demo.kotlinspringthymeleafloginsignup.dto.UserRegistrationDto
import com.knf.dev.demo.kotlinspringthymeleafloginsignup.model.Role
import com.knf.dev.demo.kotlinspringthymeleafloginsignup.model.User
import com.knf.dev.demo.kotlinspringthymeleafloginsignup.repository.UserRepository
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 java.util.*
import java.util.function.Function
import java.util.stream.Collectors


@Service
class UserServiceImpl(private val userRepository: UserRepository) : UserService {
@Autowired
private val passwordEncoder: BCryptPasswordEncoder? = null
override fun save(registrationDto: UserRegistrationDto?): User? {
val user = User(
registrationDto!!.firstName,
registrationDto.lastName, registrationDto.email,
passwordEncoder!!.encode(registrationDto.password),
Arrays.asList(Role("ROLE_USER"))
)
return userRepository.save(user)
}

@Throws(UsernameNotFoundException::class)
override fun loadUserByUsername(username: String): UserDetails {
val user = userRepository.findByEmail(username)
?: throw UsernameNotFoundException("Invalid username or password.")
return org.springframework.security.core.userdetails.User(
user.email, user.password,
mapRolesToAuthorities(user.roles!!)
)
}

private fun mapRolesToAuthorities(roles: Collection<Role>): Collection<GrantedAuthority?> {
return roles.stream().map(Function<Role, SimpleGrantedAuthority?> { role: Role ->
SimpleGrantedAuthority(
role.name
)
}).collect(Collectors.toList())
}

override val all: List<User?>?
get() = userRepository.findAll()

}


Data Transfer Object[UserRegistrationDto.kt]

package com.knf.dev.demo.kotlinspringthymeleafloginsignup.dto

class UserRegistrationDto {
var firstName: String? = null
var lastName: String? = null
var email: String? = null
var password: String? = null

constructor() {}
constructor(
firstName: String?,
lastName: String?, email: String?, password: String?
) : super() {
this.firstName = firstName
this.lastName = lastName
this.email = email
this.password = password
}
}


Security Configuration[SecurityConfiguration.kt]

package com.knf.dev.demo.kotlinspringthymeleafloginsignup.config

import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
import org.springframework.beans.factory.annotation.Autowired
import com.knf.dev.demo.kotlinspringthymeleafloginsignup.service.UserService
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
import org.springframework.security.authentication.dao.DaoAuthenticationProvider
import kotlin.Throws
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
import java.lang.Exception

@Configuration
@EnableWebSecurity
class SecurityConfiguration : WebSecurityConfigurerAdapter() {
@Autowired
private val userService: UserService? = null
@Bean
fun passwordEncoder(): BCryptPasswordEncoder {
return BCryptPasswordEncoder()
}

@Bean
fun authenticationProvider(): DaoAuthenticationProvider {
val auth = DaoAuthenticationProvider()
auth.setUserDetailsService(userService)
auth.setPasswordEncoder(passwordEncoder())
return auth
}

@Throws(Exception::class)
override fun configure(auth: AuthenticationManagerBuilder) {
auth.authenticationProvider(authenticationProvider())
}

@Throws(Exception::class)
override fun configure(http: HttpSecurity) {
http.authorizeRequests().antMatchers(
"/registration**", "/js/**",
"/css/**", "/img/**"
).permitAll().anyRequest()
.authenticated().and().formLogin().loginPage("/login").permitAll().and().logout()
.invalidateHttpSession(true).clearAuthentication(true)
.logoutRequestMatcher(AntPathRequestMatcher("/logout")).logoutSuccessUrl("/login?logout")
.permitAll()
}
}


Controller[HomeController.kt]

package com.knf.dev.demo.kotlinspringthymeleafloginsignup.controller

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

@Controller
class HomeController {
@GetMapping("/login")
fun login(): String {
return "login"
}

@GetMapping("/")
fun home(): String {
return "index"
}
}


Controller[RegistrationController.kt]

package com.knf.dev.demo.kotlinspringthymeleafloginsignup.controller

import org.springframework.web.bind.annotation.RequestMapping
import com.knf.dev.demo.kotlinspringthymeleafloginsignup.service.UserService
import org.springframework.web.bind.annotation.ModelAttribute
import com.knf.dev.demo.kotlinspringthymeleafloginsignup.dto.UserRegistrationDto
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping

@Controller
@RequestMapping("/registration")
class RegistrationController(private val userService: UserService) {
@ModelAttribute("user")
fun userRegistrationDto(): UserRegistrationDto {
return UserRegistrationDto()
}

@GetMapping
fun showRegistrationForm(): String {
return "registration"
}

@PostMapping
fun registerUserAccount(@ModelAttribute("user") registrationDto: UserRegistrationDto?): String {
userService.save(registrationDto)
return "redirect:/registration?success"
}
}


Spring Boot Main Driver

package com.knf.dev.demo.kotlinspringthymeleafloginsignup

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

@SpringBootApplication
class KotlinSpringThymeleafLoginSignupApplication {
companion object {
@JvmStatic
fun main(args: Array<String>) {
SpringApplication.run(KotlinSpringThymeleafLoginSignupApplication::class.java, *args)
}
}
}


View Layer

index.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.
org/thymeleaf-extras-springsecurity3">

<head>
<meta charset="ISO-8859-1">
<title>Spring Security Demo</title>

<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">

</head>
<style>
.navbar {
background-color: green;
}
</style>

<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" href="#" th:href="@{/}">
Registration and
Login</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><br><br>
<div class="container">
<div class="alert alert-success">
<strong>Success!</strong> Login Successful.
</div>
</div>
</body>
</html>


login.html

<!DOCTYPE html>
<html>

<head>
<meta charset="ISO-8859-1">
<title>Spring Security Demo</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">

</head>
<style>
.navbar {
background-color: green;
}
</style>

<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" href="#" th:href="@{/}">
Registration and
Login</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"> Username </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 Demo</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">

</head>
<style>
.navbar {
background-color: green;
}
</style>

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

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


Demo 

Start the Spring Boot web app: mvn spring-boot:run



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