In the tutorial Overview about request processing in Spring Security, I gave you a brief introduction to the main components in a flow for the authentication part of the Spring Security framework. In this tutorial, we will learn in more detail how Spring Security handles this authentication part!
First, I will create a new Spring Boot project with Spring Security Starter and Spring Web Starter as an example.
The following results:
As I said in the tutorial Overview about request processing in Spring Security, the AuthenticationManager class will be the class that takes care of authentication in Spring Security:
The request after going through the AuthenticationFilter, the user’s login information will be converted to the Authentication object with the implementation of the UsernamePasswordAuthenticationToken class. The AuthenticationManager will use the information in the UsernamePasswordAuthenticationToken class for authentication.
By default, Spring supports us to authenticate username and password using the DaoAuthenticationProvider class.
When authenticate, with authentication information in the UsernamePasswordAuthenticationToken class, Spring Security will get username information in UsernamePasswordAuthenticationToken to check if UserCache has information for this username? I don’t know why the current code used for UserCache is:
1 |
private UserCache userCache = new NullUserCache(); |
NullUserCache only returns null for UserDetails information. Strange thing!
Since there is no information in the cache, Spring Security will use the object of the UserDetailsService interface to get the UserDetails information using the username.
The UserDetailsService interface has only one abstract method, loadUserByUsername():
1 2 3 4 |
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; } |
The implementation of the UserDetailsService interface used in this case is InMemoryUserDetailsManager because I am not using the database for the example in this tutorial.
If there is user information with the username that we are sending when we click the Login button, the UserDetails object containing that user’s information will be returned.
Here, Spring Security also does a few more checks of the user before checking the user’s password information: including checking if the user is locked, is the user enabled and is the user expired?
The DefaultPreAuthenticationChecks class that implements the UserDetailsChecker interface is used to do this with the check() method.
Spring Security will check the password information passed by the user, is the same as the information in the system? Using the additionalAuthenticationChecks() method.
Here, the password information will depend on the encoder that we are configuring with Spring Security, the corresponding Encoder class will be used during password checking.
After checking the password information, Spring Security will have a step to check if the password is expired using the DefaultPostAuthenticationChecks class with the check() method.
During the process of handling authentication flow, if any exception occurs, the AuthenticationFailureHandler interface with SimpleUrlAuthenticationFailureHandler implementation will handle this exception.
And if everything goes smoothly, without any errors, the AuthenticationSuccessHandler interface with the implementation of the SavedRequestAwareAuthenticationSuccessHandler class will handle..
Details for what happened above, you can read more code of class AbstractAuthenticationProcessingFilter private doFilter() method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } try { Authentication authenticationResult = attemptAuthentication(request, response); if (authenticationResult == null) { // return immediately as subclass has indicated that it hasn't completed return; } this.sessionStrategy.onAuthentication(authenticationResult, request, response); // Authentication success if (this.continueChainBeforeSuccessfulAuthentication) { chain.doFilter(request, response); } successfulAuthentication(request, response, chain, authenticationResult); } catch (InternalAuthenticationServiceException failed) { this.logger.error("An internal error occurred while trying to authenticate the user.", failed); unsuccessfulAuthentication(request, response, failed); } catch (AuthenticationException ex) { // Authentication failed unsuccessfulAuthentication(request, response, ex); } } |
class UsernamePasswordAuthenticationFilter with attemptAuthentication() method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); username = (username != null) ? username : ""; username = username.trim(); String password = obtainPassword(request); password = (password != null) ? password : ""; UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } |
and authenticate() method in AbstractUserDetailsAuthenticationProvider class:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
@Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, () -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported")); String username = determineUsername(authentication); boolean cacheWasUsed = true; UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); } catch (UsernameNotFoundException ex) { this.logger.debug("Failed to find user '" + username + "'"); if (!this.hideUserNotFoundExceptions) { throw ex; } throw new BadCredentialsException(this.messages .getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials")); } Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract"); } try { this.preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } catch (AuthenticationException ex) { if (!cacheWasUsed) { throw ex; } // There was a problem, so try again after checking // we're using latest data (i.e. not from the cache) cacheWasUsed = false; user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication); this.preAuthenticationChecks.check(user); additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication); } this.postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } return createSuccessAuthentication(principalToReturn, authentication, user); } |
You can run debug, add breakpoints to these methods, to understand more about how Spring Security is handling the authentication part.
After understanding the details, you can add a custom authentication filter like I did in the tutorial Custom authentication filter login without password in Spring Security easily, depending on your purposes!