Spring Boot Authentication and Authorization with form-based login and MySQL Database
Table of contents
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:
Addition of required dependencies
Setting up of Entity and Repositories
Setting up the UserDetails and UserDetailsSerivce classes
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:
The SecurityFilterChain bean
The UserDetailsService bean
A Password Encoder bean
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