spring security authentication source code tracking

spring security authentication source code tracking

​ Before tracking the authentication source code, let's explain the internal principle of security according to the official website, mainly based on a series of filter s. You can https://docs.spring.io/spring-security/site/docs/5.5.3/reference/html5/#servlet-hello, check the relevant documentation. If the English is not good, you can use google translation.

security principle description

​ In the above figure, the security filter is circled in the red box, and each http request will pass through each specified filter in the above figure. Request where:

DelegatingFilterProxy: it is mainly responsible for connecting the servlet container's life cycle with the Spring context, that is, all security filters are delegated to it for proxy.

FilterChainProxy: a special filter, which is packaged inside DelegatingFilterProxy. It proxies SecurityFilterChain

SecurityFilterChain:SecurityFilterChain determines which Spring security filters should be invoked for this request.

DelegatingFilterProxy

​ This is a filter, so there must be a doFilter method. We mainly look at the internal two methods. First, from the doFilter method:

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
      throws ServletException, IOException {

   // Lazily initialize the delegate if necessary.
   Filter delegateToUse = this.delegate;
   if (delegateToUse == null) {
      synchronized (this.delegateMonitor) {
         delegateToUse = this.delegate;
         if (delegateToUse == null) {
             // Get the Spring Web context
            WebApplicationContext wac = findWebApplicationContext();
            if (wac == null) {
               throw new IllegalStateException("No WebApplicationContext found: " +
                     "no ContextLoaderListener or DispatcherServlet registered?");
            }
             // Initialize delegate filter
            delegateToUse = initDelegate(wac);
         }
         this.delegate = delegateToUse;
      }
   }

   // Let the delegate perform the actual doFilter operation.
   invokeDelegate(delegateToUse, request, response, filterChain);
}

// Initialize delegate filter
protected Filter initDelegate(WebApplicationContext wac) throws ServletException {
    	// Among many filters, one is FilterChainProxy
		String targetBeanName = getTargetBeanName();
		Assert.state(targetBeanName != null, "No target bean name set");
		Filter delegate = wac.getBean(targetBeanName, Filter.class);
		if (isTargetFilterLifecycle()) {
			delegate.init(getFilterConfig());
		}
		return delegate;
	}

FilterChainProxy

​ It is also a filter. There must also be a doFilter method. Let's check this method

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
      throws IOException, ServletException {
    // Whether the current request has cleared the context, because every request will pass through this filter
   boolean clearContext = request.getAttribute(FILTER_APPLIED) == null;
   if (!clearContext) {
      doFilterInternal(request, response, chain);
      return;
   }
   try {
      request.setAttribute(FILTER_APPLIED, Boolean.TRUE);
       // Internal filter method, we see this method
      doFilterInternal(request, response, chain);
   }
   catch (RequestRejectedException ex) {
      this.requestRejectedHandler.handle((HttpServletRequest) request, (HttpServletResponse) response, ex);
   }
   finally {
      SecurityContextHolder.clearContext();
      request.removeAttribute(FILTER_APPLIED);
   }
}

private void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
    // Getting the firewall configuration is not important here
		FirewalledRequest firewallRequest = this.firewall.getFirewalledRequest((HttpServletRequest) request);
		HttpServletResponse firewallResponse = this.firewall.getFirewalledResponse((HttpServletResponse) response);
    // It can be seen here that FilterChainProxy has obtained a series of filter chains that the request will go through, including CsrfFilter, UsernamePasswordAuthenticationFilter and other filters, including the filters involved in SecurityFilterChain
		List<Filter> filters = getFilters(firewallRequest);
		if (filters == null || filters.size() == 0) {
			if (logger.isTraceEnabled()) {
				logger.trace(LogMessage.of(() -> "No security for " + requestLine(firewallRequest)));
			}
			firewallRequest.reset();
			chain.doFilter(firewallRequest, firewallResponse);
			return;
		}
		if (logger.isDebugEnabled()) {
			logger.debug(LogMessage.of(() -> "Securing " + requestLine(firewallRequest)));
		}
		VirtualFilterChain virtualFilterChain = new VirtualFilterChain(firewallRequest, chain, filters);
		virtualFilterChain.doFilter(firewallRequest, firewallResponse);
	}

Authentication source tracking

​ Back to authentication, you can find several methods of spring security authentication by searching the Internet. This time, we mainly track the third authentication method: database authentication, which is also the way we usually use. First, let's explain the knowledge points of database authentication. I have a general impression:

  1. UsernamePasswordAuthenticationFilter
  2. Implement the UserDetailsService interface and inject it into spring management

These three authentication methods are divided into:

1. Configure account password in xml

spring.security.user.name=user
spring.security.user.password=123456

2. Load the account and password into memory in the code

@Bean
public UserDetailsService userDetailsService() {
    UserDetails userDetails = User.withDefaultPasswordEncoder()
            .username("user")
            .password("password")
            .roles("USER")
            .build();
    return new InMemoryUserDetailsManager(userDetails);
}

3. Read the account number from the database for authentication verification

public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // Attempt to read the user from the database
        User user = userMapper.findByUserName(username);
        // If the user does not exist, an exception is thrown
        if (user == null) {
            throw new UsernameNotFoundException("user does not exist");
        }
        // Resolve roles in the form of database to the permission set of UserDetails
        // AuthorityUtils.commaSeparatedStringToAuthorityList is Spring Security
        //Provides a method for cutting comma separated permission set strings into a list of available permission objects
        // Of course, you can also implement it yourself, such as separating it with semicolons. Refer to generateAuthorities
        user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
        return user;
    }
}

​ In this example, we will have a custom WebSecurityConfig class, which defines which Url paths need to be intercepted and which permissions need to be accessed. At the same time, in this configuration, we will inject a password encoding class. By default, NoOpPasswordEncoder is not encrypted.

@EnableWebSecurity(debug = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .antMatchers("/admin/api/**").hasRole("ADMIN")
                .antMatchers("/user/api/**").hasRole("USER")
                .antMatchers("/app/api/**").permitAll()
                .antMatchers("/css/**", "/index").permitAll()
                .antMatchers("/user/**").hasRole("USER")
                .and()
                .formLogin()
                .loginPage("/login")
                .failureUrl("/login-error")
                .permitAll();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}

​ Instead of implementing the UserDetailsService interface, let's see how spring security implements authentication?

UsernamePasswordAuthenticationFilter

​ First, find the UsernamePasswordAuthenticationFilter class and find that it inherits the AbstractAuthenticationProcessingFilter class. Let's take a look at the AbstractAuthenticationProcessingFilter class and find that there are four main methods in this class, namely:

  1. doFilter(reqeust,response,chain): each filter has its own method, the most important one.
  2. attemptAuthentication(request,response); Is an abstract method, which is given to a specific implementation class to implement the authentication logic
  3. successfulAuthentication(request,response,chain,authenticationResult); The processing logic after successful authentication is realized through different strategies
  4. unsuccessfulAuthentication(request,resonse,failed); The processing logic after authentication failure is implemented through different strategies
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
      throws IOException, ServletException {
   if (!requiresAuthentication(request, response)) {
      chain.doFilter(request, response);
      return;
   }
   try {
       // The specific authentication method is an abstract method, which is given to the specific implementation class to implement the authentication logic
      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);
      }
       // Processing logic after successful authentication
      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
       // Processing logic after authentication failure
      unsuccessfulAuthentication(request, response, ex);
   }
}

​ Next, we look back at the UsernamePasswordAuthenticationFilter class and find that it mainly rewrites the attemptAuthentication(request,response) authentication method of the AbstractAuthenticationProcessingFilter class. The details are as follows:

@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());
   }
    // Gets the requested user name
   String username = obtainUsername(request);
   username = (username != null) ? username : "";
   username = username.trim();
    // Get the requested password
   String password = obtainPassword(request);
   password = (password != null) ? password : "";
    // Construct UsernamePasswordAuthenticationToken object with user name and password
   UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
   // Allow subclasses to set the "details" property
    // Set authentication object
   setDetails(request, authRequest);
   return this.getAuthenticationManager().authenticate(authRequest);
}

Then we run the program and access it directly http://localhost:8080/admin/api/hello After redirecting to the login page, arbitrarily enter the account and password, track the interruption point on the attemptAuthentication method of UsernamePasswordAuthenticationFilter class, and track to the retrieveuser of DaoAuthenticationProvider class (string username, usernamepasswordauthenticationtoken authentication) method,

​ In the AbstractUserDetailsAuthenticationProvider abstract, you need to specify an implementation class that implements the UserDetailsService interface. If we do not specify it, the default InMemoryUserDetailManager class will be loaded.

​ Because the second method mentioned above is adopted: the account and password are loaded into the memory in the code, and then we do not pre load the account and password we entered in the memory, so the authentication naturally fails.

UserDetailsService interface

​ You want to use the user-defined authentication method, that is, the third authentication method mentioned above: read the account from the database for authentication verification. Therefore, you need to implement the UserDetailsService interface yourself. Just now, in the process of tracking the code, we found that the AbstractUserDetailsAuthenticationProvider class needs an object that implements the UserDetailsService interface, so We will customize an implementation class that implements the interface and inject it into the spring container.

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // Attempt to read the user from the database
        User user = userMapper.findByUserName(username);
        // If the user does not exist, an exception is thrown
        if (user == null) {
            throw new UsernameNotFoundException("user does not exist");
        }
        // Resolve roles in the form of database to the permission set of UserDetails
        // AuthorityUtils.commaSeparatedStringToAuthorityList is Spring Security
        //Provides a method for cutting comma separated permission set strings into a list of available permission objects
        // Of course, you can also implement it yourself, such as separating it with semicolons. Refer to generateAuthorities
        user.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(user.getRoles()));
        return user;
    }
}

​ As shown in the figure above, we have rewritten the loadUserByUsername(String username) method of the UserDetailsService interface to implement our custom authentication logic. Then we restart the service and visit again http://localhost:8080/admin/api/hello , log in again and track the code,

At this time, we found that DaoAuthenticationProvider got our customized object from its userDetailsService, and then went through our customized authentication logic.

​ That's all for the certified source code tracking. The next step is the authorized source code tracking. The tracking article is short, but we still have some gains. come on.

Keywords: Spring Security

Added by fredriksk on Sun, 28 Nov 2021 15:16:45 +0200