We should support modular Spring Security Configuration through exposing Customizer Beans. This would allow users to easily apply global changes to Spring Security (apply to multiple Security FilterChains). It also allows separating their configurations if they would like. This is also convenient for internal frameworks.

See the documentation in Servlets and WebFlux for additional details

Comment From: jzheaux

Somewhat related, as far as a demonstration of one of the benefits I see of doing this: https://github.com/spring-projects/spring-security/issues/13057

Comment From: franticticktick

Hi @rwinch, can I take this to work?

Comment From: rwinch

Thanks for volunteering @franticticktick! I've assigned it to you

Comment From: franticticktick

Hey @rwinch @jzheaux . I did some research and have some questions. Let's say we want to define several customizers as beans:

    @Bean
    Customizer<RememberMeConfigurer> rememberMeCustomizer() {
        return rememberMeConfigurer -> {
            //...  
        };
    }

    @Bean
    Customizer<JeeConfigurer> jeeCustomizer() {
        return jeeConfigurer -> {
          //...  
        };
    }

We can get beans through this trick:

String[] beanNames = applicationContext.getBeanNamesForType(Customizer.class);
for (String bean: beanNames) {
   Customizer<?> customizer = applicationContext.getBean(bean, Customizer.class);
}

But in order to apply each customizer to HttpSecurity, we at least need to know the type of the customizer, that is, somehow extract the type of the generic. So we could do something like this:

if(customizer.getConfigurerType() instanceof JeeConfigurer) {
        httpSecurity.jee((Customizer<JeeConfigurer<HttpSecurity>>) customizer);
}

Due to type erasure, we cannot get the generic type in the parameter of the customize method. I don't see any easy way to solve this problem, but maybe I'm missing something. Maybe there is some other way? Sorry for bad English :)

Comment From: kse-music

@franticticktick Is this what you want?

       ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
        String[] beanNames = applicationContext.getBeanNamesForType(Customizer.class);
        for (String bean: beanNames) {
            Customizer<?> customizer = applicationContext.getBean(bean, Customizer.class);
            BeanDefinition bd = beanFactory.getMergedBeanDefinition(bean);
            Class<?> resolveClass = bd.getResolvableType().as(Customizer.class).getGeneric().resolve();
            if(resolveClass == RememberMeConfigurer.class) {
                http.rememberMe((Customizer<RememberMeConfigurer<HttpSecurity>>) customizer);
            } else if(resolveClass == JeeConfigurer.class) {
                http.jee((Customizer<JeeConfigurer<HttpSecurity>>) customizer);
            }
        }

Comment From: franticticktick

Hey @kse-music , thanks for the note. I considered this solution, but it’s not possible to simply access ConfigurableListableBeanFactory. There is a mistake in your code: to get a ConfigurableListableBeanFactory you need to do the following trick:

GenericApplicationContext ctx = (GenericApplicationContext) applicationContext;
ConfigurableListableBeanFactory beanFactory  = ctx.getBeanFactory();

How appropriate is such a strong type casting? This is a good question and it confuses me.

Comment From: kse-music

@franticticktick If so, maybe applying Customizer beans in add configuration is a small change

AbstractConfiguredSecurityBuilder

       @SuppressWarnings("unchecked")
    private <C extends SecurityConfigurer<O, B>> void add(C configurer) {
        Assert.notNull(configurer, "configurer cannot be null");
        Class<? extends SecurityConfigurer<O, B>> clazz = (Class<? extends SecurityConfigurer<O, B>>) configurer
            .getClass();
        applyCustomizerBean(configurer, clazz);
        synchronized (this.configurers) {
            if (this.buildState.isConfigured()) {
                throw new IllegalStateException("Cannot apply " + configurer + " to already built object");
            }
            List<SecurityConfigurer<O, B>> configs = null;
            if (this.allowConfigurersOfSameType) {
                configs = this.configurers.get(clazz);
            }
            configs = (configs != null) ? configs : new ArrayList<>(1);
            configs.add(configurer);
            this.configurers.put(clazz, configs);
            if (this.buildState.isInitializing()) {
                this.configurersAddedInInitializing.add(configurer);
            }
        }
    }

        @SuppressWarnings("unchecked")
    private <C extends SecurityConfigurer<O, B>> void applyCustomizerBean(C configurer, Class<?> clazz) {
        ApplicationContext context = getSharedObject(ApplicationContext.class);
        if (context == null) {
            return;
        }
        ResolvableType resolvableType = ResolvableType.forClassWithGenerics(Customizer.class, clazz);
        Customizer<C> customizer = (Customizer<C>) context.getBeanProvider(resolvableType).getIfUnique();
        if (customizer == null) {
            return;
        }
        customizer.customize(configurer);
    }

Comment From: rwinch

Thank you for the work on this @franticticktick and your help @kse-music!

I pushed the changes necessary for this. In order to resolve the correct generic types, the code looks at the method signatures of HttpSecurity and ServerHttpSecurity since the generic information is preserved in the methods and it ensures that the Beans that are used and the types that are available on the DSLs stay in sync.