Certification process analysis

Posted by wsh on Tue, 15 Feb 2022 07:25:41 +0100

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:

  1. 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.
  2. The UsernamePasswordAuthenticationToken object will be passed into the ProviderManager for specific authentication operations.
  3. If the authentication fails, the relevant information in the SecurityContextHolder will be cleared, and the login failure callback will also be called.
  4. 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:

  1. Custom filters (described in the filter chapter).
  2. 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
    }
}

Topics: Java Spring