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.