spring-projects / spring-security

Spring Security
http://spring.io/projects/spring-security
Apache License 2.0
8.76k stars 5.88k forks source link

Spring Security & Spring MVC shared application context no longer working post version 5.8.8 #14636

Closed JohnZ1385 closed 7 months ago

JohnZ1385 commented 7 months ago

Hi, I tried asking this on community forums and stackoverflow and got no response but I'm also not sure there isn't potentially something wrong here regardless. I have an application that's an EAR deployment comprised of shared libraries, EJB's, and various WARs .. each of those WARs has a spring mvc + spring security context along with a root context for bean definitions local to that webapp along with a reference to a shared context defined in the lib jars that comprise the EAR file. Since spring security 5.8.9 I've seen the message:

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; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'securityFilterChain' defined in com.[organization].[app].console.SecurityConfig: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.security.web.SecurityFilterChain]: Factory method 'securityFilterChain' threw exception; nested exception is 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

the web.xml (slightly obfuscated) for quick reference:

<web-app version="4.0" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd">

  <display-name>mywebapp</display-name>
  <!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
  <context-param>
    <param-name>contextClass</param-name>
    <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
  </context-param>
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
      com.organization.app.console.ApplicationContextConfig,
      com.organization.app.console.SecurityConfig
    </param-value>
  </context-param>

  <listener>
    <listener-class>com.organization.app.server.servlet.LoggingServiceContextListener</listener-class>
  </listener>
  <listener>
    <!-- This class is a subclass of ContextLoaderListener with loadParentContext overwritten to consume the shared EAR context -->
    <listener-class>com.organization.app.server.servlet.MyContextLoaderListener</listener-class>
  </listener>

  <servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextClass</param-name>
      <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </init-param>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>com.organization.app.console.ServletConfig</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>appServlet</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>

  <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>

  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>

</web-app>

The error message is pretty clear that the SecurityConfig and ServletConfig should be in a shared ApplicationContext .. so I moved them accordingly.


  <context-param>
    <param-name>contextClass</param-name>
    <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
  </context-param>
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
      com.organization.app.console.ApplicationContextConfig,
      <!-- com.organization.app.console.SecurityConfig -->
    </param-value>
  </context-param>

  <servlet>
    <servlet-name>appServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextClass</param-name>
      <param-value>org.springframework.web.context.support.AnnotationConfigWebApplicationContext</param-value>
    </init-param>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value>
        com.organization.app.console.ServletConfig,
        com.organization.app.console.SecurityConfig
       </param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>

A startup with this setup in the web.xml just leads to a generic error:

<Feb 18, 2024, 4:47:23,474 PM Alaska Standard Time> <Error> <HTTP> <BEA-101165> <Could not load user defined filter in web.xml: org.springframework.web.filter.DelegatingFilterProxy.
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.doGetBean(AbstractBeanFactory.java:283)
        at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:283)
        Truncated.
>

At very least I have attempted to simulate loading this configuration setup via a Junit test and it seems to execute just fine. (ignore the code obfuscation)

image

Am I fundamentally missing something here? Is there perhaps an issue with the application server that I'm (unfortunately) stuck using for this application? (WebLogic 14.1.1) (their latest release though definitely a bit dated tech stack wise).

jzheaux commented 7 months ago

Thanks for the report, @JohnZ1385, sorry you are having trouble. Are you able to produce a minimal GitHub sample that demonstrates the issue? I think that will help us get to the bottom of your error more quickly.

For example, it may be that your ApplicationContextConfig is in some way dependent on Spring Security's filter chain, but it's unclear from the current configuration.

JohnZ1385 commented 7 months ago

@jzheaux that might be a bit of an issue because of it's EAR file dependency, if need be we can explore that option but I'll first share the ApplicationContextConfig (i.e. the RootConfig), SecurityConfig and ServletConfig classes here. Luckily they are pretty generic so I only made minimal obfuscation attempts here. I was initially under the impression that I might have that exact scenario, i.e. where my ApplicationContextConfig is potentially picking up spring security components unknowingly, but there is no component scan in the ApplicationContextConfig. The only component scan I use is for @Controller classes in the ServletConfig.

Here is the ApplicationContextConfig (i.e. RootConfig) file. As I mentioned earlier that app leverages an EAR file level shared application context .. so beans such as "ClientEmailService, ClientService" etc etc are defined in that parent application context.

Those are loaded by registering a ContextLoaderListener and overriding the method loadParentContext.


@Configuration
public class ApplicationContextConfig {

  @Autowired
  private StaticNameSpaceDataService staticNameSpaceDataService;

  @Bean
  public MonitorService monitorService(ClientEmailService clientEmailService, ClientService clientService, DeviceService deviceService,
      DeviceStatusService deviceStatusService, UserClientService userClientService) {
    return new MonitorServiceImpl(clientEmailService, clientService, deviceService, deviceStatusService, screenPreferenceService(), userClientService);
  }

  @Bean
  public ScreenPreferenceService screenPreferenceService() {
    return new ScreenPreferenceServiceImpl(staticNameSpaceDataService);
  }
}

the SecurityConfig


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

  private static final String ROLE_CLIENT_USER = "CLIENT_USER";

  @Autowired
  private UserService userService;

  @Bean("accessDeniedHandler")
  public AccessDeniedHandler accessDeniedHandler() {
    AccessDeniedHandlerImpl handler = new AccessDeniedHandlerImpl();
    handler.setErrorPage("/accessDenied");
    return handler;
  }

  @Bean
  public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    return http
      .sessionManagement().enableSessionUrlRewriting(false)
      .and()
      .authorizeHttpRequests()

        .requestMatchers(HttpMethod.GET, "/common/css/**").permitAll()
        .requestMatchers(HttpMethod.GET, "/common/images/**").permitAll()
        .requestMatchers(HttpMethod.GET, "/common/jquery/**").permitAll()
        .requestMatchers(HttpMethod.GET, "/common/js/**").permitAll()

        .requestMatchers("/login").permitAll()

        .requestMatchers("/home/**").hasRole(ROLE_CLIENT_USER)

        .requestMatchers("/accessDenied/**", "/logout/").permitAll()

        .requestMatchers("/**").hasRole(ROLE_CLIENT_USER)
        .anyRequest().denyAll()
      .and()
      .formLogin()
        .loginPage("/login").defaultSuccessUrl("/home/").usernameParameter("username").passwordParameter("password")
          .loginProcessingUrl("/login").failureUrl("/login?error=1")
      .and()
        .logout().logoutUrl("/logout")
      .and()
        .exceptionHandling().accessDeniedHandler(accessDeniedHandler())
      .and()
        .headers().disable() 
        .csrf().disable() 
    .build();
  }

  @Bean("passwordEncoder")
  public PasswordEncoder passwordEncoder() {
    return new CustomMessageDigestPasswordEncoder("SHA-256");
  }

  @Bean("customUserDetailManager")
  @Autowired
  public UserDetailsManager customUserDetailManager() {
    return new CustomUserDetailManager(userService);
  }

  @Bean("authenticationManager")
  public AuthenticationManager authenticationManager() {
    DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
    authenticationProvider.setUserDetailsService(customUserDetailManager());
    authenticationProvider.setPasswordEncoder(passwordEncoder());

    return new ProviderManager(authenticationProvider);
  }
}

and the ServletConfig (i.e. WebConfig)


import java.util.List;
import java.util.TimeZone;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@EnableWebMvc
@Configuration
@ComponentScan(basePackages = { "com.organization.app.console" }, useDefaultFilters = false, includeFilters = @Filter(type = FilterType.ANNOTATION, value = Controller.class))
public class ServletConfig implements WebMvcConfigurer {

  @Override
  public void configureViewResolvers(ViewResolverRegistry registry) {
    registry.enableContentNegotiation(new View[0]);
    registry.jsp("/WEB-INF/views/", ".jsp");
  }

  @Override
  public void addResourceHandlers(ResourceHandlerRegistry registry) {
    registry.addResourceHandler("/**").addResourceLocations("/content/");
  }

  @Override
  public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
    for (HttpMessageConverter<?> mc : converters) {
      if (mc instanceof MappingJackson2HttpMessageConverter) {
        ((AbstractJackson2HttpMessageConverter) mc).getObjectMapper().setTimeZone(TimeZone.getDefault());
      }
    }
    return;
  }
}
JohnZ1385 commented 7 months ago

@jzheaux I was able to solve this by following this comment: https://github.com/spring-projects/spring-security/issues/12319#issuecomment-1338377623 and setting the attribute org.springframework.web.servlet.FrameworkServlet.CONTEXT.[myServletName]

I could only actually accomplish this by overloading the WebApplicationInitializer.onStartup(ServletContext servletContext) method .. based on the numerous similar issues/posts I've seen for issues like this it might not be the worst idea in the future for Spring to support some sort of subclass of WebApplicationInitializer or feature set to handle this setup.