Spring Security Form Login with JPA and Springdoc Explained

Photo by Felix Kolthoff on Unsplash
Photo by Felix Kolthoff on Unsplash
This article will explain how to use Spring Security’s Form Login and how to integrate JPA. Finally, we will also explain how to integrate Springdoc.

This article will explain how to use Spring Security‘s Form Login and how to integrate JPA. In addition, we will explain the flow and architecture of Form Login. Finally, we will also explain how to integrate Springdoc. Hope after reading, you can have an in-depth understanding of Spring Security’s Form Login.

The complete code can be found in .

Form Login Flow

Before we start coding, let’s first understand the flow of Spring Security Form Login. The following figure shows the general flow of Form Login.

Form Login Flow
Form Login Flow

We will start to explain from UsernamePasswordAuthenticationFilter in the SecurityFilterChain. If you are not familiar with how Spring Security enters SecurityFilterChain, you can read this article first.

UsernamePasswordAuthenticationFilter is the first place where Spring Security uses to support Form Login. It is mainly divided into three parts, that is verification, verification success, and verification failure.

At the beginning, UsernamePasswordAuthenticationFilter will first check whether incoming HTTP Request URI is /login. If it is, get an username and password from form data. Then, it will enter the verification section. If the above conditions are not met, it calls next filter directly.

In authentication part, it first generates a UsernamePasswordAuthenticationToken called authReq, which inherits Authentication, and uses DaoAuthenticationProvider in ProviderManager to authenticate authReqDaoAuthenticationProvider uses UserDetailsService to get username password, and compares userDetails.password and authReq.password. PasswordEncoder will first encode authReq.password and then compare it with userDetails.password.

If verification is successful, it sets auth to the SecurityContextHolder and call rememberMeService.loginSuccess(). Finally, it calls successHandler.onAuthenticationSuccess(), which will redirect to an URI. This URI is / by default, so when the login is successful, it will redirect from /login to /.

If verification fails, it clears SecurityContextHolder. Finally it calls failureHandler.onAuthenticationFailure(), and redirects to /login?error.

RememberMeService is used to send cookies to front-end browsers. In this case, if cookies are detected by session, it will automatically login.

In fact, the whole flow is pretty straightforward. If you understand this flow, the following details will be easy to handle!

Form Login Architecture

The high abstraction of Spring Security makes it expandable and customizable, but it also makes it difficult to understand. The following figure shows the interfaces and classes used in Form Login, of which there are 9 interfaces and abstract classes.

Form Login Architecture
Form Login Architecture

Next we will explain each interface and class. Some have already been explained in the article below, so we won’t discuss them in depth. If you don’t understand, you can go back and review it again.

AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter mainly structure the entire Form Login process. It calls attemptAuthentication() to do verification, After that, it calls successfulAuthentication() or unsuccessfulAuthentication(), and also implements them.

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter mainly implements attemptAuthentication() to provide an authentication process. It obtains username and password from form data, and calls ProviderManager to verify.

AuthenticationManager

AuthenticationManager is an interface used to authenticate Authentication objects.

ProviderManager

ProviderManager is the most commonly used AuthenticationManager implementation. It contains a List<AuthenticationProvider> and parent, which provides multiple opportunities to verify Authentication objects.

AuthenticationProvider

AuthenticationProvider and AuthenticationManager both are to verify Authentication objects. The difference is that AuthenticationProvider is included in ProviderManager.

DaoAuthenticationProvider

The implementation of DaoAuthenticationProvider mainly uses UserDetailsService to obtain the real password of the username passed in, and compare it with PasswordEncoder to see if the passed password matches the real password.

UserDetailsService

UserDetailsService is an interface used to obtain the password of a certain username.

InMemoryUserDetailsManager

InMemoryUserDetailsManager contains users field to store several UserDetails in memory. Of course, it can only compare these usernames.

UserDetails

UserDetails is used to provide a user’s username, password, and Collection<GrantedAuthority> interface. GrantedAuthority refers to user’s authority.

User

User is the simplest UserDetails implementation. It consists of three fields, that is usernamepassword, and authorities. Although it is simple, it is very practical.

PasswordEncoder

PasswordEncoder mainly provides two methods. encode() encodes the password, while matches() compares plaintext password with encrypted password.

DelegatingPasswordEncoder

DelegatingPasswordEncoder includes a Map<String, PasswordEncoder>, which can support multiple PasswordEncoders at the same time. In order to distinguish encoding method of password, the password format is {id}Password. Password is prefixed with parentheses to enclose an id, and it uses this id to obtain the corresponding PasswordEncoder in the map. Spring Security provides some PasswordEncoders, such as BCryptPasswordEncoder, Argon2PasswordEncoder, Pbkdf2PasswordEncoder, SCryptPasswordEncoder, etc.

AuthenticationSuccessHandler

AuthenticationSuccessHandler provides an opportunity for you to process some things after authentication succeeds.

SimpleUrlAuthenticationSuccessHandler

SimpleUrlAuthenticationSuccessHandler redirects to a preset URI. The default is /.

AuthenticationFailureHandler

AuthenticationFailureHandler provides an opportunity for you to handle some things after authentication fails.

SimpleUrlAuthenticationFailureHandler

SimpleUrlAuthenticationFailureHandler redirects to a preset URI. The default is /login?error.

RedirectStrategy

RedirectStrategy provides the function of redirecting.

DefaultRedirectStrategy

DefaultRedirectStrategy is the most basic implementation. It directly calls HttpServletResponse.sendRedirect() to do redirects.

Form Login Project with JPA

After understanding the process and architecture of Spring Security’s Form Login, let’s build a Spring Security project. In this project, we use Form Login as the login process, read username and password from database, and perform verification.

Creating a Project

Create a Spring Boot project and include 4 dependencies in the figure below.

Form Login + JPA Project
Form Login + JPA Project

If you don’t know how to create a project, or don’t understand the figure above, you can refer to the following article first.

Creating a Database

Add Member as entity of the database. It has usernamepassword and authorities.

package com.waynestalk.usernamepasswordexample.model

import javax.persistence.*

@Entity
data class Member(
        @Column(unique = true) val username: String,
        val password: String,
        val name: String,
        @ElementCollection val authorities: Collection<String>,
        @Id @GeneratedValue var id: Long? = null
)

Add MemberRepository to read database data. It finds corresponding Member according to username.

package com.waynestalk.usernamepasswordexample.repository

import com.waynestalk.usernamepasswordexample.model.Member
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.data.jpa.repository.Query

interface MemberRepository : JpaRepository<Member, Long> {
    @Query("SELECT m FROM Member m JOIN FETCH m.authorities WHERE m.username = (:username)")
    fun findByUsername(username: String): Member?
}

Let’s create several members in the database in advance so that they can be used for testing later. Among them, only Monika has USER and ADMIN permissions.

package com.waynestalk.usernamepasswordexample.config

import com.waynestalk.usernamepasswordexample.model.Member
import com.waynestalk.usernamepasswordexample.repository.MemberRepository
import org.springframework.boot.ApplicationRunner
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.crypto.password.PasswordEncoder

@Configuration
class MemberConfiguration {
    @Bean
    fun initMembers(memberRepository: MemberRepository, passwordEncoder: PasswordEncoder) = ApplicationRunner {
        memberRepository.saveAll(listOf(
                Member("monika", passwordEncoder.encode("123456"), "Monika", listOf("ROLE_ADMIN", "ROLE_USER")),
                Member("jack", passwordEncoder.encode("123456"), "Jack", listOf("ROLE_USER")),
                Member("peter", "123456", "Peter", listOf("ROLE_USER"))
        ))
    }
}

Configuring Spring Security

Spring Security is configured by overwriting WebSecurityConfigurerAdapter.configure(). Add SecurityConfiguration, its code is as follows:

package com.waynestalk.usernamepasswordexample.config

import org.springframework.context.annotation.Configuration
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.WebSecurityConfigurerAdapter

@Configuration
@EnableWebSecurity
class SecurityConfiguration : WebSecurityConfigurerAdapter() {
    override fun configure(http: HttpSecurity) {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/greet").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .defaultSuccessUrl("/welcome")
    }
}

To enable Spring Security, SecurityConfiguration must be declared with @EnableWebSecurity. It’s very important, don’t miss it!

We set that GET /greet requires ADMIN authority. So Monika is able to access /greet, but Jack and Peter will be rejected.

We also have to configure to use Form Login, and after authentication succeeds, it redirects to /welcome.

Add MemberController, and implement GET /greet and GET /welcome.

package com.waynestalk.usernamepasswordexample.controller

import com.waynestalk.usernamepasswordexample.repository.MemberRepository
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import javax.servlet.http.HttpServletRequest

@RestController
class MemberController(private val memberRepository: MemberRepository) {
    @GetMapping("/welcome")
    fun welcome(request: HttpServletRequest): WelcomeResponse {
        val member = memberRepository.findByUsername(request.userPrincipal.name)!!
        return WelcomeResponse(member.name, member.authorities)
    }

    data class WelcomeResponse(val name: String, val authorities: Collection<String>)

    @GetMapping("/greet")
    fun greet(request: HttpServletRequest): GreetResponse {
        val member = memberRepository.findByUsername(request.userPrincipal.name)!!
        return GreetResponse("Hello ${member.name}")
    }

    data class GreetResponse(val message: String)
}

Configuring UserDetailsService & PasswordEncoder

From the previous architecture diagram, we can see that if we want to read member data from database for verification, we must start with UserDetailsService. Spring Security uses InMemoryUserDetailsManager by default, and we will add MemberUserDetailsService to replace it. In addition, we will also use BCryptPasswordEncoder to replace DelegatingPasswordEncoder.

The figure below shows our implementation strategy. Compared with the previous architecture diagram, red shows the parts that we will change. The figure below should be able to express more clearly why we need to add MemberUserDetailsService.

Add MemberUserDetailsService
Add MemberUserDetailsService

First of all, in SecurityConfiguration, crate a bean of BCryptPasswordEncoder. Spring Security will use this bean to replace the default PasswordEncoder.

class SecurityConfiguration : WebSecurityConfigurerAdapter() {
    @Bean
    fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()

    // ...
}

Add MemberUserDetailsService, the code is as follows:

package com.waynestalk.usernamepasswordexample.config

import com.waynestalk.usernamepasswordexample.repository.MemberRepository
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.core.userdetails.User
import org.springframework.security.core.userdetails.UserDetails
import org.springframework.security.core.userdetails.UserDetailsService
import org.springframework.security.core.userdetails.UsernameNotFoundException
import org.springframework.stereotype.Component

@Component
class MemberUserDetailsService(private val memberRepository: MemberRepository) : UserDetailsService {
    override fun loadUserByUsername(username: String): UserDetails {
        val member = memberRepository.findByUsername(username)
                ?: throw UsernameNotFoundException("$username was not found")

        val authority = member.authorities.map { SimpleGrantedAuthority(it) }
        return User(member.username, member.password, authority)
    }
}

The logic of MemberUserDetailsService is very simple. It obtains a Member from database based on the username passed in, converts the Member into a User object, and returns it.

We also only need to declare MemberUserDetailsService as @Component. Spring Security will replace it with the default UserDetailsService.

Testing

Next, let’s test it. Log in with Monika.

% curl --location --request POST 'http://localhost:8080/login' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'username=monika' \
--data-urlencode 'password=123456'

After logging in, you will receive a JSESSIONID cookie, and use it to access GET /greet.

% curl --location --request GET 'http://localhost:8080/greet' \
--header 'Cookie: JSESSIONID=56EF2022BFC6E48DE1918DDF998E33FB'

Then you will receive the following response, indicating that Monika has access to GET /greet.

{
    "message": "Hello Monika"
}

Log in with Jack again.

% curl --location --request POST 'http://localhost:8080/login' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'username=jack' \
--data-urlencode 'password=123456'

After logging in, use the received JSESSIONID to access GET /greet.

% curl --location --request GET 'http://localhost:8080/greet' \
--header 'Cookie: JSESSIONID=5BB631D503A5CF8F035D1B40E57C2E22'

As we expected, Jack does not have access to GET /greet. Therefore, you will receive the following 403 response.

{
    "timestamp": "2020-08-19T03:06:26.550+00:00",
    "status": 403,
    "error": "Forbidden",
    "message": "",
    "path": "/greet"
}

Method Security

Spring Security’s Method Security means that we can set access permissions to class’ methods through annotations. Compared with setting them in WebSecurityConfigurerAdapter.configure(), each have its own benefits. One is decentralized management, and the other is centralized management.

In addition, WebSecurityConfigurerAdapter.configure()is setting authorities to URIs. However, Method Security can set authorities to any method. This further strengthens security control of the entire program.

In SecurityConfiguration.configure(), it removes .antMatchers("/greet").hasRole("ADMIN"), and add @EnableGlobalMethodSecurity(prePostEnabled = true) into SecurityConfiguration.

import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfiguration : WebSecurityConfigurerAdapter() {
    @Bean
    fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()

    override fun configure(http: HttpSecurity) {
        http
                .csrf().disable()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .defaultSuccessUrl("/welcome")
    }
}

In MemberController, we add @PreAuthorize("hasRole('ROLE_ADMIN')") on great(). The code is as follows:

import org.springframework.security.access.prepost.PreAuthorize

@RestController
class MemberController(private val memberRepository: MemberRepository) {
    @GetMapping("/greet")
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    fun greet(request: HttpServletRequest): GreetResponse {
        val member = memberRepository.findByUsername(request.userPrincipal.name)!!
        return GreetResponse("Hello ${member.name}")
    }

    data class GreetResponse(val message: String)
}

With a few lines of code, you can enable Method Security, isn’t it pretty simple? Let’s use the previously testing process to test it again!

Form Login Integrating Springdoc

Springdoc can help us generate API files with Swagger interface. If you don’t know Springdoc, or don’t know how to include Springdoc dependency, you can read the following article first.

After including Springdoc’s dependency, add SwaggerConfiguration. The code is as follows:

package com.waynestalk.usernamepasswordexample.config

import io.swagger.v3.oas.annotations.OpenAPIDefinition
import io.swagger.v3.oas.annotations.info.Info
import org.springframework.context.annotation.Configuration

@OpenAPIDefinition(info = Info(title = "Wayne's Talk API", version = "v1.0.0"))
@Configuration
class SwaggerConfiguration

This setting is sufficient for Springdoc, but you can’t browse http://localhost:8080/swagger-ui.html because it is blocked by Spring Security. Therefore, we need to tell Spring Security to allow Springdoc-related URIs to pass without verification.

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfiguration : WebSecurityConfigurerAdapter() {
    @Bean
    fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()

    override fun configure(http: HttpSecurity) {
        http
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .defaultSuccessUrl("/welcome")
    }
}

Now you can browse http://localhost:8080/swagger-ui.html . However, we will find that there is no POST /login in Swagger, so we cannot log in in Swagger. This is because we do not have a @RestController and declare POST /login.

Add LoginController and declare login(). However, we do not implement its content. Because Spring Security will intercept POST /login and do verification. After verification succeeds, it will redirects to the default URI. Therefore, the HTTP request will not arrive to login(). The declaration here is just to allow Springdoc to generate a Swagger API for POST /login.

package com.waynestalk.usernamepasswordexample.controller

import org.springframework.http.MediaType
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RestController

@RestController
class LoginController {
    @PostMapping("/login", consumes = [MediaType.APPLICATION_FORM_URLENCODED_VALUE])
    fun login(@RequestBody request: LoginRequest) {
        throw NotImplementedError("/login should not be called")
    }

    data class LoginRequest(val username: String, val password: String)
}

Now we can log in on Swagger. After logging in, Swagger will receive a JSESSIONID cookie. Then, when accessing GET /greet, it will automatically embed cookies.

Conclusion

After reading this article, you will find that it is actually quite easy to customize functions we want under the architecture of Spring Security. Many of them can be achieved through beans. But the problem is that it is not easy to understand this complex architecture. In addition, we need to understand which components are beans and which are not, so that we know how to replace the original components. However, Spring Security has already implemented a lot of logic for us, and many people are using them. Therefore, it has reliable security and stability. I think it is still worth to understand it.

Leave a Reply

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

You May Also Like