spring-projects / spring-framework

Spring Framework
https://spring.io/projects/spring-framework
Apache License 2.0
56.43k stars 38.08k forks source link

No bean named 'mvcHandlerMappingIntrospector' available when start Spring MVC Context + DelatingFilterProxy [SPR-16301] #20848

Closed spring-projects-issues closed 6 years ago

spring-projects-issues commented 6 years ago

José María Sola Durán opened SPR-16301 and commented

I start Spring MVC Context with this WebApplicationInitializer:

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return Stream.of(AppConfig.class).toArray(size -> new Class<?>[size]);
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return Stream.of(SpringMVCRestConfig.class).toArray(size -> new Class<?>[size]);
    }

    @Override
    protected Filter[] getServletFilters() {
        return Stream.of(new DelegatingFilterProxy()).toArray(size -> new Filter[size]);
    }

    @Override
    protected String[] getServletMappings() {
        return new String[] { "/api/*" };
    }
}

Also, I start Spring Security Context. When the web app init context, I obtain the next exception:

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'mvcHandlerMappingIntrospector' available: A Bean named mvcHandlerMappingIntrospector of type org.springframework.web.servlet.handler.HandlerMappingIntrospector is required to use MvcRequestMatcher. Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext.
    at org.springframework.security.config.annotation.web.configurers.CorsConfigurer$MvcCorsFilter.getMvcCorsFilter(CorsConfigurer.java:113)
    at org.springframework.security.config.annotation.web.configurers.CorsConfigurer$MvcCorsFilter.access$000(CorsConfigurer.java:103)
    at org.springframework.security.config.annotation.web.configurers.CorsConfigurer.getCorsFilter(CorsConfigurer.java:97)
    at org.springframework.security.config.annotation.web.configurers.CorsConfigurer.configure(CorsConfigurer.java:66)
    at org.springframework.security.config.annotation.web.configurers.CorsConfigurer.configure(CorsConfigurer.java:39)
    at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.configure(AbstractConfiguredSecurityBuilder.java:384)
    at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.doBuild(AbstractConfiguredSecurityBuilder.java:330)
    at org.springframework.security.config.annotation.AbstractSecurityBuilder.build(AbstractSecurityBuilder.java:41)
    at org.springframework.security.config.annotation.web.builders.WebSecurity.performBuild(WebSecurity.java:290)
    at org.springframework.security.config.annotation.web.builders.WebSecurity.performBuild(WebSecurity.java:77)
    at org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder.doBuild(AbstractConfiguredSecurityBuilder.java:334)
    at org.springframework.security.config.annotation.AbstractSecurityBuilder.build(AbstractSecurityBuilder.java:41)
    at org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration.springSecurityFilterChain(WebSecurityConfiguration.java:104)
    at org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration$$EnhancerBySpringCGLIB$$f8f3949f.CGLIB$springSecurityFilterChain$0(<generated>)
    at org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration$$EnhancerBySpringCGLIB$$f8f3949f$$FastClassBySpringCGLIB$$75b450f2.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:228)
    at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:361)
    at org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration$$EnhancerBySpringCGLIB$$f8f3949f.springSecurityFilterChain(<generated>)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154)
    ... 26 more

I solve this error, if I add the next code in the root app context:

@Configuration
public class AppConfig {
[...]
    @Bean(name = "mvcHandlerMappingIntrospector")
    public HandlerMappingIntrospector mvcHandlerMappingIntrospector() {
        return new HandlerMappingIntrospector();
    }
}

Affects: 5.0.2

spring-projects-issues commented 6 years ago

José María Sola Durán commented

Only occurs when use @RestController in Spring MVC because I think that Spring auto-config the CORS support.

spring-projects-issues commented 6 years ago

Rossen Stoyanchev commented

You're probably using the mvcMatcher in Spring Security which aligns Spring Security with Spring MVC pattern matching configuration. However Spring Security is in the "parent", root context while Spring MVC config is in the "child", Servlet context. So Spring Security can't find the Spring MVC mapping configuration. This is why the error message says "Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext."

You can combine AppConfig and SpringMVCRestConfig and return both as your root config. That's all that should be necessary sine the MVC Java config includes an HandlerMappingIntrospector bean (this is in WebMvcConfigurationSupport.

For more details see the Spring Security docs https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#mvc-requestmatcher.

spring-projects-issues commented 6 years ago

José María Sola Durán commented

The Spring Security Context is declare in class SpringMVCRestConfig. This is the 'child' context of the parent root context 'AppConfig'.

Below, the first lines of SpringMVCRestConfig class:

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "com.example.customproject.api.mvc.*" })
@Import({ SpringSecurityConfig.class })
public class SpringMVCRestConfig implements WebMvcConfigurer {

[...]

This is de SpringSecurityConfig class:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(jsr250Enabled = true, prePostEnabled = true, securedEnabled = true, proxyTargetClass=true)
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService());
        auth.authenticationProvider(authenticationProvider());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().httpBasic().and().authorizeRequests().antMatchers(HttpMethod.OPTIONS).permitAll().
            anyRequest().authenticated().and().csrf()
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());

    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService());
        authenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
        return authenticationProvider;
    }

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Bean
    public UserDetailsService userDetailsService() {
        return new UserDetailsServiceImpl();
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder(11);
    }
}

In the previous versión of Spring (v4.2.x), yes it worked! Thank you for the supports!

spring-projects-issues commented 6 years ago

Rossen Stoyanchev commented

This is odd because you will see that WebMvcConfigurationSupport declares an mvcHandlerMappingIntrospector bean. Is it possible that something is causing SpringSecurityConfig to be loaded from the root context, e.g. via a component scan and since SpringSecurityConfig is marked with @Configuration? Also could you confirm the version of Spring Security?

/cc robwinch if you have any other ideas?

spring-projects-issues commented 6 years ago

José María Sola Durán commented

In the Spring Framework v4.3.x my @Configuration class SpringMVCRestConfig extends WebMvcConfigurerAdapter. However, in the Spring Framework v5.0.0.RELEASE the class WebMvcConfigurerAdapter is deprecated. Then, SpringMVCRestConfig implements the interface, with default methods, WebMvcConfigurer. I don't use WebMvcConfigurationSupport. Also, I try that SpringMVCRestConfig extends WebMvcConfigurationSupport, but it isn't work either.

The version of Spring Security is 5.0.0.RELEASE. The only way it works is add manually in the AppConfig class (root context) @Bean 'mvcHandlerMappingIntrospector'.

@Configuration
public class AppConfig {
[...]
    @Bean(name = "mvcHandlerMappingIntrospector")
    public HandlerMappingIntrospector mvcHandlerMappingIntrospector() {
        return new HandlerMappingIntrospector();
    }
}

I think that the Spring Security context start with 'child' context because, in first time Spring load root context, then create a HandlerMappingIntrospector bean, and after start 'child' DispacherServlet context and then, this one does find the HandlerMappingIntrospector bean.

spring-projects-issues commented 6 years ago

Rossen Stoyanchev commented

It's absolutely fine that SpringMVCRestConfig implements WebMvcConfigurer. The deprecation of WebMvcConfigurerAdapter has no impact whatsoever -- it simply takes advantage of Java 8 default methods on an interface. You do not need to extend WebMvcConfigurationSupport as you are using @EnableWebMvc which, if you look inside its declaration, you'll see that it imports WebMvcConfigurationSupport (or rather a sub-class of it). None of this explains the issue.

The only way it works is add manually in the AppConfig class (root context) @Bean 'mvcHandlerMappingIntrospector'.

This means that Spring Security configuration is somehow loaded in the root context. Can you check what's in your AppConfig.class? It must be pulling in Spring Security configuration somehow.

The only way it works is add manually in the AppConfig class (root context) @Bean 'mvcHandlerMappingIntrospector'.

Just a warning, please don't do that in production. In other words don't ignore the second part of the error message:

"A Bean named mvcHandlerMappingIntrospector of type org.springframework.web.servlet.handler.HandlerMappingIntrospector is required to use MvcRequestMatcher. Please ensure Spring Security & Spring MVC are configured in a shared ApplicationContext."

spring-projects-issues commented 6 years ago

José María Sola Durán commented

The fact that the security context starts together with "SpringMVCRestConfig" is that if in AppConfig the HandlerMappingIntrospector bean is added manually, the security context does not throw any exception when starting. In the console log of Tomcat, the trace reflects the following: 1.) Start the root context (A HandlerMappingIntrospector bean has just been instantiated). 2.) Start the context of DispacherServlet, and with it, the security context of Spring Framework

If the HandlerMappingIntrospector class is not added to AppConfig, then: 1.) Start the root context (A HandlerMappingIntrospector is not instantiated) 2.) When starting the context of DispacherServlet, and with it, the security context of Spring Framework, the exception that I indicated in previous comments is throwing.

The same code with this versions working perfectly: 1.) Spring Framework --> 4.3.13.RELEASE 2.) Spring Security --> 4.2.3.RELEASE

I think that in Spring Beans artifact (spring-beans-5.0.x.RELEASE.jar) something has changed and then, the order to load the beans is distinct.

Thank you! Happy New Year!

spring-projects-issues commented 6 years ago

Rossen Stoyanchev commented

SpringMVCRestConfig has @EnableWebMvc which imports DelegatingWebMvcConfiguration which automatically declares a HandlerMappingIntrospector bean (see declaration). The SpringSecrityConfig which is imported from SpringMVCRestConfig should be able to find that bean just fine. All of this has no impact on the "root" context AppConfig.

The only way to explain the error coming from AppConfig is that you are importing Spring Security configuration from AppConfig (which means you've configured Spring Security in both root and child contexts!) so my advice still stands:

You can combine AppConfig and SpringMVCRestConfig and return both as your root config. That's all that should be necessary sine the MVC Java config includes an HandlerMappingIntrospector bean (this is in WebMvcConfigurationSupport.

For more details see the Spring Security docs https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#mvc-requestmatcher.

spring-projects-issues commented 6 years ago

Rossen Stoyanchev commented

Resolving for now as we've discussed this enough. You can comment further but if you wish to re-open please provide a sample app to demonstrate the issue.

spring-projects-issues commented 6 years ago

José María Sola Durán commented

You can download an example code in this link: https://we.tl/1V2GCUuqyJ

Please, you must download code as soon as possible because the will expire. With this code, you can reproduce the error.

I think that the bug is in AbstractAnnotationConfigDispatcherServletInitializer. If you change the class net.ddns.jmsola.customproject.api.WebAppInitializer with the code below it's working!


package net.ddns.jmsola.customproject.api;

import java.util.EnumSet;

import javax.servlet.DispatcherType;
import javax.servlet.FilterRegistration;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration;

import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.ContextLoaderListener;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.DelegatingFilterProxy;
import org.springframework.web.servlet.DispatcherServlet;

import net.ddns.jmsola.customproject.api.config.AppConfig;
import net.ddns.jmsola.customproject.api.config.SpringMVCRestConfig;

public class WebAppInitializer implements WebApplicationInitializer {

    private static final String SPRING_SECURITY_FILTER_CHAIN = "springSecurityFilterChain";

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {

        AnnotationConfigWebApplicationContext rootAppContext = new AnnotationConfigWebApplicationContext();
        rootAppContext.register(AppConfig.class);
        servletContext.addListener(new ContextLoaderListener(rootAppContext));

        AnnotationConfigWebApplicationContext mvcContext = new AnnotationConfigWebApplicationContext();
        rootAppContext.register(SpringMVCRestConfig.class);

        DispatcherServlet dispacherServlet = servletContext.createServlet(DispatcherServlet.class);
        dispacherServlet.setApplicationContext(mvcContext);

        ServletRegistration.Dynamic registrationDispacherServlet = servletContext.addServlet("dispacherServlet",
                dispacherServlet);
        registrationDispacherServlet.setLoadOnStartup(1);
        registrationDispacherServlet.addMapping("/api/*");

        DelegatingFilterProxy springSecurityFilterChain = new DelegatingFilterProxy(SPRING_SECURITY_FILTER_CHAIN);
        FilterRegistration.Dynamic registration = servletContext.addFilter(SPRING_SECURITY_FILTER_CHAIN,
                springSecurityFilterChain);
        if (registration == null) {
            throw new IllegalStateException("Duplicate Filter registration for '" + SPRING_SECURITY_FILTER_CHAIN
                    + "'. Check to ensure the Filter is only configured once.");
        }
        EnumSet<DispatcherType> dispatcherTypes = EnumSet.of(DispatcherType.REQUEST, DispatcherType.ERROR);
        registration.addMappingForUrlPatterns(dispatcherTypes, false, "/*");

    }
}
spring-projects-issues commented 6 years ago

Rossen Stoyanchev commented

Thanks for the sample.

Just as I suspected the following in your AppConfig in turn loads SpringSecurityConfig (see Javadoc of @Configuration with component scanning):

@ComponentScan(basePackages = { "net.ddns.jmsola.customproject.api.security.*" })

I moved SpringSecurityConfig into the config package next to AppConfig (because it's already explicitly imported from the web config). I then changed WebApplicationInitializer to load all configuration from the root context:

    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[] {AppConfig.class, SpringMVCRestConfig.class};
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return null;
    }

It now works as expected.

Why did I have to make such changes when it was working fine before? Your application wasn't configured correctly before, you just didn't get any error messages. That's the difference. You were loading Spring Security configuration in two places -- both root and child context, but only the one from the root context is really used and plugged in through the DelegatingProxyFilter. In addition the mvcMatcher in Spring Security couldn't have worked correctly either.

spring-projects-issues commented 6 years ago

Eric Deandrea commented

Sorry I'm coming late to this. I've been working on a project where I just moved from Spring Boot 1.5.9 to 1.5.10 (which also upgraded spring-webmvc from 4.3.13 to 4.3.14 and spring-security from 4.2.3 to 4.2.4. Unlike the examples above I don't have any initializers or anything like that, nor do I have any classes that use @EnableWebMvc or that implement WebMvcConfigurer.

I'm actually seeing this issue in some unit tests where I am testing my security configuration. I am building a library that is used by applications. Upgrading our reference application from 1.5.9 to 1.5.10 seems to be fine, but our unit tests for our library are now failing.

My test class simply uses

@RunWith(SpringRunner.class)
@ActiveProfiles("header-user-filter-tests")
@WebAppConfiguration
@SpringBootTest(classes = { MyTestClass.Config.class })

at the class level and then a nested inner Config class:

@Configuration
@ImportAutoConfiguration({ SomeCustomAutoConfigurationClass.class, SomeCustomSecurityAutoConfigurationClass.class, org.springframework.boot.autoconfigure.security.SecurityAutoConfiguration.class })
@Profile("header-user-filter-tests")
public static class Config {
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetailsService userDetailsService = Mockito.mock(UserDetailsService.class);

        // @formatter:off
        BDDMockito
            .given(userDetailsService.loadUserByUsername(BDDMockito.anyString()))
            .willReturn(User.withUsername("user").password("n/a").authorities(new GrantedAuthority[0]).build());
        // @formatter:off

        return userDetailsService;
    }
}

The SomeCustomAutoConfigurationClass just does a component scan of some of our library classes and also has an @ConfigurationProperties annotation.

The SomeCustomSecurityAutoConfigurationClass class has the @EnableWebSecurity annotation as well as it extends WebSecurityConfigurerAdapter.

I've tried a number of different things, including manually defining the mvcHandlerMappingIntrospector bean in my configuration but nothing seems to work.

Any help would be appreciated!

spring-projects-issues commented 6 years ago

Rossen Stoyanchev commented

I'm not sure what would be the best approach in this (test) scenario but let's not make this ticket, which is a related but clearly different scenario, any longer than it already is. Please create a ticket under Spring Security.

spring-projects-issues commented 6 years ago

Eric Deandrea commented

Will do - https://github.com/spring-projects/spring-security/issues/4995.