Hi,

I upgraded from 5.7.x to 5.8.0 and replaced the deprecated antMatchers(String...) with requestMatchers(String...) as suggested by the Javadoc, but now i'm getting the following error on application startup (note the weird exception message "No bean named 'A Bean named..."):

org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named '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.' available
    at org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry.createMvcMatchers(AbstractRequestMatcherRegistry.java:187)
    at org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry.requestMatchers(AbstractRequestMatcherRegistry.java:302)
    at org.springframework.security.config.annotation.web.AbstractRequestMatcherRegistry.requestMatchers(AbstractRequestMatcherRegistry.java:329)
    at com.example.spring.security.SecurityConfig.filterChain(SecurityConfig.java:36)
    at com.example.spring.security.SecurityConfig$$EnhancerBySpringCGLIB$$d314e6bc.CGLIB$filterChain$0(<generated>)
    at com.example.spring.security.SecurityConfig$$EnhancerBySpringCGLIB$$d314e6bc$$FastClassBySpringCGLIB$$f1e371fd.invoke(<generated>)
    at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:244)
    at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331)
    at com.example.spring.security.SecurityConfig$$EnhancerBySpringCGLIB$$d314e6bc.filterChain(<generated>)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.springframework.beans.factory.support.SimpleInstantiationStrategy.instantiate(SimpleInstantiationStrategy.java:154)

SpringWebAppInitializer.java

public class SpringWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

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

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { ServletConfig.class };
    }

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

    public static class SecurityWebAppInitializer extends AbstractSecurityWebApplicationInitializer {}
}

RootConfig.java

@Configuration
@ComponentScan
public class RootConfig {}

SecurityConfig.java

@Configuration
@ComponentScan
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig implements ApplicationContextAware {

    private ApplicationContext context;

    @Override
    public void setApplicationContext(ApplicationContext context) {
        this.context = context;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.jee().authenticatedUserDetailsService(token -> context.getBean(UserDetailsService.class).loadUserDetails(token)).mappableAuthorities(UserRole.stream().map(UserRole::getName).collect(Collectors.toSet())) // lambda required for warm context refresh
            .and().authorizeHttpRequests().requestMatchers("/admin/**").hasAuthority(RoleNames.ROLE_ADMIN) // this doesn't work but antMatchers did 
            .and().csrf().disable().logout().logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.NO_CONTENT));
        return http.build();
    }
}

ServletConfig.java

@Configuration
@ComponentScan
@EnableWebMvc
@EnableAspectJAutoProxy
public class ServletConfig implements WebMvcConfigurer {}

I noted that if I return null from getRootConfigClasses() and move both RootConfig.class & SecurityConfig.class to the array returned by getServletConfigClasses(), the application starts, but I don't understand the reason behind this, and I would like to keep separated the beans of root and servlet contexts.

Note also that moving only SecurityConfig.class from the root config to the servlet config produces another exception on application startup:

org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'springSecurityFilterChain' available
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:874)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1358)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:309)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:213)
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1160)
    at org.springframework.web.filter.DelegatingFilterProxy.initDelegate(DelegatingFilterProxy.java:334)
    at org.springframework.web.filter.DelegatingFilterProxy.initFilterBean(DelegatingFilterProxy.java:239)
    at org.springframework.web.filter.GenericFilterBean.init(GenericFilterBean.java:239)
    at org.apache.catalina.core.ApplicationFilterConfig.initFilter(ApplicationFilterConfig.java:272)
    at org.apache.catalina.core.ApplicationFilterConfig.<init>(ApplicationFilterConfig.java:106)
    at org.apache.catalina.core.StandardContext.filterStart(StandardContext.java:4609)
    at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:5248)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1393)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1383)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
    at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:140)
    at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:916)
    at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:835)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1393)
    at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1383)
    at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
    at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:75)
    at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:140)
    at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:916)
    at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:265)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
    at org.apache.catalina.core.StandardService.startInternal(StandardService.java:430)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
    at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:930)
    at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183)
    at org.apache.catalina.startup.Catalina.start(Catalina.java:772)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:566)
    at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:345)
    at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:476)

To Reproduce, please download this sample app and deploy it into Tomcat 9.0.

Thanks.

Comment From: marcusdacoregio

Hi @albertus82, thanks for the report.

The problem here is that some beans are not getting scanned during the ApplicationContext initialization. Since the SecurityConfig is a root config class, the MvcRequestMatcher needs a bean named mvcHandlerMappingIntrospector which is initialized by @EnableWebMvc, and this annotation is present on ServletConfig.

The RootConfig contains a @ComponentScan but it will scan beans under com.example.spring.core only, not detecting com.example.spring.web.ServletConfig.

One simple fix to this would be to change @ComponentScan from RootConfig to @ComponentScan(basePackages = "com.example"), this way the ServletConfig will get scanned and the @EnableWebMvc annotation is imported before initializing the SecurityConfig.

Comment From: albertus82

Hi @marcusdacoregio and thank you for your support.

I gave a try to your change, but now the "root" application context contains not only middle-tier services, data sources, etc., but also controllers, view resolvers, locale resolvers, and other web-related beans that should be managed only by the Servlet application context.

So, trying to avoid mixing up all the beans:

  • if I put SecurityConfig in the root context or in both the contexts, I get org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named '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.' available at startup;
  • if I put SecurityConfig in the Servlet context only, the application breaks with org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named 'springSecurityFilterChain' available.

Comment From: marcusdacoregio

Another simple way to make it work is to move @EnableWebMvc from ServletConfig to SecurityConfig. Would that be fine for you?

Comment From: albertus82

Hi Marcus and thank you again.

This way the application starts but none of my request mappings are recognized; I always GET 404:

19:20:38.507 [http-nio-8080-exec-8] DEBUG org.springframework.security.web.FilterChainProxy - Securing GET /
19:20:38.508 [http-nio-8080-exec-8] DEBUG org.springframework.security.web.context.SecurityContextPersistenceFilter - Set SecurityContextHolder to empty SecurityContext
19:20:38.508 [http-nio-8080-exec-8] DEBUG org.springframework.security.web.authentication.preauth.j2ee.J2eePreAuthenticatedProcessingFilter - Authenticating null
19:20:38.508 [http-nio-8080-exec-8] DEBUG org.springframework.security.web.authentication.preauth.j2ee.J2eePreAuthenticatedProcessingFilter - PreAuthenticated J2EE principal: admin
19:20:38.508 [http-nio-8080-exec-8] DEBUG org.springframework.security.web.authentication.preauth.j2ee.J2eePreAuthenticatedProcessingFilter - preAuthenticatedPrincipal = admin, trying to authenticate
19:20:38.508 [http-nio-8080-exec-8] DEBUG org.springframework.security.web.authentication.preauth.j2ee.J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource - J2EE roles [[ROLE_ADMIN]] mapped to Granted Authorities: [[ROLE_ADMIN]]
19:20:38.514 [http-nio-8080-exec-8] DEBUG org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider - PreAuthenticated authentication request: PreAuthenticatedAuthenticationToken [Principal=admin, Credentials=[PROTECTED], Authenticated=false, Details=PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null]; [ROLE_ADMIN], Granted Authorities=[]]
19:20:38.514 [http-nio-8080-exec-8] INFO com.example.spring.security.UserDetailsService - Authenticated user: [com.example.spring.security.AuthenticatedUser [Username=ADMIN, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_ADMIN]]].
19:20:38.514 [http-nio-8080-exec-8] DEBUG org.springframework.security.web.authentication.preauth.j2ee.J2eePreAuthenticatedProcessingFilter - Authentication success: PreAuthenticatedAuthenticationToken [Principal=com.example.spring.security.AuthenticatedUser [Username=ADMIN, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_ADMIN]], Credentials=[PROTECTED], Authenticated=true, Details=PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null]; [ROLE_ADMIN], Granted Authorities=[ROLE_ADMIN]]
19:20:38.515 [http-nio-8080-exec-8] DEBUG org.springframework.security.web.context.HttpSessionSecurityContextRepository - Created HttpSession as SecurityContext is non-default
19:20:38.515 [http-nio-8080-exec-8] DEBUG org.springframework.security.web.context.HttpSessionSecurityContextRepository - Stored SecurityContextImpl [Authentication=PreAuthenticatedAuthenticationToken [Principal=com.example.spring.security.AuthenticatedUser [Username=ADMIN, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_ADMIN]], Credentials=[PROTECTED], Authenticated=true, Details=PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null]; [ROLE_ADMIN], Granted Authorities=[ROLE_ADMIN]]] to HttpSession [org.apache.catalina.session.StandardSessionFacade@6b722d3e]
19:20:38.515 [http-nio-8080-exec-8] DEBUG org.springframework.security.web.FilterChainProxy - Secured GET /
19:20:38.515 [http-nio-8080-exec-8] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/spring-sec-sample-web/", parameters={}
19:20:38.516 [http-nio-8080-exec-8] WARN org.springframework.web.servlet.PageNotFound - No mapping for GET /spring-sec-sample-web/
19:20:38.516 [http-nio-8080-exec-8] DEBUG org.springframework.security.web.context.HttpSessionSecurityContextRepository - Stored SecurityContextImpl [Authentication=PreAuthenticatedAuthenticationToken [Principal=com.example.spring.security.AuthenticatedUser [Username=ADMIN, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_ADMIN]], Credentials=[PROTECTED], Authenticated=true, Details=PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null]; [ROLE_ADMIN], Granted Authorities=[ROLE_ADMIN]]] to HttpSession [org.apache.catalina.session.StandardSessionFacade@6b722d3e]
19:20:38.516 [http-nio-8080-exec-8] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 404 NOT_FOUND
19:20:38.516 [http-nio-8080-exec-8] DEBUG org.springframework.security.web.context.HttpSessionSecurityContextRepository - Stored SecurityContextImpl [Authentication=PreAuthenticatedAuthenticationToken [Principal=com.example.spring.security.AuthenticatedUser [Username=ADMIN, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_ADMIN]], Credentials=[PROTECTED], Authenticated=true, Details=PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null]; [ROLE_ADMIN], Granted Authorities=[ROLE_ADMIN]]] to HttpSession [org.apache.catalina.session.StandardSessionFacade@6b722d3e]
19:20:38.516 [http-nio-8080-exec-8] DEBUG org.springframework.security.web.context.SecurityContextPersistenceFilter - Cleared SecurityContextHolder to complete request

I notice that however the root context now contains many web related beans, but at least there aren't the ones written by me.

Now I honestly don't understand if I have a weird or uncommon configuration, or maybe people in general don't care what contexts beans are in, but then why is there a distinction between root and Servlet context?

Comment From: marcusdacoregio

What I usually see being done is: most of the applications live in a single DispatcherServlet world, thus not requiring a different application context, sticking with just one root context.

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

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

If you really want to stick with the hierarchical contexts, I'd recommend moving the SecurityConfig.class to getRootConfigClasses and move @EnableWebMvc to the SecurityConfig.

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

@Override
protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[] { ServletConfig.class };
}

@Configuration
@ComponentScan
@EnableWebSecurity
@EnableWebMvc
@EnableMethodSecurity(securedEnabled = true)
public class SecurityConfig implements ApplicationContextAware {
    // ...
}

I'll ask @jzheaux to provide his input on this matter.

Comment From: albertus82

What I usually see being done is: most of the applications live in a single DispatcherServlet world, thus not requiring a different application context, sticking with just one root context.

Interesting, but clearly in contrast with the Spring documentation, which states:

  • for the root application context: The returned context is delegated to ContextLoaderListener.ContextLoaderListener(WebApplicationContext) and will be established as the parent context for any DispatcherServlet application contexts. As such, it typically contains middle-tier services, data sources, etc.
  • for the Servlet application context: The returned context is delegated to Spring's DispatcherServlet.DispatcherServlet(WebApplicationContext). As such, it typically contains controllers, view resolvers, locale resolvers, and other web-related beans.

If you really want to stick with the hierarchical contexts, I'd recommend moving the SecurityConfig.class to getRootConfigClasses and move @EnableWebMvc to the SecurityConfig.

I tried but with no luck. I found only two working configurations: one with everything in the Servlet context and another with everything in the root application context.

Digging into the code, I found this condition that determines the usage of either MVC matchers or Ant matchers; that condition verifies only the presence of the HandlerMappingIntrospector class (true → use MVC matchers, false → use Ant matchers), but afterwards there is a blocking check that ensures that a bean named mvcHandlerMappingIntrospector exist too. Maybe we could think about this logic?

It seems that this deprecation had some unintended side effect.

Thanks.

Comment From: marcusdacoregio

I tried but with no luck.

Something might be different then, with that setup I get Authenticated user: ADMIN - Roles: [ROLE_ADMIN] in the root path.

Maybe we could think about this logic?

That was my first discussion with @rwinch when designing this feature. It is designed this way because if you have Spring WebMVC in the classpath but somehow don't have the mvcHandlerMappingIntrospector bean, that means that Spring Security Configuration is not in the same ApplicationContext as your DispatcherServlet, and the MvcRequestMatcher needs that bean, see here. If you do not want to use MvcRequestMatcher, an explicit configuration is needed.

That said, I brought this matter to the team's attention and we are looking into it.

Comment From: marcusdacoregio

Hi, @albertus82. After reaching out to the team and getting some good explanations, especially from @rwinch and @dsyer:

The getRootConfigClasses and getServletConfigClasses are meant to be used by applications with more than one Servlet that depends on a shared set of service beans, in such cases, it could make sense to have shared services in the root context so they are only created once for all servlet instances. This setup was common a long time ago (XML days) but not common now, even with WAR deployments.

Therefore, this was before the concept of MvcRequestMatcher, as I mentioned, the documentation states that you need the Spring Security and Spring MVC beans to be visible to each other. If you have multiple Servlets it then becomes complicated because you run into a situation where you likely need a springSecurityFilterChain for each servlet so that you can ensure the routing of Security and the servlets align. There are better ways to handle things than having more than one servlet (DispatcherServlet is meant to be the entry point to everything and it dispatches out to various controllers rather than having lots of DispatcherServlets).

With that said, if such a setup makes sense for you, you can configure your initializer like this:

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

@Override
protected Class<?>[] getServletConfigClasses() {
    return new Class<?>[] { ServletConfig.class, SecurityConfig.class };
}

And override AbstractSecurityWebApplicationInitializer.getDispatcherWebApplicationContextSuffix() to return AbstractDispatcherServletInitializer.DEFAULT_SERVLET_NAME, so that Spring Security is loaded from the DispatcherServlet's ApplicationContext.

The final code for your initializer would be:

public class SpringWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {

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

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class<?>[] { ServletConfig.class, SecurityConfig.class };
    }

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

    public static class SecurityWebAppInitializer extends AbstractSecurityWebApplicationInitializer {

        @Override
        protected String getDispatcherWebApplicationContextSuffix() {
            return AbstractDispatcherServletInitializer.DEFAULT_SERVLET_NAME;
        }

    }

}

I hope that the explanation makes sense to you.

Comment From: albertus82

Thank you very much. I think moving SecurityConfig.class to the Servlet application context is the right choice; I tried to do it but I was missing that override of AbstractSecurityWebApplicationInitializer.getDispatcherWebApplicationContextSuffix().

Comment From: JohnZ1385

I've implemented this fix as suggested and it works well, just curious if this is possible from a web.xml.

DelegatingFilterProxy springSecurityFilterChain = new DelegatingFilterProxy();
// this is a hack introduced to support Spring Security 5.8.9 or greater (at very least one backed by Spring dev team)
// previously the SecurityConfig was at the rootCtx, but post 5.8.9 the framework explicitly requires the Security and Servlet configs to be defined at the same level
// see https://github.com/spring-projects/spring-security/issues/12319#issuecomment-1338377623
springSecurityFilterChain.setContextAttribute(SERVLET_CONTEXT_PREFIX + "app-servlet");
FilterRegistration.Dynamic security = servletContext.addFilter("springSecurityFilterChain", springSecurityFilterChain);
security.addMappingForUrlPatterns(EnumSet.of(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE), true, "/*");