Spring Boot Authentication and Authorization with form-based login and MySQL Database


Introduction

Authentication and authorization are key requirements for securing web applications. For Spring/Java web applications, Spring Security is the mainstream security framework. The implementation of Spring Security had a notable change from Spring version 5.7.0 - M2 the WebSecurityConfigurerAdapter class was deprecated. Instead, a component-based approach to security was recommended. This new approach utilizes a SecurityFilterChain bean

This article presents a common use case for web security – securing a web application with credentials stored on an external database – using the SecurityFilterChain approach


Objective

After reading this article, the reader should be able to understand and implement Spring Security, using the SecurityFilterChain, to secure (authenticate and authorize) a Spring Boot Application. The credentials will be persisted on an external database

Scope

This article presents a use case with characteristics as follows:

  • Authentication and authorization of a web application

  • User credentials stored on an already connected MySQL database

  • Form-based login (Thymeleaf template)

  • Maven-based spring boot project

It does not address the setup and connection to the external database and does not cover the implementation of the Thymeleaf templates for the login and registration


Prerequisites

Readers are to be familiar with the following:

  • Key concepts of Spring Boot applications

  • Java lambdas

  • Database interaction concepts (e.g Entities, Repostories)

  • Integration of application with MySQL database

  • Thymeleaf templating


Summary Workflow

A summary of the steps for this implementation is as follows:

  1. Addition of required dependencies

  2. Setting up of Entity and Repositories

  3. Setting up the UserDetails and UserDetailsSerivce classes

  4. Writing the SecurityConfig class


Dependencies

To implement Spring Security in Spring Boot, only one dependency is required. Copy and paste the following into the pom.xml file.

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Additionally, to utilize the external mySQL database and the Thymeleaf template respectively, the following dependencies should be added:

<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>8.0.30</version>
</dependency>

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

Code steps

1. Set up the Entity class

This is a Java class that corresponds to a table in the database. This User entity class has three properties: email, username and password. Note that Lombok is utilized to reduce boilerplate for constructors, getters and setters

@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter

public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private long id;

    @Column (unique = true)
    private String email;
    @Column (unique = true)
    private String username;
    private String password;

2. Set up the repository (extending JpaRepository)

The repository provides a simplified way to interact with a database. We will be utilizing the JpaRepository in this case. Define a findByUsername method in the repository

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
}

3. Create a custom UserDetailsService class

Create a class that implements the UserDetailsService interface. Its main function is to retrieve the user from the database. This class will be referenced later in the SecurityConfig class

Override the loadUserByUsername method, which accepts a username as an argument and returns a UserDetails type

public class MyUserDetailsService implements UserDetailsService {
    @Autowired
    UserRepository userRepository;


    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Optional<User> user = userRepository.findByUsername(username);

        user.orElseThrow(()-> new UsernameNotFoundException(username + " not found"));

        return user.map(MyUserDetails::new).get();
    }

}

4. Create a custom UserDetails class

Create a class that implements the UserDetails interface. The purpose of this class is to provide essential information about a user - such as their username, password, authorities/roles, and whether the account is enabled or expired.

You need to implement all the default methods. In this example, all the methods apart from the getPassword and getUsername are set to ‘true’ for convenience. You can set these to interact with your Entity as required

public class MyUserDetails implements UserDetails {
    String username;
    String password;

    public MyUserDetails(User user) {
        username = user.getUsername();
        password = user.getPassword();
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }
    @Override
    public String getPassword() {
        return password;
    }
    @Override
    public String getUsername() {
        return username;
    }
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }
    @Override
    public boolean isEnabled() {
        return true;
    }
}

5. Create Security Configuration Class

This is the core of the Spring Security setup. We will define the authentication and authorization protocols in this class. The following will be implemented:

  1. The SecurityFilterChain bean

  2. The UserDetailsService bean

  3. A Password Encoder bean

  4. The Authentication provider

Create a SecurityFilterChain bean

Implement a SecurityFilterChain bean that takes httpSecurity as a parameter. Call the authorizeHttpRequests method and utilize lambdas to define the permissions. Here's an explanation of the code parts:

  • .requestMatchers - The register and login routes will not require any authentication

  • .anyRequest().authenticated() – Indicates that all other routes apart from the ones specified above require authentication

  • .formLogin… - Indicates that the login will be handled by a custom form and specifies the route of the form

  • .defaultSuccessful – Specifies the route that will be triggered when a login is successful

  • .logout… – Specifies the URL for logging out and the route to be redirected to after a successful logout (which is login in this case).

  • .rememberMe() – Allows authentication to be persisted across multiple sessions

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


    http.authorizeHttpRequests((authz) -> authz
                    .requestMatchers( "/register","/login").permitAll()
                    .anyRequest()
            .authenticated())
            .formLogin(formLogin->formLogin.loginPage("/login")
                    .defaultSuccessUrl("/farms")
                    .permitAll())
            .logout((logout) -> logout
                    .logoutUrl("/logout")
                    .logoutSuccessUrl("/login")

            )
            .rememberMe(Customizer.withDefaults());
                        return http.build();
}

Create a UserDetailsService bean. This should only return your custom class

@Bean
MyUserDetailsService myUserDetailsService() {
    return new MyUserDetailsService();
}

Create a password encoder bean. This is used to encode the password using the chosen encoder type. In this case, the BCrypt encoder is used

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

Create an AuthenticaionProvider bean. This will utilize the DaoAuthenticationProvider, which is an interface commonly used to authenticate users against a user repository such as a database. Provide your custom UserDetailsService class and PasswordEncoder as parameters for the corresponding methods

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

Bonus

Here's an example of a sample user registration service code. Note that the same BCryptPasswordEncoder is used to encode the password before persisting it in the database

@Service
public class UserService {
    @Autowired
    UserRepository userRepository;

    public User addUser(User user){
        PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        String hashedPassword=passwordEncoder.encode(user.getPassword());
        user.setPassword(hashedPassword);
        System.out.println(hashedPassword);
        return userRepository.save(user);
    }

Conclusion

By using the foregoing guideline, you would have implemented authentication and authorization in your Spring Boot web application. You would also have gained a good understanding of the function of each block of code, which will enable you to customize your applications as required