Spring security is a framework that you should use to secure Spring applications. This is because it is the de facto standard for authorization and authentication.

If you want to customize the security, you can extend from it to define your own implementation.

In this tutorial, you will learn how to secure an application using Spring Security. You will use the form-based approach to authenticate users.

Prerequisites

Project setup

To create a Spring Boot project, go to spring initialzr and select Maven in the Project section. In the language section, select Java and in the Spring Boot section, select 3.x.x where x is the current version.

In the Project Metadata section, enter the respective sections as shown below.

  • Group – com.javawhizz
  • Artifact – SpringSecurity
  • Package name – com.javawhizz.SpringSecurity
  • Packaging – Jar
  • Java – 17

Press the ADD DEPENDENCIES button and add the following dependencies.

  • Spring Web – create web applications using the MVC pattern.
  • Spring Security – authorization and authentication framework for spring applications.
  • Lombok – uses annotations to reduce boilerplate code by generating helper methods.
  • Thymeleaf – A templating engine for the Java programming language.
  • Spring Data JPA – An ORM tool to map Java classes to entities and vice versa.
  • PostgreSQL Driver – A library to connect to the PostgreSQL database using Java code.

The project structure of your project should be as shown in the following image.

spring security project structure

The GENERATE button to download a ZIP file containing your project. Unzip the file and import the project folder into IntelliJ.

Add database connection properties

Spring Boot creates a properties file by default to add connection details. It is better to use the YAML file since it is easy to use and also removes boilerplate properties.

To add database connection details, rename the properties file to application.yml. Copy and paste the following properties into the file.

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/spring_security_db
    username: username
    password: password
  jpa:
    hibernate:
      ddl-auto: create
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
        format_sql: true
    show-sql: true

Ensure that you have created a database named spring_security_db. This is the database name specified as the data source URL. Additionally, replace the username and password to match your database.

Create a customer model

Create a named customer in src/main/java/com/javawhizz/SpringSecurity. In the customer package, create a file named Customer.java. Copy and paste the following code into the file.

package com.javawhizz.SpringSecurity.customer;

import com.javawhizz.SpringSecurity.security.UserAccount;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(name = "customer")
public class Customer {
    @Id
    @GeneratedValue
    private Long customerId;

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

    @Column(name = "last_name")
    private String lastName;

    @Column(name = "email")
    private String email;

    @Transient
    private String password;

    @OneToOne(
            mappedBy = "customer",
            cascade = CascadeType.ALL,
            fetch = FetchType.LAZY
    )
    private UserAccount userAccount;
}

This class creates an entity for a customer having the fields customer id, first name, last name, and email. Note that the password is not persisted in the database.

The @Transient annotation ensures that the password field is not persisted. You will use the customer’s email as the username but you can define your own username.

The one-to-one relation helps to map the customer to their user account.

Create a customer repository

Create a file named CustomerRepository.java in the customer package. Copy and paste the following code into the file.

package com.javawhizz.SpringSecurity.customer;

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

@Repository
public interface CustomerRepository extends JpaRepository<Customer, Long> {

}

You will use the customer repository in this class to persist customers in the database. Ensure that you passed the entity type in the first generic argument and the ID type in the second argument.

Create a customer service

Create a file named CustomerService.java in the customer package. Copy and paste the following code into the file.

package com.javawhizz.SpringSecurity.customer;

public interface CustomerService {
    Customer createCustomer(Customer customer);

}

The customer service interface defines one method to create a customer. Create a file named CustomerServiceImpl.java in the customer package. Copy and paste the following code into the file.

package com.javawhizz.SpringSecurity.customer;

import com.javawhizz.SpringSecurity.security.UserAccountService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CustomerServiceImpl implements CustomerService{
    private final CustomerRepository customerRepository;
    private final UserAccountService userAccountService;
    @Override
    public Customer createCustomer(Customer customer) {
        Customer theCustomer = customerRepository.save(customer);
        userAccountService.createUserAccount(theCustomer);
        return theCustomer;
    }
}

This class inherits from customer service to override the createCustomer() method. The method then creates a customer and uses the new record to create a user account.

You will write the code for the createUserAccount() method later in the tutorial. Don’t worry about it now.

Create a customer controller

Create a file named CustomerController.java in the customer package. Copy and paste the following code into the file.

package com.javawhizz.SpringSecurity.customer;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("api/v1/customers")
@RequiredArgsConstructor
public class CustomerController {
    private final CustomerService customerService;
    @PostMapping("/register")
    public String createCustomer(@ModelAttribute("customer") 
                                     Customer customer) {
        customerService.createCustomer(customer);
        return "redirect:/profile";
    }

}

The @RequestMapping annotation sets the base URI as api/v1/customers.

To create a new customer, you should annotate the methods to create data with @PostMapping. The annotation also sets the path for post-mapping to /register.

As a result, any request to api/v1/customers/register should be a post request to create a customer. To bind the customer data to a customer object, add @ModelAttribute to the method.

After creating a customer, the method redirects to /profile. You will create the method to handle this redirect later in the tutorial.

Create a role for a user

Create a package named auth in src/main/java/com/javawhizz/SpringSecurity. Create a file named Role.java in the auth package and copy and paste the following code into the file.

package com.javawhizz.SpringSecurity.auth;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.javawhizz.SpringSecurity.security.UserAccount;
import jakarta.persistence.*;
import lombok.*;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

@Entity
@Getter
@Setter
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "role")
public class Role {
    @Id
    @GeneratedValue
    @Column(name = "role_id")
    private Long roleId;
    @Column(name = "role_name")
    private String roleName;
    @OneToMany(
            fetch = FetchType.EAGER,
            mappedBy = "role",
            cascade = {
                    CascadeType.PERSIST,
                    CascadeType.MERGE,
                    CascadeType.REFRESH,
                    CascadeType.DETACH
            }
    )
    private Set<Authority> authorities = new HashSet<>();

    @ManyToOne(
            fetch = FetchType.LAZY
    )
    @JoinColumn(name = "userAccountId")
    @JsonIgnore
    private UserAccount userAccount;

    public Role(String roleName) {
        this.roleName = roleName;
    }

    public Role addAuthorities(Set<Authority> authorities){
        for (Authority authority : authorities) {
            if (authority != null){
                this.authorities.add(authority);
                authority.setRole(this);
            }
        }
        return this;
    }

    public Set<SimpleGrantedAuthority> getGrantedAuthorities(){
        Set<SimpleGrantedAuthority> grantedAuthorities = this.authorities
                .stream()
                .map(authority -> 
                        new SimpleGrantedAuthority(authority.getAuthorityName()))
                .collect(Collectors.toSet());
        
        grantedAuthorities.add(
                new SimpleGrantedAuthority("ROLE_"+this.roleName));
        return grantedAuthorities;
    }
}

Spring Security will use the class defined in this class to identify a user. The role name identifies a particular user of the application.

The class also has a one-to-many relationship with the authority class. This means that a role can have many authorities.

You will create the authority class in the next section. The class also has a many-to-one relationship with the user account class. You will create this class later in the tutorial, don’t worry about it.

The Role() and addAuthorities() methods will help you to create a role and add authorities.

Create authority for a user

Create a file named Authority.java under the auth package. Copy and paste the following code into the file.

package com.javawhizz.SpringSecurity.auth;

import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.ToString;

@Getter
@Setter
@ToString
@Entity
@NoArgsConstructor
@Table(name = "authority")
public class Authority {
    @Id
    @GeneratedValue
    @Column(name = "authority_id")
    private Long authorityId;
    @Column(name = "authority_name")
    private String authorityName;
    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "roleId")
    @JsonIgnore
    private Role role;

    public Authority(String authorityName) {
        this.authorityName = authorityName;
    }
}

The authority name validates whether a user can access a particular resource. To add authority to a role, you should create a method Authority() that expects a role name parameter.

Create a role repository

Create a file named RoleRepository.java in the auth package. Copy and paste the following code into the file.

package com.javawhizz.SpringSecurity.auth;

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

import java.util.Optional;
@Repository
public interface RoleRepository extends
        JpaRepository<Role, Long> {
    Optional<Role> findRoleByRoleName(String roleName);
}

The role repository will execute queries for the role entity. To achieve this, you should ensure that the interface extends the JPA repository. Additionally, pass the role class and the type of identifier in the generic arguments.

In the class, define the method findRoleByRoleName() to find a role by name. The method will help you to find if a role exists before creating a new role.

Create a role service

Create a file named RoleService.java in the auth package. Copy and paste the following code into the file.

package com.javawhizz.SpringSecurity.auth;

public interface RoleService {
    Role getRoleUSER();
}

The interface created in the file defines a method named getRoleUSER() that returns a role. This method adds a role to a user when creating a user account.

Create a file named RoleServiceImpl.java in the auth package. Copy and paste the following code into the file.

package com.javawhizz.SpringSecurity.auth;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.Optional;
import java.util.Set;

@Service
@RequiredArgsConstructor
public class RoleServiceImpl implements RoleService {
    private final RoleRepository roleRepository;
    @Override
    public Role getRoleUSER() {
        Role role = new Role("USER")
                .addAuthorities(Set.of(
                        new Authority("customer:read"),
                        new Authority("customer:write")));

        Optional<Role> theRole = roleRepository
                .findRoleByRoleName(role.getRoleName());
        if (theRole.isEmpty()){
            return roleRepository.save(role);
        }
        return theRole.stream()
                .findFirst()
                .orElseThrow(() -> new 
                        RuntimeException("role not found"));
    }
}

The method getRoleUSER() returns an instance of a role every time it gets called. To realize this, the method defines a role named USER with the authority to read and write from a customer.

Before returning a role, the method first checks if the role exists in the database. Otherwise, the method saves the new role using the role repository and returns the new entry.

Create a user account model

Create a package named security in src/main/java/com/javawhizz/SpringSecurity. In the security package, create a file named UserAccount.java. Copy and paste the following code into the file.

package com.javawhizz.SpringSecurity.security;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.javawhizz.SpringSecurity.auth.Role;
import com.javawhizz.SpringSecurity.customer.Customer;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.util.HashSet;
import java.util.Set;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class UserAccount {

    @Id
    @GeneratedValue
    private Long userAccountId;

    @Column(name = "username")
    private String username;

    @Column(name = "password")
    private String password;

    @OneToMany(
            fetch = FetchType.EAGER,
            mappedBy = "userAccount",
            cascade = {
                    CascadeType.MERGE,
                    CascadeType.DETACH,
                    CascadeType.REFRESH,
                    CascadeType.PERSIST
            }
    )
    private Set<Role> roles = new HashSet<>();

    @OneToOne(
            fetch = FetchType.EAGER,
            cascade = {
                    CascadeType.MERGE,
                    CascadeType.DETACH,
                    CascadeType.REFRESH,
                    CascadeType.PERSIST
            }
    )
    @JoinColumn(name = "customer_id")
    @JsonIgnore
    private Customer customer;
    private boolean isAccountNonExpired;
    private boolean isAccountNonLocked;
    private boolean isCredentialsNonExpired;
    private boolean isEnabled;

    public UserAccount(String username, String password){
        this.username = username;
        this.password = password;
        this.isAccountNonLocked = true;
        this.isAccountNonExpired = true;
        this.isCredentialsNonExpired = true;
        this.isEnabled = true;
    }

    public UserAccount addRoles(Set<Role> roles){
        for (Role role : roles) {
            if (role != null){
                this.roles.add(role);
                role.setUserAccount(this);
            }
        }
        return this;
    }

    public UserAccount addCustomer(Customer customer){
        if (customer != null){
            this.customer = customer;
            customer.setUserAccount(this);
        }
        return this;
    }

}

The user account class will create an entity to persist the user credentials. A user should provide a username and password during the login process.

As a result, the user account should define the username and password fields. Since a user account can have many roles, the class has a one-to-many relationship with the role class.

The UserAccount() method expecting a username and password will create a new user.

You should also add a one-to-one relationship mapping with customers. This is because a user account belongs to a customer.

To add a set of roles to the user account, you will use the addRoles() method. The method addCustomer() adds the customer who owns the user account.

Creating user account details

Create a file named UserAccountDetails.java in the security package. Copy and paste the following code into the file.

package com.javawhizz.SpringSecurity.security;

import com.javawhizz.SpringSecurity.auth.Role;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;
import java.util.stream.Collectors;

public class UserAccountDetails implements UserDetails {
    private String username;
    private String password;
    private boolean isAccountNonExpired;
    private boolean isAccountNonLocked;
    private boolean isCredentialsNonExpired;
    private boolean isEnabled;
    private Set<? extends GrantedAuthority> grantedAuthorities;

    public UserAccountDetails(UserAccount userAccount){
        this.username =
                userAccount.getUsername();
        this.password =
                userAccount.getPassword();
        this.isAccountNonExpired =
                userAccount.isAccountNonExpired();
        this.isAccountNonLocked =
                userAccount.isAccountNonLocked();
        this.isCredentialsNonExpired =
                userAccount.isCredentialsNonExpired();
        this.isEnabled =
                userAccount.isEnabled();
        this.grantedAuthorities = grantedAuthorities(userAccount);
    }

    private Set<SimpleGrantedAuthority>
    grantedAuthorities(UserAccount userAccount){
        return userAccount
                .getRoles()
                .stream()
                .map(Role::getGrantedAuthorities)
                .flatMap(Set::stream)
                .collect(Collectors.toSet());

    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return grantedAuthorities;
    }

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

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

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

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

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

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

When a user logs in, spring security expects to get back the core user information. As a result, once a user logs in, you should return a class whose base class is the user details class.

From the fields of the class, you can see that the class returns the details of an authenticated user. Some of these details include username, password, roles, and authorities.

This information gets added to an authentication object. As a result, you can retrieve it from the object once a user logs in.

Create a user account repository

Create a file named UserAcountRepository.java in the security package. Copy and paste the following code into the file.

package com.javawhizz.SpringSecurity.security;

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

import java.util.Optional;
@Repository
public interface UserAccountRepository extends 
        JpaRepository<UserAccount, Long> {
    Optional<UserAccount> findUserAccountByUsername(String username);
}

The user account repository executes queries for the user account entity. The method in the class uses the provided username to check if a user exits during the login process.

Create a password encoder

Create a file named UserAccountPasswordEncoder.java in the security package. Copy and paste the following code into the file.

package com.javawhizz.SpringSecurity.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class UserAccountPasswordEncoder {
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder(10);
    }
}

The configuration class defines a PasswordEncoder bean to encode the password. Note that Bcrypt is one of the password encoder providers.

Encode a password by calling the encode() method and passing the password as the argument.

Create a user account service

Create a file named AccountService.java in the security package. Copy and paste the following code into the file.

package com.javawhizz.SpringSecurity.security;

import com.javawhizz.SpringSecurity.customer.Customer;

public interface AccountService {
    UserAccount createUserAccount(Customer customer) ;
}

The method createUserAccount() defined in the account service creates a user account. Since a customer must have a user account, it uses the customer data to create a user account.

Create a file named UserAccountService.java in the security package. Copy and paste the following code into the file.

package com.javawhizz.SpringSecurity.security;

import com.javawhizz.SpringSecurity.auth.RoleService;
import com.javawhizz.SpringSecurity.customer.Customer;
import lombok.RequiredArgsConstructor;
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.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.Optional;

@Service
@RequiredArgsConstructor
public class UserAccountService implements
        UserDetailsService, AccountService {
    private final UserAccountRepository userAccountRepository;
    private final PasswordEncoder passwordEncoder;
    private final RoleService roleService;
    @Override
    public UserDetails loadUserByUsername(String username) throws
            UsernameNotFoundException {
        UserAccount userAccount = userAccountRepository
                .findUserAccountByUsername(username)
                .stream()
                .findFirst()
                .orElseThrow(() ->
                        new UsernameNotFoundException("username not found")
                );
        return new UserAccountDetails(userAccount);
    }

    @Override
    public UserAccount createUserAccount(Customer customer) {
        UserAccount userAccount = new UserAccount(customer.getEmail(),
                passwordEncoder.encode(customer.getPassword()))
                .addRoles(Collections.singleton(roleService.getRoleUSER()))
                .addCustomer(customer);

        Optional<UserAccount> userAccountByUsername = userAccountRepository
                .findUserAccountByUsername(userAccount.getUsername());
        if (userAccountByUsername.isEmpty()){
            return userAccountRepository.save(userAccount);
        }
        return userAccountByUsername.stream()
                .findFirst()
                .orElseThrow(() -> new
                        RuntimeException("User account not found"));
    }
}

You have seen that the core user information gets returned when a user logs in. In spite of that, how does spring security handle this process?

To achieve this, the UserAccountService class should inherit from the UserDetailsService. As a result, you must override the loadUserByUsername() method.

The method loads a user from the database and returns your custom user account details. The user data gets added to the authentication object of spring security.

The method to create a user account from the AccountService is also overridden in the class. Note that the user account uses the customer’s email as the username.

Additionally, the user account uses the customer’s password as the user password. The user account also gets assigned a role using the getRoleUSER() method.

Call the method addCustomer() on the user account to assign the associated customer.

Create a user account controller

Create a file named UserAccountController.java in the security package. Copy and paste the following code into the file.

package com.javawhizz.SpringSecurity.security;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.javawhizz.SpringSecurity.customer.Customer;
import lombok.RequiredArgsConstructor;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
@RequestMapping("/")
@RequiredArgsConstructor
public class UserAccountController {
    private final UserAccountRepository userAccountRepository;
    @GetMapping
    public String home(Model model){
        model.addAttribute("customer",
                new Customer());
        return "register";
    }

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

    @PreAuthorize("hasAuthority('customer:read')")
    @GetMapping("/profile")
    public String userProfile(Authentication authentication, Model model) {
        UserAccount userAccount = userAccountRepository
                .findUserAccountByUsername(authentication.getName())
                .stream()
                .findFirst()
                .orElseThrow(() -> new
                        UsernameNotFoundException("username not found"));
        model.addAttribute("customer", userAccount.getCustomer());
        return "profile";
    }


}

The method named home() returns the file named register.html for creating a customer. Note that the form to create a customer is the default page returned when you access the application.

To authenticate a user to access a resource, spring security uses the signIn() method. This is due to the @GetMapping annotation which maps the login page to /login.

As a result, the method returns the login.html page for the user to log in. In the next section, you will configure spring security to use this path.

The default behavior is to return the customer profile after the customer registration. Since the userProfile() method has the @PreAuthorize annotation, the user must log in.

Use the authentication object to retrieve the logged-in user by calling getName(). To add the customer object to the model, search for a user whose username matches the logged-in user.

Finally, call the getCustomer() method on the user object to get the customer’s data.

Configure spring security

Create a package named config in src/main/java/com/javawhizz/SpringSecurity. In the config package, create a file named SpringSecurityConfig.java. Copy and paste the following code into the file.

package com.javawhizz.SpringSecurity.config;

import com.javawhizz.SpringSecurity.security.UserAccountService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@RequiredArgsConstructor
@EnableMethodSecurity
public class SpringSecurityConfig {
    private final UserAccountService userAccountService;
    private final PasswordEncoder passwordEncoder;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) 
           throws Exception{
       http.
               csrf(AbstractHttpConfigurer::disable);
       http
               .authorizeHttpRequests((httpRequests) -> {
                   httpRequests.requestMatchers("/", "register")
                           .permitAll()
                           .requestMatchers(HttpMethod.POST,
                                    "/api/v1/customers/*")
                           .permitAll()
                           .anyRequest()
                           .authenticated();
               }).formLogin((formLogin) -> {
                   formLogin.loginPage("/login")
                           .loginProcessingUrl("/login")
                           .defaultSuccessUrl("/profile")
                           .permitAll();
               }).logout((logout) -> {
                   logout.logoutUrl("/logout")
                           .logoutSuccessUrl("/login")
                           .permitAll();
               });

       return http.build();
    }

    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider(){
        DaoAuthenticationProvider provider =
                new DaoAuthenticationProvider();
        provider.setPasswordEncoder(passwordEncoder);
        provider.setUserDetailsService(userAccountService);
        return provider;
    }

    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) 
           throws Exception{
        AuthenticationManagerBuilder auth =
                http.getSharedObject(AuthenticationManagerBuilder.class);
        auth.authenticationProvider(daoAuthenticationProvider());
        return auth.build();
    }
}

This class will contain configurations, add the @Configuration annotation to the class. Additionally, add the annotation @EnableMethodSecurity to the class.

This annotation allows the application to use @PreAuthorize annotations on a controller.

The SecurityFilterChain bean creates a filter chain that gets matched against a HttpServletRequest . As a result, spring security applies security to the matched requests for a user to access a resource.

In the above filter chain, the register page is accessible without authentication. The same case applies to any request that has the URI /api/v1/customers/*.

Spring security requires authentication for other requests. The formLogin() sets the path to GET the login page as /login. Additionally, it also sets the path to POST the login credentials as /login. On successful login, the application returns the profile page.

Apart from this, the filter chain also sets the logout path as /logout. On successful logout, the application returns the login page. The login and logout paths have no security by default.

When a user logs in, spring security delegates authentication to the AuthenticationManager. The DaoAuthenticationProvider gets the user details from the database for authentication.

Create registration page

Create a file named register.html in the package src/main/resources/templates. Copy and paste the following code into the file.

<!doctype html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Bootstrap demo</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
          rel="stylesheet"
          integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65"
          crossorigin="anonymous">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-12">
            <h2>Register</h2>
            <form th:object="${customer}"
                  th:action="@{/api/v1/customers/register}"
                  method="post">
                <div class="mb-3">
                    <label for="firstName"
                           class="form-label">First Name</label>
                    <input type="text" class="form-control"
                           id="firstName" required th:field="*{firstName}">
                </div>
                <div class="mb-3">
                    <label for="lastName"
                           class="form-label">Last Name</label>
                    <input type="text" class="form-control"
                           id="lastName" required th:field="*{lastName}">
                </div>
                <div class="mb-3">
                    <label for="username"
                           class="form-label">Username</label>
                    <input type="email" class="form-control"
                           id="username" required th:field="*{email}">
                </div>
                <div class="mb-3">
                    <label for="password"
                           class="form-label">Password</label>
                    <input type="password" class="form-control"
                           id="password" required th:field="*{password}">
                </div>

                <button type="submit" class="btn btn-primary">Signup</button>
            </form>

        </div>

    </div>

</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
        crossorigin="anonymous"></script>
</body>
</html>

Create login page

Create a file named login.html in the package src/main/resources/templates. Copy and paste the following code into the file.

<!doctype html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Bootstrap demo</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css"
          rel="stylesheet"
          integrity="sha384-GLhlTQ8iRABdZLl6O3oVMWSktQOp6b7In1Zl3/Jr59b6EGGoI1aFkw7cmDA6j6gD"
          crossorigin="anonymous">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-12">
            <h2>Login</h2>
            <form th:action="@{/login}" method="post">
                <div class="mb-3">
                    <label for="username"
                           class="form-label">Username</label>
                    <input type="email" class="form-control"
                           id="username" required name="username">
                </div>
                <div class="mb-3">
                    <label for="password"
                           class="form-label">Password</label>
                    <input type="password" class="form-control"
                           id="password" required name="password">
                </div>
                <button type="submit" class="btn btn-primary">Login</button>
            </form>
        </div>

    </div>

</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-w76AqPfDkMBDXo30jS1Sgez6pr3x5MlQ1ZAGC+nuZB+EYdgRZgiwxhTBTkF7CXvN"
        crossorigin="anonymous"></script>
</body>
</html>

Create profile page

Create a file named profile.html in the package sr/main/resources/templates. Copy and paste the following code into the file.

<!doctype html>
<html lang="en" xmlns:th="https://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Bootstrap demo</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"
          rel="stylesheet"
          integrity="sha384-rbsA2VBKQhggwzxH7pPCaAqO46MgnOM80zW1RWuH61DGLwZJEdK2Kadq2F9CUG65"
          crossorigin="anonymous">
</head>
<body>
<div class="container">
    <div class="row">
        <div class="col-md-12">
            <nav class="navbar bg-light">
                <div class="container-fluid">
                    <a class="navbar-brand">Profile</a>
                    <div class="d-flex" >
                        <div th:if="${customer != null}">
                            <p>Welcome, 
                                <span 
                                    th:text="${customer.getFirstName()}">
                                </span>
                            </p>
                            <a class="btn btn-danger" 
                               th:href="@{/logout}">
                                Logout
                            </a>
                        </div>

                    </div>
                </div>
            </nav>
        </div>

    </div>

</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"
        integrity="sha384-kenU1KFdBIe4zVF0s0G1M5b4hcpxyD9F7jL+jjXkk+Q2h455rYXK/7HAuoJl+0I4"
        crossorigin="anonymous"></script>
</body>
</html>

Run and test the application

To run the application in IntelliJ, press View > Tool Windows > Terminal to open a terminal window. Copy a paste the following command in the terminal then press enter to run the application.

C:\Users\redhat\Documents\Projects\SpringSecurity> ./mvnw spring-boot:run

If your application starts without any errors, go to localhost:8080. You should see the following page. Fill in the details and press the signup button.

user registration page

After pressing the signup button, the application directs you to the profile page. In spite of this, spring security returns the login page.

This is because the profile page is only accessible to authorized users. Enter the username and password in the login form and press the login button.

user login page

You should see the following page if you are able to log in without any errors.

user profile page

Conclusion

In this tutorial, you have learned how to use Spring Security to secure a Spring Boot application. One of the important concepts in this tutorial is how to create roles and authorities for a user.

The article has also taught you how to return a user with their granted authorities during the login. In spring security these are the most challenging features for a beginner to develop.

Play around with the application to refactor it or add more spring security features. This will enhance grasping more security concepts to use in your application.

If you get stuck while reading the article, this article is also available on YouTube to follow through. The code for the tutorial is also available on GitHub.

Happy Hacking!


0 Comments

Leave a Reply

Avatar placeholder

Your email address will not be published. Required fields are marked *