Certification process analysis
3.1 login process analysis
To understand the spring security authentication process, we must first understand the three basic components related to it: AuthenticationManager, ProviderManager and AuthenticationProvider. At the same time, we should also understand the filter of access authentication function, AbstractAuthenticationProcessingFilter.
3.1.1AuthenticationManager
// The default implementation is ProviderManager public interface AuthenticationManager { // Authenticate the identity of the incoming Authentication object. At this time, the incoming parameters only include simple attributes such as user name / password. // If the Authentication is successful, the attribute of the returned Authentication will be completely filled, including the role information of the user Authentication authenticate(Authentication authentication) throws AuthenticationException; }
3.1.2AuthenticationProvider
/** * Perform specific identity authentication for different identity types. For example, DaoAuthenticationProvider is used to support user name / password login authentication, * RememberMeAuthenticationProvider Used to support remember my certification. */ public interface AuthenticationProvider { // Perform specific authentication Authentication authenticate(Authentication authentication) throws AuthenticationException; // Judge whether the current AuthenticationProvider supports the corresponding identity type boolean supports(Class<?> authentication); }
The specific implementation of the authenticate method of DaoAuthenticationProvider is in AbstractUserDetailsAuthenticationProvider:
public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware { // Do not enable cache objects private UserCache userCache = new NullUserCache(); // Whether to force the Principal object to be treated as a string. The default is false private boolean forcePrincipalAsString = false; // Whether to hide the exception of user name search failure. The default is true. In this way, when the UsernameNotFoundException exception is thrown when the user name search fails, // It is automatically hidden and replaced by a BadCredentialsException exception, which plays a certain role in confusion protected boolean hideUserNotFoundExceptions = true; // It is used to check the user status. In the process of user authentication, it is necessary to check whether the user status is normal, such as locked, expired, available, etc private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks(); // Be responsible for checking whether the password expires after the password verification is successful private UserDetailsChecker postAuthenticationChecks = new DefaultPostAuthenticationChecks(); @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // Get user name String username = determineUsername(authentication); boolean cacheWasUsed = true; // Load users from cache UserDetails user = this.userCache.getUserFromCache(username); if (user == null) { cacheWasUsed = false; try { // Load users from the database, and the related implementation is in DaoAuthenticationProvider 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")); } } try { // Conduct user status check this.preAuthenticationChecks.check(user); // Conduct password verification, and the relevant implementation is in DaoAuthenticationProvider 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); } // Check whether the password has expired this.postAuthenticationChecks.check(user); if (!cacheWasUsed) { this.userCache.putUserInCache(user); } Object principalToReturn = user; if (this.forcePrincipalAsString) { principalToReturn = user.getUsername(); } // Create an authenticated UsernamePasswordAuthenticationToken object and return it return createSuccessAuthentication(principalToReturn, authentication, user); } }
Some abstract methods of AbstractUserDetailsAuthenticationProvider are implemented in DaoAuthenticationProvider:
public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider { // The default password when the user fails to search is mainly to avoid side channel attack. If finding users by user name fails, // Just throw an exception without password comparison. After a lot of tests, hackers will find that some requests take significantly less time than other requests, // The user name of the request is a user name that does not exist, so you can get the system information private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword"; }
3.1.3ProviderManager
In spring security, because the system may support many different authentication methods at the same time, such as user name / password authentication, remember me authentication, mobile phone number dynamic authentication, etc., and different authentication methods correspond to different authenticationproviders, a complete authentication process may be provided by multiple authenticationproviders, The list of them will be represented by ProviderManager.
ProviderManager itself can also configure another AuthenticationManager as a parent, so that when ProviderManager authentication fails, you can enter the parent for authentication again. Theoretically, the parent of ProviderManager can be any type of AuthenticationManager, but usually it is ProviderManager to play the role of parent, that is, ProviderManager is the parent of ProviderManager.
There can also be multiple providermanagers. Multiple providermanagers share the same parent, which is very useful when there are multiple filter chains. When there are multiple filter chains, different paths may correspond to different authentication methods, but different paths may have some common authentication methods at the same time. These common authentication methods can be handled uniformly in the parent.
public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); // Exception thrown by the current authentication process AuthenticationException lastException = null; AuthenticationException parentException = null; Authentication result = null; Authentication parentResult = null; int currentPosition = 0; int size = this.providers.size(); for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } if (logger.isTraceEnabled()) { logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)", provider.getClass().getSimpleName(), ++currentPosition, size)); } try { result = provider.authenticate(authentication); if (result != null) { copyDetails(authentication, result); break; } } catch (AccountStatusException | InternalAuthenticationServiceException ex) { prepareException(ex, authentication); // SEC-546: Avoid polling additional providers if auth failure is due to // invalid account status throw ex; } catch (AuthenticationException ex) { lastException = ex; } } if (result == null && this.parent != null) { // Allow the parent to try. try { parentResult = this.parent.authenticate(authentication); result = parentResult; } catch (ProviderNotFoundException ex) { // ignore as we will throw below if no other exception occurred prior to // calling parent and the parent // may throw ProviderNotFound even though a provider in the child already // handled the request } catch (AuthenticationException ex) { parentException = ex; lastException = ex; } } if (result != null) { if (this.eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { // Authentication is complete. Remove credentials and other secret data // from authentication ((CredentialsContainer) result).eraseCredentials(); } // If the parent AuthenticationManager was attempted and successful then it // will publish an AuthenticationSuccessEvent // This check prevents a duplicate AuthenticationSuccessEvent if the parent // AuthenticationManager already published it // Publishing login success event requires parentResult to be null. If the parentResult is not null, it indicates that the authentication has been successful in the parent, // The event of successful authentication has also been published in the parent, which will cause the event of successful authentication to be published repeatedly if (parentResult == null) { this.eventPublisher.publishAuthenticationSuccess(result); } return result; } // Parent was null, or didn't authenticate (or throw an exception). if (lastException == null) { lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } // If the parent AuthenticationManager was attempted and failed then it will // publish an AbstractAuthenticationFailureEvent // This check prevents a duplicate AbstractAuthenticationFailureEvent if the // parent AuthenticationManager already published it if (parentException == null) { prepareException(lastException, authentication); } throw lastException; } }
3.1.4AbstractAuthenticationProcessingFilter
As a link in the spring security filter chain, AbstractAuthenticationProcessingFilter can be used to process any Authentication submitted to it, and associate the Authentication, AuthenticationManager, AuthenticationProvider and ProviderManager components described earlier:
AbstractAuthenticationProcessingFilter, as an abstract class, has many implementations. If you log in by user name / password, its corresponding implementation class is UsernamePasswordAuthenticationFilter, and the constructed Authentication object is UsernamePasswordAuthenticationToken. As for AuthenticationManager, generally, its implementation class is ProviderManager:
General certification process:
- When a user submits a login request, UsernamePasswordAuthenticationFilter will extract the login user name / password from the current request HttpServletRequest, and then create a UsernamePasswordAuthenticationToken object.
- The UsernamePasswordAuthenticationToken object will be passed into the ProviderManager for specific authentication operations.
- If the authentication fails, the relevant information in the SecurityContextHolder will be cleared, and the login failure callback will also be called.
- If the authentication is successful, the login information storage, session concurrent processing, login success event publishing, login success method callback and other operations will be carried out.
public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware { private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { // Judge whether the current request is a login authentication request. If not, continue with the remaining filters directly if (!requiresAuthentication(request, response)) { chain.doFilter(request, response); return; } try { // Obtain an authenticated Authentication object, which is specifically implemented in subclasses, such as UsernamePasswordAuthenticationFilter Authentication authenticationResult = attemptAuthentication(request, response); if (authenticationResult == null) { // return immediately as subclass has indicated that it hasn't completed return; } // Dealing with session concurrency this.sessionStrategy.onAuthentication(authenticationResult, request, response); // Authentication success // Judge whether the request needs to continue to go down. The default is false 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); } } protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) { if (this.requiresAuthenticationRequestMatcher.matches(request)) { return true; } if (this.logger.isTraceEnabled()) { this.logger .trace(LogMessage.format("Did not match request to %s", this.requiresAuthenticationRequestMatcher)); } return false; } protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { // Store user information into SecurityContextHolder SecurityContextHolder.getContext().setAuthentication(authResult); if (this.logger.isDebugEnabled()) { this.logger.debug(LogMessage.format("Set SecurityContextHolder to %s", authResult)); } // Processing cookie s this.rememberMeServices.loginSuccess(request, response, authResult); if (this.eventPublisher != null) { // Publish the authentication success event. This event type is InteractiveAuthenticationSuccessEvent, which indicates that the authentication is successful through some automatic interactive methods, // For example, log in through remember me this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass())); } // Call the callback method with successful authentication this.successHandler.onAuthenticationSuccess(request, response, authResult); } protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { // Clear data from SecurityContextHolder SecurityContextHolder.clearContext(); this.logger.trace("Failed to process authentication request", failed); this.logger.trace("Cleared SecurityContextHolder"); this.logger.trace("Handling authentication failure"); // Processing cookie s this.rememberMeServices.loginFail(request, response); // Call the callback method of authentication failure this.failureHandler.onAuthenticationFailure(request, response, failed); } }
3.2 configuring multiple data sources
Authentication needs to go through the AuthenticationProvider. Each AuthenticationProvider is configured with a UserDetailsService, and different userdetailsservices can represent different data sources. Therefore, you only need to manually configure multiple authenticationproviders and provide different userdetailsservices for different authenticationproviders.
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean @Primary UserDetailsService userDetailsService01() { return new InMemoryUserDetailsManager(User.builder().username("javaboy").password("{noop}123").roles("admin").build()); } @Bean UserDetailsService userDetailsService02() { return new InMemoryUserDetailsManager(User.builder().username("sang").password("{noop}123").roles("user").build()); } @Override @Bean // Extra attention public AuthenticationManager authenticationManagerBean() throws Exception { DaoAuthenticationProvider dao1 = new DaoAuthenticationProvider(); dao1.setUserDetailsService(userDetailsService01()); DaoAuthenticationProvider dao2 = new DaoAuthenticationProvider(); dao2.setUserDetailsService(userDetailsService02()); return new ProviderManager(dao1, dao2); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() // ellipsis } }
3.3 add login verification code
The login verification code needs to be defined by the developer. Generally speaking, there are two ways to realize login verification code:
- Custom filters (described in the filter chapter).
- Custom authentication logic.
@Configuration public class KaptchaConfig { @Bean Producer kaptcha() { Properties properties = new Properties(); properties.setProperty("kaptcha.image.width", "150"); properties.setProperty("kaptcha.image.height", "50"); properties.setProperty("kaptcha.textproducer.char.string", "0123456789"); properties.setProperty("kaptcha.textproducer.char.length", "4"); Config config = new Config(properties); DefaultKaptcha defaultKaptcha = new DefaultKaptcha(); defaultKaptcha.setConfig(config); return defaultKaptcha; } } @Controller public class HelloController { @Autowired Producer producer; @GetMapping("/vc.jpg") public void getVerifyCode(HttpServletResponse response, HttpSession session) throws IOException { response.setContentType("image/jpeg"); String text = producer.createText(); // Store the generated verification code content into the session for later judgment session.setAttribute("kaptcha", text); BufferedImage image = producer.createImage(text); try (ServletOutputStream outputStream = response.getOutputStream()) { ImageIO.write(image, "jpg", outputStream); } } } public class KaptchaAuthenticationProvider extends DaoAuthenticationProvider { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String kaptcha = request.getParameter("kaptcha"); String sessionKaptcha = (String) request.getSession().getAttribute("kaptcha"); if (kaptcha != null && kaptcha.equals(sessionKaptcha)) { // If the verification code is entered correctly, continue to execute the authenticate method of the parent class for the next step of verification return super.authenticate(authentication); } throw new AuthenticationServiceException("Verification code input error"); } } @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { // 1. Configure UserDetailsService and configure data source @Bean UserDetailsService userDetailsService01() { return new InMemoryUserDetailsManager(User.builder().username("javaboy").password("{noop}123").roles("admin").build()); } // 2. Provide an AuthenticationProvider instance and configure UserDetailsService @Bean AuthenticationProvider kaptchaAuthenticationProvider() { KaptchaAuthenticationProvider provider = new KaptchaAuthenticationProvider(); provider.setUserDetailsService(userDetailsService01()); return provider; } // 3. Rewrite the authenticationManagerBean method, provide its own ProviderManager, and use a custom AuthenticationProvider instance @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return new ProviderManager(kaptchaAuthenticationProvider()); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/vc.jpg").permitAll() .anyRequest().authenticated() // ellipsis } }