Spring Boot User Login and Register Example

In this tutorial, you will learn how to create Login and Register functionality in spring boot. You can use this tutorial with different databases like PostgreSQL or MySQL and build tools like gradle or maven. This tutorial is implemented on Maven, PostgreSQL, SecurityFilterChain and Standard Folder Structure. You will find how to use User Role and how to restrict users from accessing pages based on role associated to account. This is a complete tutorial from login / signup to home and dashboard with email login. So let’s get start…

Libraries / Dependencies

  • Spring Web: Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container.
  • Thymeleaf: modern server-side Java template engine. Allow HTML files to correctly displayed in browsers and as static prototypes.
  • Spring Data JPA: Persist data in SQL stores with Java Persistence API using Spring Data and Hibernate.
  • PostgreSQL Driver: A JDBC and R2DBC driver that allows Java programs to connect to a PostgreSQL database using standard, database independent Java code.
  • Spring Security: Highly customizable authentication and access-control framework for Spring applications.
  • Validation: Bean Validation with Hibernate validator.

Download the starter project with all dependencies from here.

Note: Please add the database configuration in you application.properties. Find a example here.

Folder Structure

Bellow is the folder structure used for developing the login register project.

src
    └── main
        ├── java
        │   └── com
        │       └── example
        │           └── demo
        │               ├── LoginRegisterApplication.java
        │               ├── config
        │               │   ├── CustomLoginSucessHandler.java
        │               │   ├── WebMvcConfig.java
        │               │   └── WebSecurityConfig.java
        │               ├── controller
        │               │   ├── AdminController.java
        │               │   ├── AuthController.java
        │               │   └── UserController.java
        │               ├── model
        │               │   ├── Role.java
        │               │   └── User.java
        │               ├── repository
        │               │   └── UserRepository.java
        │               └── service
        │                   ├── UserServiceImpl.java
        │                   └── UserService.java
        └── resources
            ├── application.properties
            ├── static
            └── templates
                ├── about-us.html
                ├── access-denied.html
                ├── homepage.html
                ├── admin
                │   └── dashboard.html
                ├── auth
                │   ├── login.html
                │   └── register.html
                └── user
                └── dashboard.html

User and Role Model

Lets create a User.java which implements UserDetails.

package com.example.demo.model;

import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import org.hibernate.validator.constraints.Length;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.Collections;


@Entity
@Table(name = "users")
public class User implements UserDetails  {
    @SequenceGenerator(
            name = "users_sequence",
            sequenceName = "users_sequence",
            allocationSize = 1
    )
    @Id
    @GeneratedValue(
            strategy = GenerationType.SEQUENCE,
            generator = "users_sequence"
    )
    private int id;

    @NotNull(message = "First Name cannot be empty")
    @Column(name = "first_name")
    private String firstName;

    @NotNull(message = "Last Name cannot be empty")
    @Column(name = "last_name")
    private String lastName;

    @NotNull(message = "Email cannot be empty")
    @Email(message = "Please enter a valid email address")
    @Column(name = "email", unique = true)
    private String email;

    @NotNull(message = "Password cannot be empty")
    @Length(min = 7, message = "Password should be atleast 7 characters long")
    @Column(name = "password")
    private String password;

    @Column(name = "mobile", unique = true)
    @Length(min = 10, message = "Password should be atleast 10 number long")
    private String mobile;

    @CreationTimestamp
    @Column(name = "created_at", updatable = false)
    private LocalDateTime createdAt;

    @UpdateTimestamp
    @Column(name = "updated_at")
    private LocalDateTime updatedAt;

    @Enumerated(EnumType.STRING)
    private Role role;

    @Column(name = "locked")
    private Boolean locked = false;

    @Column(name = "enabled")
    private Boolean enabled = true;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.name());
        return Collections.singletonList(authority);
    }

    @Override
    public String getPassword() {
        return password;
    }

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

    @Override
    public String getUsername() {
        return email;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return !locked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return enabled;
    }

    public Role getRole() { return role; }

    public void setRole(com.example.demo.model.Role role) {
        this.role = role;
    }

    public String getEmail() { return email; }

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

    public String getFirstName() { return firstName; }

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

    public String getMobile() { return mobile; }

    public void setMobile(String mobile) { this.mobile = mobile; }

    public String getLastName() { return lastName; }

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

Now create Role.java Interface

package com.example.demo.model;

public enum Role {
    USER("User"),
    ADMIN("Admin");

    private final String value;

    private Role(String value) {
        this.value = value;
    }

    public String getValue() {
        return value;
    }
}

User Repository Interface

Create UserRepository.java interface which extends JpaRepository<User, Long>. In this, we create findByEmail() and findByMobile().

package com.example.demo.repository;

import com.example.demo.model.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

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

User Services Interface with Implementation

Create UserService.java interface to save user and check user existance.

package com.example.demo.service;

import com.example.demo.model.User;

import java.util.List;

public interface UserService {
    public void saveUser(User user);
    public List<Object> isUserPresent(User user);
}

Now create UserServiceImpl.java which implements UserService.java and UserDetailsService interface. In this we also override loadUserByUsername() method

package com.example.demo.service;

import com.example.demo.model.User;
import com.example.demo.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

@Service
public class UserServiceImpl implements UserService, UserDetailsService {

    @Autowired
    BCryptPasswordEncoder bCryptPasswordEncoder;

    @Autowired
    UserRepository userRepository;

    @Override
    public void saveUser(User user) {
        String encodedPassword = bCryptPasswordEncoder.encode(user.getPassword());
        user.setPassword(encodedPassword);
//        user.setRole(Role.USER);
        userRepository.save(user);
    }

    @Override
    public List<Object> isUserPresent(User user) {
        boolean userExists = false;
        String message = null;
        Optional<User> existingUserEmail = userRepository.findByEmail(user.getEmail());
        if(existingUserEmail.isPresent()){
            userExists = true;
            message = "Email Already Present!";
        }
        Optional<User> existingUserMobile = userRepository.findByMobile(user.getMobile());
        if(existingUserMobile.isPresent()){
            userExists = true;
            message = "Mobile Number Already Present!";
        }
        if (existingUserEmail.isPresent() && existingUserMobile.isPresent()) {
            message = "Email and Mobile Number Both Already Present!";
        }
        System.out.println("existingUserEmail.isPresent() - "+existingUserEmail.isPresent()+"existingUserMobile.isPresent() - "+existingUserMobile.isPresent());
        return Arrays.asList(userExists, message);
    }

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        return userRepository.findByEmail(email).orElseThrow(
                ()-> new UsernameNotFoundException(
                        String.format("USER_NOT_FOUND", email)
                ));
    }
}

Configuration for Web Security, MVC and Custom Login Success Handler

Create WebSecurityConfig.java which EnableWebSecurity Configuration. In this, we implemented SecurityFilterChain which eleminates WebSecurityConfigurerAdapter Deprecated warning. All routes permission can be given here with authorize restriction.

package com.example.demo.config;

import com.example.demo.service.UserServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Autowired
    private CustomLoginSucessHandler sucessHandler;

    @Bean
    public UserDetailsService userDetailsService() {
        return new UserServiceImpl();
    }

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

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
        return authConfig.getAuthenticationManager();
    }

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();

        authProvider.setUserDetailsService(userDetailsService());
        authProvider.setPasswordEncoder(passwordEncoder());

        return authProvider;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {

                http.authorizeRequests()
                // URL matching for accessibility
                .antMatchers("/", "/login", "/register").permitAll()
                .antMatchers("/admin/**").hasAnyAuthority("ADMIN")
                .antMatchers("/account/**").hasAnyAuthority("USER")
                .anyRequest().authenticated()
                .and()
                // form login
                .csrf().disable().formLogin()
                .loginPage("/login")
                .failureUrl("/login?error=true")
                .successHandler(sucessHandler)
                .usernameParameter("email")
                .passwordParameter("password")
                .and()
                // logout
                .logout()
                .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                .logoutSuccessUrl("/")
                .and()
                .exceptionHandling()
                .accessDeniedPage("/access-denied");

                http.authenticationProvider(authenticationProvider());
                http.headers().frameOptions().sameOrigin();

                return http.build();
    }

    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().antMatchers("/images/**", "/js/**", "/webjars/**");
    }

}

Create CustomLoginSucessHandler.java for login success url based on Role.

package com.example.demo.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

@Configuration
public class CustomLoginSucessHandler extends SimpleUrlAuthenticationSuccessHandler {
    @Override
    protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
    throws IOException {
        String targetUrl = determineTargetUrl(authentication);
        if(response.isCommitted()) return;
        RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
        redirectStrategy.sendRedirect(request, response, targetUrl);
    }

    protected String determineTargetUrl(Authentication authentication){
        String url = "/login?error=true";
        Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
        List<String> roles = new ArrayList<String>();
        for(GrantedAuthority a : authorities){
            roles.add(a.getAuthority());
        }
        if(roles.contains("ADMIN")){
            url = "/admin/dashboard";
        }else if(roles.contains("USER")) {
            url = "/dashboard";
        }
        return url;
    }
}

Create WebMvcConfig.java for adding views.

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("/access-denied").setViewName("access-denied");
        registry.addViewController("/").setViewName("homepage");
        registry.addViewController("/about-us").setViewName("about-us");
    }
}

In above WebMvcConfig, we create 3 view, so lets create templates for it – homepage.html, about-us.html and access-denied.html.

<!--homepage.html-->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Homepage</title>
</head>
<body>
  <h2>Welcome to Homepage</h2>
  <p sec:authorize="hasRole('ROLE_ANONYMOUS')">Text visible to anonymous.</p>
  <p sec:authorize="hasRole('USER')">Text visible to user.</p>
  <p sec:authorize="hasRole('ADMIN')">Text visible to admin.</p>
  <p sec:authorize="isAuthenticated()">Text visible only to authenticated users.</p>

  <div sec:authorize="hasRole('ROLE_ANONYMOUS')">
    <p><a th:href="@{|/login|}" th:text="'Log in'"></a></p>
    <p><a th:href="@{|/register|}" th:text="'Register'"></a></p>
  </div>

  <div sec:authorize="isAuthenticated()">
    <p>Logged as: <span sec:authentication="name"></span></p>
    <p>Has role: <span sec:authentication="authorities"></span></p>
    <p sec:authorize="hasAuthority('USER')"><a th:href="@{|/dashboard|}" th:text="'User Dashboard'"></a></p>
    <p sec:authorize="hasAuthority('ADMIN')"><a th:href="@{|/admin/dashboard|}" th:text="'Admin Dashboard'"></a></p>
    <a th:href="@{/logout}">Log out</a>
  </div>

</body>
</html>
<!--about-us.html-->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>About Us</title>
</head>
<body>
    <h2>About Us</h2>
</body>
</html>
<!--access-denied.html-->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Access Denied</title>
</head>
<body>
    <h2>Access Denied</h2>
</body>
</html>

Admin, User and Auth Controller with HTML

Now lets create AuthController.java where will add login and register request mapping. In this we have create 3 request mapping where 2 are get method and 1 is post method.

package com.example.demo.controller;

import com.example.demo.model.User;
import com.example.demo.service.UserService;
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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.validation.Valid;
import java.util.List;

@Controller
public class AuthController {
    @Autowired
    UserService userService;

    @RequestMapping(value = {"/login"}, method = RequestMethod.GET)
    public String login(){
        return "auth/login";
    }

    @RequestMapping(value = {"/register"}, method = RequestMethod.GET)
    public String register(Model model){
        model.addAttribute("user", new User());
        return "auth/register";
    }

    @RequestMapping(value = {"/register"}, method = RequestMethod.POST)
    public String registerUser(Model model, @Valid User user, BindingResult bindingResult){
        if(bindingResult.hasErrors()){
            model.addAttribute("successMessage", "User registered successfully!");
            model.addAttribute("bindingResult", bindingResult);
            return "auth/register";
        }
        List<Object> userPresentObj = userService.isUserPresent(user);
        if((Boolean) userPresentObj.get(0)){
            model.addAttribute("successMessage", userPresentObj.get(1));
            return "auth/register";
        }

        userService.saveUser(user);
        model.addAttribute("successMessage", "User registered successfully!");

        return "auth/login";
    }
}

In AuthController.java we have used 2 templates login.html and register.html

<!--login.html-->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Login</title>
</head>
<body>
    <form th:action="@{/login}" method="POST" class="form-signin">
        <h3 th:text="Login"></h3>
        <input type="text" name="email" th:placeholder="Email" /> <br /><br />
        <input type="password" th:placeholder="Password" name="password" /> <br /><br />
        <button type="Submit" th:text="Login"></button> | <a th:href="@{|/register|}" th:text="'Register'"></a><br />
        <div th:if="${param.error}"><p>Email or Password is invalid.</p></div> <br />
    </form>
</body>
</html>
<!--register.html-->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title>Login</title>
</head>
<body>
  <form th:action="@{/register}" th:object="${user}" method="post">
    <h3>Registration</h3>
    <input type="text" th:field="*{firstName}" placeholder="First Name" /> <br/><br/>
    <input type="text" th:field="*{lastName}" placeholder="Last Name" /> <br/><br/>
    <input type="text" th:field="*{mobile}" placeholder="Mobile Number" /> <br/><br/>
    <input type="text" th:field="*{email}" placeholder="Email" /> <br/><br/>
    <input type="password" th:field="*{password}" placeholder="Password" /> <br/><br/>
    <select th:field="*{role}" required>
      <option value="">Select</option>
      <option value="ADMIN">ADMIN</option>
      <option value="USER">USER</option>
    </select> <br/><br/>
    <button type="submit">Register</button> | <a th:href="@{|/login|}" th:text="'Log in'"></a>
    <span th:utext="${successMessage}"></span>
    <div th:if="${bindingResult!=null && bindingResult.getAllErrors()!=null}">
      <ul th:each="data : ${bindingResult.getAllErrors()}">
        <li th:text="${data.getObjectName() + ' :: ' + data.getDefaultMessage()}"></li>
      </ul>
    </div>
</form>
</body>
</html>

Lets create now AdminController.java for creating page only view for admin users.

package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class AdminController {

    @RequestMapping(value = {"/admin/dashboard"}, method = RequestMethod.GET)
    public String adminHome(){
        return "admin/dashboard";
    }
}

In AdminController, admin/dashboard.html template is returned

<!--admin/dashboard.html-->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title>User Dashboard</title>
</head>
<body>
  <h2>Welcome to Admin Dashboard</h2>
  <p>Logged as: <span sec:authentication="name"></span></p>
  <p>Has role: <span sec:authentication="authorities"></span></p>
  <p><a th:href="@{/}">Home</a></p>
  <p><a th:href="@{/logout}">Log out</a></p>
</body>
</html>

Now lets create last controller, UserController.java which only give User role views.

package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class UserController {

    @RequestMapping(value = {"/dashboard"}, method = RequestMethod.GET)
    public String homePage(){
        return "user/dashboard";
    }
}

In UserController, user/dashboard.html template is returned

<!--user/dashboard.html-->
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
  <title>User Dashboard</title>
</head>
<body>
  <h2>Welcome to User Dashboard</h2>
  <p>Logged as: <span sec:authentication="name"></span></p>
  <p>Has role: <span sec:authentication="authorities"></span></p>
  <div>
    <p><a th:href="@{/}">Home</a></p>
    <p><a th:href="@{/logout}">Log out</a></p>
  </div>
</body>
</html>

Done! Now you can run the project, you can successfully add users, login and view pages based on role permissions.

Download User Login Register in Spring Boot

Find this tutorial on GitHub.