This is the 26th day of my participation in the November Gwen Challenge. Check out the event details: The last Gwen Challenge 2021

The premise is introduced

This article introduces the principles of Spring Security, which will be of some reference value to the actual business development. I hope it will be helpful to you. In order to meet our needs with Spring Security, it is best to understand its principle, so that we can expand at will. This article mainly records the basic operation process of Spring Security.

The mechanism of filters

Spring Security basically implements configured authentication, permission authentication, and logout through filters.

Spring Security registers a filter FilterChainProxy in the Servlet’s filter chain, which proxies requests to Spring Security’s own filter chains, each of which matches a number of urls. If there is a match, the corresponding filter is executed. The filter chain is sequential, and only the first matching filter chain is executed for a request.

Spring Security’s configuration is essentially adding, removing, and modifying filters.By default, the 15 filters injected by the system correspond to different configuration requirements.

Control filter UsernamePasswordAuthenticationFilter certification

Then we will focus on the analysis of UsernamePasswordAuthenticationFilter this filter, he is used to login using the username and password authentication filters, but in many cases, we more than just a simple user name and password to log in, and can be used in the third party authorization login, This time we need to use a custom filter, of course, here is not to do the details, just say how to inject a custom filter.

@Override
protected void configure(HttpSecurity http) throws Exception { http.addFilterAfter(...) ; . }Copy the code

Identity authentication process

There are a few basic concepts we need to understand before we begin the authentication process:

SecurityContextHolder

SecurityContextHolder stores the SecurityContext object. SecurityContextHolder is a storage proxy with three storage modes:

  • MODE_THREADLOCAL: The SecurityContext is stored in the thread.
  • MODE_INHERITABLETHREADLOCAL: The SecurityContext is stored in the thread, but the child thread can get the SecurityContext in the parent thread.
  • MODE_GLOBAL: SecurityContext is the same in all threads.

The SecurityContextHolder uses MODE_THREADLOCAL mode by default, and the SecurityContext is stored in the current thread.

The SecurityContextHolder object can be retrieved directly from the current thread without being passed the displayed argument.

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
Copy the code

Authentication

Authentication indicates the identity of the current user. What is authentication? For example, a set of user names and passwords is authentication, as is the wrong user name and password, but Spring Security will fail.

The Authentication interface
public interface Authentication extends Principal.Serializable {
       Collection getAuthorities(a);
       Object getCredentials(a);
       Object getDetails(a);
       Object getPrincipal(a);
       boolean isAuthenticated(a);
       void setAuthenticated(boolean isAuthenticated);
}
Copy the code
  • GetAuthorities () : Obtains user authority, which is typical of obtaining information about the user’s role.

  • GetCredentials () : Obtains the authentication information of the user. Generally, the authentication information is obtained, such as the password. However, the authentication information is removed once the login is successful.

  • GetDetails () : Gets additional information about the user, such as IP address, latitude and longitude, and so on

  • GetPrincipal () : Gets the user’s identity information, the user name in the unauthenticated case, and the UserDetails in the authenticated case.

  • IsAuthenticated () : obtains whether the current Authentication isAuthenticated

  • SetAuthenticated: Indicates whether the current Authentication is authenticated

AuthenticationManager ProviderManager AuthenticationProvider

The three are easy to distinguish:

  • AuthenticationManager: The main purpose is to complete the identity authentication process.
  • ProviderManager: Concrete implementation class for the AuthenticationManager interface
    • ProviderManager has a collection property provider that records the AuthenticationProvider object
  • The AuthenticationProvider interface class has two methods
public interface AuthenticationProvider {
    // Implement specific authentication logic, authentication failure throws the corresponding exception
    Authentication authenticate(Authentication authentication)
            throws AuthenticationException;
    // Whether the Authentication class supports the Authentication of this Authentication
    boolean supports(Class authentication);
}
Copy the code

The next step is to traverse the providers collection in the ProviderManager and find the appropriate AuthenticationProvider to authenticate.

UserDetailsService UserDetails

There is only one simple method in the UserDetailsService interface

public interface UserDetailsService {
    // Find the corresponding UserDetails object based on the user name
    UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Copy the code

Perform certification process

Slowly analysis when running to UsernamePasswordAuthenticationFilter filters.

The first is to enter its parent class AbstractAuthenticationProcessingFilter doFilter () method

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)throws IOException, ServletException {...If yes, perform the following authentication; if no, skip
    if(! requiresAuthentication(request, response)) { chain.doFilter(request, response);return; }... Authentication authResult;try {
        / / the key methods to realize the Authentication logic and return to Authentication, by its subclasses UsernamePasswordAuthenticationFilter implementation
        authResult = attemptAuthentication(request, response);
        if (authResult == null) {
            // return immediately as subclass has indicated that it hasn't completed
            // authentication
            return;
        }
        sessionStrategy.onAuthentication(authResult, request, response);
    }
    catch (InternalAuthenticationServiceException failed) {
        // Authentication failed to be invoked
        unsuccessfulAuthentication(request, response, failed);
        return;
   }catch (AuthenticationException failed) {
        // Authentication failed to be invoked
        unsuccessfulAuthentication(request, response, failed);
        return;
    }
    // Authentication success
    if (continueChainBeforeSuccessfulAuthentication) {
        chain.doFilter(request, response);
    }
    // Authentication succeeded
    successfulAuthentication(request, response, chain, authResult);
}
Copy the code

Handling logic of authentication failure

protected void unsuccessfulAuthentication(HttpServletRequest request,HttpServletResponse response, AuthenticationException failed)throws IOException, ServletException { SecurityContextHolder.clearContext(); . rememberMeServices.loginFail(request, response);// This handler handles the failed interface jump and response logic
    failureHandler.onAuthenticationFailure(request, response, failed);
}
Copy the code

The default configuration of processing handler is SimpleUrlAuthenticationFailureHandler failure, can be customized.

public class SimpleUrlAuthenticationFailureHandler implements
        AuthenticationFailureHandler {...public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)throws IOException, ServletException {
        // If the URL fails to jump is not configured, it responds with an error
        if (defaultFailureUrl == null) {
            logger.debug("No failure URL set, sending 401 Unauthorized error");
            response.sendError(HttpStatus.UNAUTHORIZED.value(),
                HttpStatus.UNAUTHORIZED.getReasonPhrase());
        }else {
            / / otherwise
            // The cache is abnormal
            saveException(request, exception);
            // Redirect the abnormal page in different ways according to whether it is redirected or forwarded
            if (forwardToDestination) {
                logger.debug("Forwarding to " + defaultFailureUrl);
                request.getRequestDispatcher(defaultFailureUrl)
                        .forward(request, response);
            }else {
                logger.debug("Redirecting to "+ defaultFailureUrl); redirectStrategy.sendRedirect(request, response, defaultFailureUrl); }}}// If the cache is abnormal, forwarding is stored in request and redirection is stored in session
    protected final void saveException(HttpServletRequest request, AuthenticationException exception) {
        if (forwardToDestination) {        
	    request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
        }else {
            HttpSession session = request.getSession(false);
            if(session ! =null|| allowSessionCreation) { request.getSession().setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION,exception); }}}}Copy the code

Here is a small extension: use the system error processing handler, specify the URL of authentication failure to jump, in MVC corresponding URL method can get error information from request or session through key, feedback to the front end.

Authentication success processing logic

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
        throws IOException, ServletException {...// It is important to note that the return of Authentication is saved in the thread's 'SecurityContext'
    SecurityContextHolder.getContext().setAuthentication(authResult);
    rememberMeServices.loginSuccess(request, response, authResult);
    // Fire event
    if (this.eventPublisher ! =null) {
        eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
    }
    // This handler is used to complete the page jump
    successHandler.onAuthenticationSuccess(request, response, authResult);
}
Copy the code

Here is the success of the default configuration processing handler SavedRequestAwareAuthenticationSuccessHandler, code doesn’t do elaborate on the inside, it is to jump to the specified certification after successful interface, can be customized.

Identity Details

public class UsernamePasswordAuthenticationFilter extends
        AbstractAuthenticationProcessingFilter {...public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
    private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
    private boolean postOnly = true; .// Start the authentication logic
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if(postOnly && ! request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        String username = obtainUsername(request);
        String password = obtainPassword(request);
        if (username == null) {
            username = "";
        }
        if (password == null) {
            password = "";
        }
        username = username.trim();
        // Encapsulate a simple AuthenticationToken with the username and password submitted from the front end
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        // The actual authentication logic is handed over to Authenticate (..) The method is done, and then look down
        return this.getAuthenticationManager().authenticate(authRequest); }}Copy the code

The source breakpoint trace shows that the final parsing is done by the AuthenticationManager interface implementation class ProviderManager

public class ProviderManager implements AuthenticationManager.MessageSourceAware.InitializingBean {...privateList providers = Collections.emptyList(); .public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {...// Go through all the AuthenticationProviders to find the right one to complete the authentication
        for (AuthenticationProvider provider : getProviders()) {
            if(! provider.supports(toTest)) {continue; }...try {
                / / for a specific authentication logic, is used here to DaoAuthenticationProvider, remember to look down the specific logic
                result = provider.authenticate(authentication);
                if(result ! =null) {
                    copyDetails(authentication, result);
                    break; }}catch. }...throwlastException; }}Copy the code

DaoAuthenticationProvider

DaoAuthenticationProvider inherited from AbstractUserDetailsAuthenticationProvider implement AuthenticationProvider interface

public abstract class AbstractUserDetailsAuthenticationProvider implements
        AuthenticationProvider.InitializingBean.MessageSourceAware {...private UserDetailsChecker preAuthenticationChecks = new DefaultPreAuthenticationChecks();
    private UserDetailsChecker postAuthenticationChecks = newDefaultPostAuthenticationChecks(); .public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {...// Get the submitted user name
        String username = (authentication.getPrincipal() == null)?"NONE_PROVIDED" : authentication.getName();
        // Find the UserDetails from the cache based on the user name
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;
            try {
                // retrieveUser is passed if there is none in cache (..) The realization of the method to find the (see below DaoAuthenticationProvider)
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }catch. }try {
            // Check before comparison, for example, the account has some status information (whether locked, expired...)
            preAuthenticationChecks.check(user);
            / / subclass implementation than rules (see below the realization of the DaoAuthenticationProvider)
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }catch (AuthenticationException exception) {
            if (cacheWasUsed) {
                // 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);
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }else {
                throw exception;
            }
        }
        postAuthenticationChecks.check(user);
        if(! cacheWasUsed) {this.userCache.putUserInCache(user);
        }
        Object principalToReturn = user;
        if (forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }
        // Regenerate the detailed Authentication object based on some information about the final user and return it
        return createSuccessAuthentication(principalToReturn, authentication, user);
    }
    // The generation is dependent on the subclass implementation
    protected Authentication createSuccessAuthentication(Object principal,Authentication authentication, UserDetails user) {
        // Ensure we return the original credentials the user supplied,
        // so subsequent attempts are successful even with encoded passwords.
        // Also ensure we return the original getDetails(), so that future
        // authentication events after cache expiry contain the details
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
                principal, authentication.getCredentials(),
                authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        returnresult; }}Copy the code

Next we’ll look at the inside of the DaoAuthenticationProvider three important method, comparison method, ultimately return need than populated UserDetails object as well as the production method of Authentication.

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {...// Password comparison
    @SuppressWarnings("deprecation")
    protected void additionalAuthenticationChecks(UserDetails userDetails,UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(messages.getMessage(
 "AbstractUserDetailsAuthenticationProvider.badCredentials"."Bad credentials"));
        }
        String presentedPassword = authentication.getCredentials().toString();
        // Use PasswordEncoder to compare passwords. Note: This parameter can be customized
        if(! passwordEncoder.matches(presentedPassword, userDetails.getPassword())) { logger.debug("Authentication failed: password does not match stored value");
            throw new BadCredentialsException(messages.getMessage(
"AbstractUserDetailsAuthenticationProvider.badCredentials"."Bad credentials")); }}// Run the UserDetailsService command to obtain the UserDetails
    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
            // Run the UserDetailsService command to obtain the UserDetails
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        }
        catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw newInternalAuthenticationServiceException(ex.getMessage(), ex); }}// Generates the Authentication returned after the Authentication succeeds, and records the identity information
    @Override
    protected Authentication createSuccessAuthentication(Object principal,Authentication authentication, UserDetails user) {
        boolean upgradeEncoding = this.userDetailsPasswordService ! =null && this.passwordEncoder.upgradeEncoding(user.getPassword());
        if (upgradeEncoding) {
            String presentedPassword = authentication.getCredentials().toString();
            String newPassword = this.passwordEncoder.encode(presentedPassword);
            user = this.userDetailsPasswordService.updatePassword(user, newPassword);
        }
        return super.createSuccessAuthentication(principal, authentication, user); }}Copy the code