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 .
Table of Contents
- Form Login Flow
- Form Login Architecture
- Form Login Project with JPA
- Form Login Integrating Springdoc
- Conclusion
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.
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 authReq
. DaoAuthenticationProvider 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.
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 username
, password
, 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.
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 username
, password
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.
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.