This is the most difficult part of Spring Security

Posted by olidenia on Mon, 03 Jan 2022 11:33:12 +0100

This article is excerpted from fat brother's latest Spring Security 5.6 X's Spring Security dry goods tutorial. The old version of the tutorial will be downloaded in January 1, 2022, and the students who need it will be required to reply to the "2021 start benefits" through the official account as soon as possible.

The hardest part about Spring Security is HttpSecurity Top level design. If you don't believe it, look HttpSecurity Definition of.

public final class HttpSecurity extends AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>
        implements SecurityBuilder<DefaultSecurityFilterChain>, HttpSecurityBuilder<HttpSecurity> {
    // ellipsis
}

If you can't feel it, let me show you the UML diagram:

Why is it so complicated? I saw it for the first time HttpSecurity When I doubt whether I am developing Java. Many years later, I understood this kind of design only after I deeply studied it. As a framework, especially the security framework, the configuration must be flexible enough to be applicable to more business scenarios. Spring Security adopts the architecture design of separating configuration from construction to ensure this.

Separation of configuration and build

Configuration only needs to collect configuration items, and construction only needs to build all configurations into target objects. Each does his own work and separates responsibilities. This approach can improve the maintainability and readability of the code. Spring Security uses interface isolation to highly abstract configuration and construction, improve flexibility and reduce complexity. But the system is still very large. In order to reduce the difficulty of learning, we need to decompose big problems into small problems and break them one by one. This learning method is very effective in learning some complex abstract theories.

SecurityBuilder

SecurityBuilder is an abstraction of a build. If you look at the class diagram above, it's too complex, but if you look at SecurityBuilder, it's very simple.

public interface SecurityBuilder<O> {
    // structure
 O build() throws Exception;
}

For one action, build the generalized target object O. Through the following set of abstract and concrete definitions, I think you should understand SecurityBuilder.

 // abstract
 SecurityBuilder -> O
 // specific
 HttpSecurity->DefaultSecurityFilterChain

❝ in a word, I do all the construction work.

AbstractSecurityBuilder

AbstractSecurityBuilder is an implementation of SecurityBuilder. The source code is as follows:

public abstract class AbstractSecurityBuilder<O> implements SecurityBuilder<O> {

 private AtomicBoolean building = new AtomicBoolean();

 private O object;

 @Override
 public final O build() throws Exception {
  if (this.building.compareAndSet(false, true)) {
      //The core logic of the build is provided by the hook method
   this.object = doBuild();
   return this.object;
  }
  throw new AlreadyBuiltException("This object has already been built");
 }
     // Get build target object
 public final O getObject() {
  if (!this.building.get()) {
   throw new IllegalStateException("This object has not been built");
  }
  return this.object;
 }

 /**
  *  Hook Method 
  */
 protected abstract O doBuild() throws Exception;

}

It restricts the call of the build method build() through the atomic class AtomicBoolean: each target object can only be built once to avoid inconsistency in security policies. The construction method also adds the final keyword, which cannot be overwritten! The built core logic is extended by the reserved hook method doBuild(), which is a common inheritance strategy. In addition, AbstractSecurityBuilder also provides a method getObject to get the built target object.

❝ in a word, I only do the construction work once.

HttpSecurityBuilder

public interface HttpSecurityBuilder<H extends HttpSecurityBuilder<H>>
  extends SecurityBuilder<DefaultSecurityFilterChain> {

     // Get configuration based on class name  
 <C extends SecurityConfigurer<DefaultSecurityFilterChain, H>> C getConfigurer(Class<C> clazz);
    // Remove configuration based on class name 
 <C extends SecurityConfigurer<DefaultSecurityFilterChain, H>> C removeConfigurer(Class<C> clazz);
    // Set an object to be shared so that it can be used in multiple securityconfigurers
 <C> void setSharedObject(Class<C> sharedType, C object);
    // Get a shared object
 <C> C getSharedObject(Class<C> sharedType);
    //  Add additional AuthenticationProvider
 H authenticationProvider(AuthenticationProvider authenticationProvider);
    //  Add additional UserDetailsService
 H userDetailsService(UserDetailsService userDetailsService) throws Exception;
    // Register a filter after the existing afterFilter class in the filter chain
 H addFilterAfter(Filter filter, Class<? extends Filter> afterFilter);
    // Register a filter in front of the existing beforeFilter class in the filter chain
 H addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter);
    // Register a filter in the filter chain. The filter must be in the built-in registry FilterOrderRegistration
 H addFilter(Filter filter);

}

HttpSecurityBuilder enhances the construction of DefaultSecurityFilterChain and adds some additional entries to its builder to obtain or manage configuration. See the notes above. In addition, the biggest function of this interface is to open up the relationship between construction and configuration, and you can operate the SecurityConfigurer to be discussed below.

❝ in a word, I only build DefaultSecurityFilterChain.

SecurityConfigurer

SecurityConfigurer is an abstraction of configuration. Configuration is only a means, and construction is the end. Therefore, configuration is the configuration of the build.

public interface SecurityConfigurer<O, B extends SecurityBuilder<O>> {
   // The builder initializes the configuration to be injected for subsequent information sharing
 void init(B builder) throws Exception;
   // Other necessary configurations  
 void configure(B builder) throws Exception;
}

SecurityConfigurer has two methods, both of which are very important. One is the init method, which you can think of as the logic of the SecurityBuilder constructor. If you want to execute some logic during SecurityBuilder initialization or share some variables in subsequent configuration, you can implement it in init method; The second method is configure, which configures some necessary properties for SecurityBuilder. Not finished here? The two methods have a clear order of execution. There may be multiple securityconfigurers in a build. The configure method will be executed one by one only after all inits are executed one by one. The relevant source code is marked in AbstractConfiguredSecurityBuilder:

  @Override
 protected final O doBuild() throws Exception {
  synchronized (this.configurers) {
   this.buildState = BuildState.INITIALIZING;
   beforeInit();
            // ① Execute all initialization methods
   init();
   this.buildState = BuildState.CONFIGURING;
   beforeConfigure();
            // ② Execute all the configure methods
   configure();
   this.buildState = BuildState.BUILDING;
   O result = performBuild();
   this.buildState = BuildState.BUILT;
   return result;
  }
 }

❝ in a word, I am responsible for configuring SecurityBuilder.

SecurityConfigurerAdapter

SecurityConfigurer has limitations in some scenarios. It cannot obtain the SecurityBuilder being configured, so you cannot further operate SecurityBuilder, and the scalability of the configuration will be greatly reduced. Therefore, SecurityConfigurerAdapter is introduced to extend SecurityConfigurer.

 
public abstract class SecurityConfigurerAdapter<O, B extends SecurityBuilder<O>> implements SecurityConfigurer<O, B> {

    private B securityBuilder;

    private CompositeObjectPostProcessor objectPostProcessor = new CompositeObjectPostProcessor();

    @Override
    public void init(B builder) throws Exception {
    }

    @Override
    public void configure(B builder) throws Exception {
    }
   // Gets the builder being configured to expose the builder's api
    public B and() {
        return getBuilder();
    }
 
    protected final B getBuilder() {
        Assert.state(this.securityBuilder != null, "securityBuilder cannot be null");
        return this.securityBuilder;
    }
    
    //  The composite object post processor is used to process objects to change the characteristics of some objects
    @SuppressWarnings("unchecked")
    protected <T> T postProcess(T object) {
        return (T) this.objectPostProcessor.postProcess(object);
    }
    // Add an ObjectPostProcessor to the compliance builder
    public void addObjectPostProcessor(ObjectPostProcessor<?> objectPostProcessor) {
        this.objectPostProcessor.addObjectPostProcessor(objectPostProcessor);
    }
    // Set the builder to be configured, so that multiple securityconfigureradapters can configure a SecurityBuilder
    public void setBuilder(B builder) {
        this.securityBuilder = builder;
    }
    // Other omissions
}

In this way, you can specify the SecurityBuilder, expose the SecurityBuilder, and adjust the SecurityBuilder anytime and anywhere, which greatly improves the flexibility.

Specifically, you can obtain SecurityBuilder through the and() method and operate on other configuration items of SecurityBuilder, such as switching between securityconfigureradapters in the above figure. In addition, ObjectPostProcessor is introduced to post operate some built-in objects that are not open. About object postprocessor, we will find a suitable scenario to explain it.

❝ in a word, configuring SecurityBuilder is nothing. Flexible adaptation is the flower.

AbstractHttpConfigurer

Not all configurations are useful. For some configurations, we want to have a closed entry function. For example, csrf function, csrfconfigurator controls the configuration of csrf. If only csrfconfigurator had a shutdown function. Therefore, abstracthttpconfigurator is derived from SecurityConfigurerAdapter to meet this requirement.

  public abstract class AbstractHttpConfigurer<T extends AbstractHttpConfigurer<T, B>, B extends HttpSecurityBuilder<B>>
        extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, B> {
    // Close current configuration
    @SuppressWarnings("unchecked")
    public B disable() {
        getBuilder().removeConfigurer(getClass());
        return getBuilder();
    }
    //  Enhanced the new ObjectPostProcessor method of the parent class 
    @SuppressWarnings("unchecked")
    public T withObjectPostProcessor(ObjectPostProcessor<?> objectPostProcessor) {
        addObjectPostProcessor(objectPostProcessor);
        return (T) this;
    }

}

There are many implementation classes of abstracthttpconfigurator, and most of the daily configuration items are controlled by the implementation class of abstracthttpconfigurator.

This class is one of the important entries for customized configuration. If you want to master Spring Security, you must master this class.

❝ in a word, I can "kill" myself.

AbstractConfiguredSecurityBuilder

We want multiple securityconfigurers to configure SecurityBuilder, such as form login, session management, csrf, etc. Make the configuration policy based. Therefore, AbstractConfiguredSecurityBuilder is introduced.

 public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer) throws Exception {
        // Inject objectPostProcessor into configurer
  configurer.addObjectPostProcessor(this.objectPostProcessor);
        // Set up the Builder for the SecurityConfigurerAdapter so that you can get it   
        // Note that it is different from other securityconfigurers
  configurer.setBuilder((B) this);
  add(configurer);
  return configurer;
 }
 
 public <C extends SecurityConfigurer<O, B>> C apply(C configurer) throws Exception {
  add(configurer);
  return configurer;
 }

All securityconfigurers can be adapted through the above two apply methods, and then the life cycle can be refined through doBuilder. You can do some necessary operations at each life cycle stage.

❝ in a word, I will adapt all configurations.

summary

We split the whole configuration building system of Spring Security, which will be simpler. Even so, it is unrealistic to understand this system by no means relying on one or two articles. However, it can also be seen that if your code wants to be highly flexible, you must layer and abstract each life cycle.