flow icon indicating copy to clipboard operation
flow copied to clipboard

GraalVM native image and VaadinSecurityFilterChainBean

Open simasch opened this issue 9 months ago • 1 comments

I tried to build a native image of my Vaadin Flow app (24.4.0.beta3)

The build works fine, but if I start the app, I get this exception:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration': Unsatisfied dependency expressed through method 'setFilterChains' parameter 0: Error creating bean with name 'VaadinSecurityFilterChainBean': Instantiation of supplied bean failed
        at org.springframework.beans.factory.aot.AutowiredMethodArgumentsResolver.resolveArguments(AutowiredMethodArgumentsResolver.java:187) ~[na:na]
        at org.springframework.beans.factory.aot.AutowiredMethodArgumentsResolver.resolve(AutowiredMethodArgumentsResolver.java:137) ~[na:na]
        at org.springframework.beans.factory.aot.AutowiredMethodArgumentsResolver.resolve(AutowiredMethodArgumentsResolver.java:123) ~[na:na]
        at org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration__Autowiring.apply(WebSecurityConfiguration__Autowiring.java:24) ~[na:na]
        at org.springframework.beans.factory.support.InstanceSupplier$1.get(InstanceSupplier.java:83) ~[na:na]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.obtainInstanceFromSupplier(DefaultListableBeanFactory.java:949) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1217) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1161) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:975) ~[jtaf4:6.1.6]
        at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:962) ~[jtaf4:6.1.6]
        at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:624) ~[jtaf4:6.1.6]
        at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[na:na]
        at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:754) ~[jtaf4:3.2.5]
        at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:456) ~[jtaf4:3.2.5]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:334) ~[jtaf4:3.2.5]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1354) ~[jtaf4:3.2.5]
        at org.springframework.boot.SpringApplication.run(SpringApplication.java:1343) ~[jtaf4:3.2.5]
        at ch.jtaf.Jtaf4Application.main(Jtaf4Application.java:20) ~[jtaf4:na]
        at java.base@22/java.lang.invoke.LambdaForm$DMH/sa346b79c.invokeStaticInit(LambdaForm$DMH) ~[na:na]
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'VaadinSecurityFilterChainBean': Instantiation of supplied bean failed
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1223) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1161) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:562) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:522) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.config.DependencyDescriptor.resolveCandidate(DependencyDescriptor.java:254) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.addCandidateEntry(DefaultListableBeanFactory.java:1689) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:1653) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveMultipleBeanCollection(DefaultListableBeanFactory.java:1543) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveMultipleBeans(DefaultListableBeanFactory.java:1511) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1392) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1353) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.aot.AutowiredMethodArgumentsResolver.resolveArguments(AutowiredMethodArgumentsResolver.java:179) ~[na:na]
        ... 24 common frames omitted
Caused by: java.lang.IllegalStateException: Could not locate field 'rolePrefix' on class class java.lang.Object
        at org.springframework.security.util.FieldUtils.getField(FieldUtils.java:53) ~[na:na]
        at org.springframework.security.util.FieldUtils.getField(FieldUtils.java:51) ~[na:na]
        at org.springframework.security.util.FieldUtils.getField(FieldUtils.java:51) ~[na:na]
        at com.vaadin.flow.spring.security.VaadinRolePrefixHolder.resetRolePrefix(VaadinRolePrefixHolder.java:127) ~[jtaf4:na]
        at com.vaadin.flow.spring.security.VaadinRolePrefixHolder.resetRolePrefix(VaadinRolePrefixHolder.java:114) ~[jtaf4:na]
        at java.base@22/java.util.Optional.ifPresent(Optional.java:178) ~[jtaf4:na]
        at com.vaadin.flow.spring.security.VaadinRolePrefixHolder.resetRolePrefix(VaadinRolePrefixHolder.java:109) ~[jtaf4:na]
        at com.vaadin.flow.spring.security.VaadinWebSecurity.lambda$filterChain$1(VaadinWebSecurity.java:155) ~[jtaf4:na]
        at java.base@22/java.util.Optional.ifPresent(Optional.java:178) ~[jtaf4:na]
        at com.vaadin.flow.spring.security.VaadinWebSecurity.filterChain(VaadinWebSecurity.java:154) ~[jtaf4:na]
        at ch.jtaf.configuration.security.SecurityConfiguration$$SpringCGLIB$$0.CGLIB$filterChain$3(<generated>) ~[jtaf4:na]
        at ch.jtaf.configuration.security.SecurityConfiguration$$SpringCGLIB$$FastClass$$0.invoke(<generated>) ~[jtaf4:na]
        at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:258) ~[jtaf4:6.1.6]
        at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331) ~[na:na]
        at ch.jtaf.configuration.security.SecurityConfiguration$$SpringCGLIB$$0.filterChain(<generated>) ~[jtaf4:na]
        at com.vaadin.flow.spring.security.VaadinWebSecurity__BeanDefinitions.lambda$getVaadinSecurityFilterChainBeanInstanceSupplier$0(VaadinWebSecurity__BeanDefinitions.java:22) ~[na:na]
        at org.springframework.util.function.ThrowingBiFunction.apply(ThrowingBiFunction.java:68) ~[jtaf4:6.1.6]
        at org.springframework.util.function.ThrowingBiFunction.apply(ThrowingBiFunction.java:54) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.aot.BeanInstanceSupplier.lambda$get$2(BeanInstanceSupplier.java:206) ~[na:na]
        at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:58) ~[jtaf4:6.1.6]
        at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:46) ~[jtaf4:6.1.6]
        at org.springframework.beans.factory.aot.BeanInstanceSupplier.invokeBeanSupplier(BeanInstanceSupplier.java:218) ~[na:na]
        at org.springframework.beans.factory.aot.BeanInstanceSupplier.get(BeanInstanceSupplier.java:206) ~[na:na]

Comment by @marcushellberg: It seems like we need to add some more runtime hints for the AOT compiler

simasch avatar May 17 '24 09:05 simasch

This particular issue should be fixed if we add

hints.reflection().registerType(VaadinSecurityFilterChainBean.class, MemberCategory.values());

to https://github.com/vaadin/flow/blob/main/vaadin-spring/src/main/java/com/vaadin/flow/spring/springnative/VaadinHintsRegistrar.java

But we need to check if there are other new classes or resources that need to be added as well.

marcushellberg avatar May 17 '24 09:05 marcushellberg

I'm not sure if the suggested fix could simply be applied, as VaadinSecurityFilterChainBean is actually a bean name for a SecurityFilterChain instance, and its instantiation is failing due to reflection error that happens when VaadinRolePrefixHolder:: resetRolePrefix is called.

The fact that the following exception is occurring is a bit weird:

Caused by: java.lang.IllegalStateException: Could not locate field 'rolePrefix' on class class java.lang.Object
        at org.springframework.security.util.FieldUtils.getField(FieldUtils.java:53) ~[na:na]
        at org.springframework.security.util.FieldUtils.getField(FieldUtils.java:51) ~[na:na]
        at org.springframework.security.util.FieldUtils.getField(FieldUtils.java:51) ~[na:na]
        at com.vaadin.flow.spring.security.VaadinRolePrefixHolder.resetRolePrefix(VaadinRolePrefixHolder.java:127) ~[jtaf4:na]

The following code is clearly filtering filters that are not of type SecurityContextHolderAwareRequestFilter:

public void resetRolePrefix(DefaultSecurityFilterChain defaultSecurityFilterChain) {
    defaultSecurityFilterChain.getFilters().stream()
            .filter(filter -> SecurityContextHolderAwareRequestFilter.class
                     .isAssignableFrom(filter.getClass()))
            .map(SecurityContextHolderAwareRequestFilter.class::cast)
            .findFirst().ifPresent(this::resetRolePrefix);
}

So it is not clear why FieldUtils.getField(type, "rolePrefix") is being called on type java.lan.Object (according to the error log). The implementation of getField shows what leads to that error log:

public static Field getField(Class<?> clazz, String fieldName) throws IllegalStateException {
    Assert.notNull(clazz, "Class required");
    Assert.hasText(fieldName, "Field name required");
    try {
    	return clazz.getDeclaredField(fieldName);
    }
    catch (NoSuchFieldException ex) {
	// Try superclass
	if (clazz.getSuperclass() != null) {
    	    return getField(clazz.getSuperclass(), fieldName);
        }
        throw new IllegalStateException("Could not locate field '" + fieldName + "' on class " + clazz);
    }
}

taefi avatar May 20 '24 11:05 taefi

The resetRolePrefix(Object source, Class<?> type) gets called potentially for two object types: SecurityContextHolderAwareRequestWrapper and SecurityContextHolderAwareRequestFilter. So we need to register reflection hints for these classes to make sure that fields information is available for the native image.

mcollovati avatar May 20 '24 18:05 mcollovati

It's strange that we need to add hints for Spring Security classes. Sounds like we should create an issue on their end for them for a proper fix. But good in the short term.

marcushellberg avatar May 20 '24 19:05 marcushellberg

Maybe not so strange. I suppose Spring never needs reflection on those classes

mcollovati avatar May 20 '24 19:05 mcollovati

I chatted with them on Twitter and they said we can create an issue as they normally would have the hints already added in the framework.

But maybe it's that we use Spring Security in a non-standard way in this instance? I couldn't see the issue with a project from start.spring.io

marcushellberg avatar May 20 '24 21:05 marcushellberg

My opinion is that we need the hint on our side because we are accessing a field on the spring classes by using reflection. I suppose Spring itself does not need to use reflection on those classes, so they do not need to add hints by default.

mcollovati avatar May 20 '24 21:05 mcollovati