[Spring Security Series 02] Spring Security Form Authentication Logic Source Code Interpretation

Keywords: Java Spring Database Session

outline

In the previous section, Spring Security form authentication can be achieved through simple configuration. Today, this section will learn how Spring Security achieves these functions by reading the source code. High-energy early warning ahead. This article analyses the source code for a long time.

<!-- more -->

Filter chain

As I said earlier, Spring Security is based on the form of a filter chain, so I'll explain what filters are there.

Filter Class introduce
SecurityContextPersistenceFilter Determine whether the current user is logged in
CrsfFilter Used to prevent csrf attacks
LogoutFilter Dealing with write-off requests
UsernamePasswordAuthenticationFilter Processing form login requests (also our protagonist today)
BasicAuthenticationFilter Handling requests for http basic authentication

Because there are too many filters in the filter chain, I did not list them one by one. I adjusted several important introductions.

From the above we know that Spring Security's authentication request for form login is handled by Username Password Authentication Filter. Then the specific authentication process is as follows:

As you can see from the figure above, UsernamePasswordAuthentication Filter inherits from the abstract class AbstractAuthentication Processing Filter.

Specific certification is:

  1. Enter the doFilter method to determine whether to authenticate or not, and enter the attemptAuthentication method if authentication is required, if it does not need to end directly.
  2. The attemptAuthentication method constructs a UsernamePasswordAuthentication Token object based on username and password (token is not authenticated at this time), and gives it to ProviderManger to complete authentication.
  3. Provider Manger maintains this list of Authentication Provider objects, determines through traversal and finally chooses the Dao Authentication Provider object to complete the final authentication.
  4. DaoAuthentication Provider retrieves username according to token from Provider Manger, and calls the loadUserByUsername method of UserDetails Service written by us to read user information from the database, then compares user passwords. If authentication passes, the user information is also UserDetails object, and reconstructs UsernPassword Authentication Token (token at this time). It has been certified.

Next, we will analyze the whole authentication process through the source code.

AbstractAuthenticationProcessingFilter

AbstractAuthentication Processing Filter is an abstract class. The filter of all authentication requests will inherit it. It mainly implements some common functions, and the specific verification logic is handed over to the sub-class implementation. It is a little similar to the parent class setting up the authentication process, and the sub-class is responsible for the specific authentication logic, which is somewhat similar to the template method pattern of the design pattern.

Now let's analyze the more important methods in it.

1,doFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        // Eliminate irrelevant code...
    // 1. Determine whether the current request is authenticated
        if (!requiresAuthentication(request, response)) {
      // No need to go directly to the next filter
            chain.doFilter(request, response);
            return;
        }
        try {
      // 2. Start requesting authentication. Attempt Authentication is implemented to subclasses. If authentication succeeds, it returns an Authentication object that has been authenticated.
            authResult = attemptAuthentication(request, response);
            if (authResult == null) {
                return;
            }
      // 3. The successful login puts the authenticated user information into session Session Authentication Strategy interface for extension.
            sessionStrategy.onAuthentication(authResult, request, response);
        }
        catch (InternalAuthenticationServiceException failed) {
      //2.1. An exception occurs, the login fails, and the handler callback for the login fails
            unsuccessfulAuthentication(request, response, failed);
            return;
        }
        catch (AuthenticationException failed) {
      //2.1. An exception occurs, the login fails, and the login failure processor enters.
            unsuccessfulAuthentication(request, response, failed);
            return;
        }
        // 3.1. Successful login, access to the successful login processor.
        successfulAuthentication(request, response, chain, authResult);
    }

2,successfulAuthentication

Login Success Processor

protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {
    //1. The Authentication object with successful authentication is successfully logged into the Security ContextHolder
    //      Security ContextHolder is essentially a ThreadLocal
        SecurityContextHolder.getContext().setAuthentication(authResult);
    //2. If the remember me function is enabled, loginSuccess, which calls rememberMeServices, generates a token
      //   Put token into cookie s so that it can be authenticated next time without login. Detailed analysis of rememberMeServices will be discussed in the next few articles.
        rememberMeServices.loginSuccess(request, response, authResult);
        // Fire event
    //3. Publish a login event.
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                    authResult, this.getClass()));
        }
    //4. Call our own defined login success processor, which is also an extension point for us to know login success.
        successHandler.onAuthenticationSuccess(request, response, authResult);
    }

3,unsuccessfulAuthentication

Logon Failure Processor

protected void unsuccessfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, AuthenticationException failed)
            throws IOException, ServletException {
    //1. The login failed, clearing the information in the Security ContextHolder
        SecurityContextHolder.clearContext();
    //2. Dealing with login failure to remember my function
        rememberMeServices.loginFail(request, response);
    //3. Call our own defined login failure handler, where you can expand the log to record login failure.
        failureHandler.onAuthenticationFailure(request, response, failed);
    }

This is the main analysis of Abstract Authentication Processing Filter. As we can see from the source code, when a request enters the filter, the specific flow is

  1. Determine whether the request is to be authenticated
  2. Call the attemptAuthentication method to start authentication, because it is an abstract method that specifies authentication logic to subclasses
  3. If the login is successful, the Authentication object is written to the session according to the session policy, and the authentication result is written to the SecurityContextHolder. If the function of remembering me is turned on, the token is generated and written to the cookie according to the function of remembering me. Finally, the method of a successHandler object is called. This object can be injected by our configuration to handle me. Some of their custom login success logic (such as logging success log, etc.).
  4. If the login fails, empty the information in the Security ContextHolder and call our own injected failureHandler object to handle our own login failure logic.

UsernamePasswordAuthenticationFilter

From the above analysis, we can know that UsernamePassword Authentication Filter is inherited from AbstractAuthentication Processing Filter and implements its attempt Authentication method to achieve the specific logical implementation of authentication. Next, we read the source code of Username Password Authentication Filter to see how it completes authentication. Since this involves the construction of the UsernamePasswordAuthentication Token object, let's first look at the source code for UsernamePasswordAuthentication Token.

1,UsernamePasswordAuthenticationToken

// Inheritance to AbstractAuthentication Token 
// AbstractAuthentication Token mainly defines that toke needs to have some necessary information in Spring Security
// For example, authority set Collection < Granted Authority > authorities; whether to authenticate through boolean authenticated = false; user information Object details that authenticate through;
public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
 
  // UserDetails objects are saved when the user name is successfully logged in.
    private final Object principal;
  // Password
    private Object credentials;

  /**
  * Constructor, where the user is not logged in, the authenticated is false, representing that the user has not yet been authenticated
  */
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }

 /**
  * Constructor. When the user logs in successfully, an additional parameter is the user's permission set. At this time, authenticated is true, representing the success of authentication.
  */
    public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
            Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true); // must use super, as we override
    }
}

Next we can analyze the attemptAuthentication method.

2,attemptAuthentication

public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
     // 1. Judge if it is a post request, or if it is not, throw an Authentication Service Exception exception. Note that all the exceptions thrown here are captured in the AbstractAuthentication Processing Filter#doFilter method. After capturing, they will enter the login failure logic.
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
    // 2. Get the username and password from the request
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        // 3. Non-empty processing to prevent NPE anomalies
        if (username == null) {
            username = "";
        }
        if (password == null) {
            password = "";
        }
    // 4. Remove spaces
        username = username.trim();
    // 5. Construct a UsernamePassword Authentication Token object based on username and password. From the analysis above, we can see that token is not authenticated at this time.
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);
    // 6. Configure other information ip, etc.
        setDetails(request, authRequest);
   //  7. Call Provider Manger's authenticate method for specific authentication logic
        return this.getAuthenticationManager().authenticate(authRequest);
    }

ProviderManager

Maintain an Authentication Provider list for authentication logic verification

1,authenticate

public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
    // 1. Get the type of token.
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
   // 2. Traversing the Authentication Provider List
        for (AuthenticationProvider provider : getProviders()) {
      // 3. If Authentication Provider does not support the current token type, it skips directly
            if (!provider.supports(toTest)) {
                continue;
            }

            try {
        // 4. If the Provider supports the current token, it is given to the Provider to complete the authentication.
                result = provider.authenticate(authentication);
     
            }
            catch (AccountStatusException e) {

                throw e;
            }
            catch (InternalAuthenticationServiceException e) {
                throw e;
            }
            catch (AuthenticationException e) {
                lastException = e;
            }
        }
    // 5. Login successfully returns token with successful login
        if (result != null) {
            eventPublisher.publishAuthenticationSuccess(result);
            return result;
        }

    }

AbstractUserDetailsAuthenticationProvider

1,authenticate

AbstractUserDetails Authentication Provider implements the Authentication Provider interface and implements some methods. DaoAuthentication Provider inherits from the AbstractUserDetails Authentication Provider class, so let's first look at the implementation of AbstractUserDetails Authentication Provider.

public abstract class AbstractUserDetailsAuthenticationProvider implements
        AuthenticationProvider, InitializingBean, MessageSourceAware {

  // Internationalization
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();


    /**
     * For some checks on token, specific checking logic is handed over to subclasses for implementation, abstract methods
     */
    protected abstract void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException;


  /**
     * The implementation of authentication logic, calling the abstract method retrieveUser to obtain UserDetails objects based on username
     */
    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
    
    // 1. Getting usernmae
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();

    // 2. Attempt to retrieve UserDetails objects from the cache
        UserDetails user = this.userCache.getUserFromCache(username);
    // 3. If it is empty, it means that the current object is not cached.
        if (user == null) {
            cacheWasUsed = false;
            try {
        //4. Call retrieveUser to get UserDetail object. Why is this abstract method easy to know? If UserDetail information exists in relational database, you can rewrite this method and get user information from relational database. If UserDetail information exists elsewhere, you can rewrite this method to get user information in other ways, so it is not affected at all. Sound the whole authentication process, easy to expand.
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
      catch (UsernameNotFoundException notFound) {
                
                // Capture exception log processing and throw it up. Logon failed.
                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "Bad credentials"));
                }
                else {
                    throw notFound;
                }
            }
        }

        try {
      // 5. Pre-check to determine whether the current user is locked, disabled, etc.
            preAuthenticationChecks.check(user);
      // 6. Other checks, in the Dao Authentication Provider, are to check if the passwords are identical.
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException exception) {
        
        }

    // 7. Post-check to determine whether the password has expired
        postAuthenticationChecks.check(user);

     
        // 8. Login successfully reconstructs an authenticated Token object through the UserDetail object
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

    
    protected Authentication createSuccessAuthentication(Object principal,
            Authentication authentication, UserDetails user) {
        // Call the second constructor to construct an authenticated Token object
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
                principal, authentication.getCredentials(),
                authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());

        return result;
    }

}

Next, let's look at the implementation of retrieveUser in detail. Without looking at the source code, you should also know that the retrieveUser method should call UserDetailsService to query whether the user is in the database and whether the user's password is the same.

DaoAuthenticationProvider

DaoAuthentication Provider mainly obtains UserDetail objects through UserDetailService.

1,retrieveUser

protected final UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        try {
      // 1. Call the loadUserByUsername method of the UserDetailsService interface to get the UserDeail object
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
       // 2. If the loadedUser is null representing the current user does not exist, throwing an exception login fails.
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
      // 3. Return the results of the query
            return loadedUser;
        }
    }

2,additionalAuthenticationChecks

protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
    // 1. If the password is empty, an exception is thrown.
        if (authentication.getCredentials() == null) {
            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }

    // 2. Get the password entered by the user
        String presentedPassword = authentication.getCredentials().toString();

    // 3. Call the match method of passwordEncoder to determine whether the password is consistent
        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            logger.debug("Authentication failed: password does not match stored value");

      // 4. Throw an exception if it is inconsistent.
            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
    }

summary

So far, the whole certification process has been analyzed. If you don't know anything, you can pay attention to my public number and discuss it together.

Learning is a long process, learning the source code may be very difficult, but as long as you work hard, there will be acquisition, we all agree to encourage.

Posted by arya6000 on Sat, 04 May 2019 19:30:51 -0700