It is reasonable to say that the new objects and the container are not related, but in the Spring Security framework also new a lot of objects out, but also can be managed by the container. How is this done?

In the Spring Security authorization configuration code of some projects, you will find a line of code like this.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setAccessDecisionManager(customUrlDecisionManager);
                        object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                        return object;
                    }
                })
                .and()
                ...
    }
}

What exactly is the meaning of withObjectPostProcessor here?

1. The role of ObjectPostProcessor

Let’s take a look at what ObjectPostProcessor really does, starting with the definition of this interface.

There is only one postProcess method in the interface.

Let’s look at the inheritance relationship of ObjectPostProcessor.

inheritance relationship of ObjectPostProcessor

Two more important implementation classes, of which AutowireBeanFactoryObjectPostProcessor is worth talking about, take a look at the definition of AutowireBeanFactoryObjectPostProcessor.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
final class AutowireBeanFactoryObjectPostProcessor
        implements ObjectPostProcessor<Object>, DisposableBean, SmartInitializingSingleton {
    AutowireBeanFactoryObjectPostProcessor(
            AutowireCapableBeanFactory autowireBeanFactory) {
        this.autowireBeanFactory = autowireBeanFactory;
    }
    @SuppressWarnings("unchecked")
    public <T> T postProcess(T object) {
        if (object == null) {
            return null;
        }
        T result = null;
        try {
            result = (T) this.autowireBeanFactory.initializeBean(object,
                    object.toString());
        }
        catch (RuntimeException e) {
            Class<?> type = object.getClass();
            throw new RuntimeException(
                    "Could not postProcess " + object + " of type " + type, e);
        }
        this.autowireBeanFactory.autowireBean(object);
        if (result instanceof DisposableBean) {
            this.disposableBeans.add((DisposableBean) result);
        }
        if (result instanceof SmartInitializingSingleton) {
            this.smartSingletons.add((SmartInitializingSingleton) result);
        }
        return result;
    }
}

The source code of AutowireBeanFactoryObjectPostProcessor is well understood.

  1. First of all, when building the AutowireBeanFactoryObjectPostProcessor object, an AutowireCapableBeanFactory object is passed in, and if you have seen the Spring source code, you know that AutowireCapableBeanFactory can help us manually register an instance into the Spring container.
  2. In the postProcess method, is the specific registration logic, are very simple, I will not repeat.

On a macro level, AutowireCapableBeanFactory provides the following capabilities.

  1. assembling properties for objects that have been instantiated with property objects that are managed by Spring.
  2. instantiate a bean and automatically assemble it, these assembled property objects are managed by Spring, but the instantiated bean can be not managed by Spring (this is especially important). So this interface provides functionality is automatically assembled bean related.

(The original object for auto-assembly can be outside of Spring’s IOC container, but the member that needs to be injected must be a bean governed by the Spring container)

This interface is primarily intended for applications outside of the framework that do not host beans to Spring. By exposing this functionality, applications outside of the Spring Framework can have the ability to auto-assemble (which this interface gives it).

You can use this interface to integrate with other frameworks. Bind and populate (inject) instances that are not managed by Spring lifecycle and already exist. Like the integration of WebWork Actions and Tapestry Page is very practical. General application developers will not use this interface , so the appearance of implementation classes like ApplicationContext will not implement this interface , but provides getAutowireCapableBeanFactory() method allows you to take this tool to do what you need .

As you can see, the main role of the ObjectPostProcessor is to manually register the instance into the Spring container (and let the instance go through the bean’s lifecycle).

Normally, the beans in our project are injected into the Spring container through automatic scanning, however, in the Spring Security framework, there is a large amount of code that is not injected into the Spring container through automatic scanning, but directly new out, which is intended to simplify the project configuration.

These directly new out of the code, if you want to be managed by the Spring container what to do? Then you have to use ObjectPostProcessor.

2. Use cases in the framework

The next few examples of the framework directly new object, we look at the role of ObjectPostProcessor.

HttpSecurity initialization code (WebSecurityConfigurerAdapter#getHttp).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
protected final HttpSecurity getHttp() throws Exception {
    if (http != null) {
        return http;
    }
    ...
    ...
    http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
            sharedObjects);
    ...
    ...
}

WebSecurity initialization code (WebSecurityConfiguration#setFilterChainProxySecurityConfigurer).

1
2
3
4
5
6
7
8
9
public void setFilterChainProxySecurityConfigurer(
        ObjectPostProcessor<Object> objectPostProcessor,
        @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
        throws Exception {
    webSecurity = objectPostProcessor
            .postProcess(new WebSecurity(objectPostProcessor));
    ...
    ...
}

Manual assembly is seen everywhere in the Spring Security framework source code. all filters in the filter chain in Spring Security are configured through the corresponding xxxConfigure, which all inherit from the SecurityConfigurerAdapter. as follows.

Spring Security filter chain

The configure methods of these xxxConfigure invariably have their respective configured managers registered in the Spring container and go through the bean’s lifecycle. For example, the AbstractAuthenticationFilterConfigurer#configure method.

1
2
3
4
5
6
public void configure(B http) throws Exception {
    ...
    ...
    F filter = postProcess(authFilter);
    http.addFilter(filter);
}

The other xxxConfigurer#configure methods all have similar implementations.

3. Why is this

Isn’t it better to register the bean directly to the Spring container via auto-scan? Why do we have to do this?

In Spring Security, the framework itself uses a lot of Java configuration and does not expose all the properties of the object, which is intended to simplify the configuration. However, one of the problems this brings is the need to manually register the bean into the Spring container, ObjectPostProcessor is to solve the problem.

Once a bean is registered in the Spring container, we have a way to enhance the functionality of a bean or modify the properties of a bean.

For example, the authorization configuration code mentioned at the beginning.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
                    @Override
                    public <O extends FilterSecurityInterceptor> O postProcess(O object) {
                        object.setAccessDecisionManager(customUrlDecisionManager);
                        object.setSecurityMetadataSource(customFilterInvocationSecurityMetadataSource);
                        return object;
                    }
                })
                .and()
                ...
    }
}

Permission management itself is controlled by the FilterSecurityInterceptor. The default FilterSecurityInterceptor has already been created and I have no way to modify its properties, so what can I do? We can use the withObjectPostProcessor method to modify the relevant properties of the FilterSecurityInterceptor.

One of the reasons why the above configuration works is because the FilterSecurityInterceptor retraces the postProcess method after it is successfully created. The property modification can be achieved here by overriding the postProcess method, and we can look at the method for configuring the FilterSecurityInterceptor (AbstractInterceptUrlConfigurer#configure).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
abstract class AbstractInterceptUrlConfigurer<C extends AbstractInterceptUrlConfigurer<C, H>, H extends HttpSecurityBuilder<H>>
        extends AbstractHttpConfigurer<C, H> {
    @Override
    public void configure(H http) throws Exception {
        FilterInvocationSecurityMetadataSource metadataSource = createMetadataSource(http);
        if (metadataSource == null) {
            return;
        }
        FilterSecurityInterceptor securityInterceptor = createFilterSecurityInterceptor(
                http, metadataSource, http.getSharedObject(AuthenticationManager.class));
        if (filterSecurityInterceptorOncePerRequest != null) {
            securityInterceptor
                    .setObserveOncePerRequest(filterSecurityInterceptorOncePerRequest);
        }
        securityInterceptor = postProcess(securityInterceptor);
        http.addFilter(securityInterceptor);
        http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor);
    }
}

As you can see, after the securityInterceptor object is created successfully, it will still go to the postProcess method.

Once you understand the above code, I’ll give you another example that you should be able to understand in a second.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("admin")
                ...
                .and()
                .formLogin()
                .withObjectPostProcessor(new ObjectPostProcessor<UsernamePasswordAuthenticationFilter>() {
                    @Override
                    public <O extends UsernamePasswordAuthenticationFilter> O postProcess(O object) {
                        object.setUsernameParameter("name");
                        return object;
                    }
                })
                ...
    }
}

Here, I take the configured UsernamePasswordAuthenticationFilter out again and modify the username key (normally, you don’t need to go through all this trouble to modify the username key, this is mainly to show you the effect of ObjectPostProcessor). After the modification, when the user logs in, the username is not username but name.

4. Summary

Well, if you have mastered the above usage, in the future in Spring Security, if you want to modify the properties of an object, but do not know where to start, then you may want to try withObjectPostProcessor!

Reference http://www.enmalvi.com/2021/06/23/springsecurity-objectpostprocessor/