Automatically apply Customizer Beans to the Security DSL
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 convenient for internal frameworks
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
Hi @rwinch, can I take this to work?
Thanks for volunteering @franticticktick! I've assigned it to you
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 :)
@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);
}
}
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.
@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);
}
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.