升级Spring Security至6.0.5+遇Servlet规范相关异常求助
在Tomcat 10.1.7上使用Spring 6、Java 17构建CAS认证的Web应用,以下版本组合运行正常:
- spring-security-config: 6.0.4
- spring-webmvc: 6.0.11
- spring-security-cas: 6.1.2
- spring-session-jdbc: 3.1.1
升级spring-security-config到6.0.5及以上版本后,部署时抛出异常:
java.lang.UnsupportedOperationException: Section 4.4 of the Servlet 3.0 specification does not permit this method to be called from a ServletContextListener that was not defined in web.xml, a web-fragment.xml file nor annotated with @WebListener
异常触发于SecurityConfiguration中requestMatchers方法的首次调用,原因是Spring Security 6.0.5+在AbstractRequestMatcherRegistry.requestMatchers中新增了对jakarta.servlet.ServletContext.getServletRegistrations()的调用,当前应用的初始化方式导致该调用违反Servlet 3.0规范。
已尝试添加空的@WebListener ServletContextListener、移除AbstractSecurityWebApplicationInitializer,均未解决问题。
解决方案1:显式禁用ServletRegistrationLookup
Spring Security 6.0.5+支持通过配置禁用对Servlet注册信息的查询,直接规避违规调用。在SecurityFilterChain配置中添加如下设置:
@Bean public SecurityFilterChain filterChain(HttpSecurity http, UserDetailsService userDetailsService) throws Exception { http .authorizeHttpRequests(auth -> auth .lookupServletRegistration(false) // 禁用Servlet注册查询 .requestMatchers("/index*", "/search*", "/show*").hasAuthority(Permission.QUERY.getName()) .requestMatchers("/edit*").hasAuthority(Permission.MANAGING.getName()) .anyRequest().permitAll() ) // 原有异常处理、过滤器、logout等配置保持不变 .exceptionHandling(excep -> excep .accessDeniedPage("/notAuthorized") ) .addFilter(casAuthenticationFilter(userDetailsService)) .addFilterAfter(new CsrfCookieGeneratorFilter(), CsrfFilter.class) .addFilterBefore(casSingleLogoutFilter(), CasAuthenticationFilter.class) .httpBasic(httpBasic -> httpBasic .authenticationEntryPoint(casAuthenticationEntryPoint()) ) .logout(logout -> logout .logoutRequestMatcher(new AntPathRequestMatcher("/close-sesion")) .logoutSuccessUrl(env.getProperty("security.cas.logoutUrl")) .deleteCookies("auth_code","JSESSIONID","CSRF-TOKEN") .invalidateHttpSession(true).permitAll() ) .headers(headers -> headers .xssProtection(xss -> xss.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED_MODE_BLOCK)) .contentSecurityPolicy(csp -> csp.policyDirectives("script-src 'self'; form-action 'self';")) ); return http.build(); }
解决方案2:调整应用初始化顺序
当前通过AbstractAnnotationConfigDispatcherServletInitializer同时加载Web和Security配置,导致Security配置在ServletContext完全初始化前被处理。拆分配置加载时机,让Security在ServletContext初始化后期加载:
步骤1:拆分Security配置类
创建独立的根安全配置类,仅包含Spring Security相关配置:
@Configuration @ComponentScan(basePackages = "com.myapp.security") @EnableWebSecurity @PropertySource(value = { "classpath:dga-security.properties" }, ignoreResourceNotFound = false) public class RootSecurityConfig { @Autowired private Environment env; // 原SecurityConfiguration中的所有Bean定义(filterChain、casAuthenticationFilter等)移到这里 }
步骤2:修改DispatcherServlet初始化类
让AnnotationConfigDispatcherServletInitializer仅加载Web配置:
public class AnnotationConfigDispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { @Override protected Class<?>[] getRootConfigClasses() { return new Class[] {WebConfig.class}; // 仅加载Web配置 } @Override protected Class<?>[] getServletConfigClasses() { return null; } @Override protected String[] getServletMappings() { return new String[] { "/" }; } @Override protected Filter[] getServletFilters() { return null; } @Override protected void customizeRegistration(Dynamic registration) { Properties prop = getProperties("mensajes.properties"); File uploadDirectory = new File(System.getProperty("java.io.tmpdir")); long maxUploadSize = 10485760; registration.setMultipartConfig(new MultipartConfigElement( uploadDirectory.getAbsolutePath(), maxUploadSize, maxUploadSize*2, (int)(maxUploadSize/2) )); } }
步骤3:修改Security初始化类
让SecurityWebApplicationInitializer显式指定安全配置类,确保在ServletContext合适阶段加载:
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer { public SecurityWebApplicationInitializer() { super(RootSecurityConfig.class); // 显式指定Security配置类 } @Override protected void beforeSpringSecurityFilterChain(ServletContext servletContext) { insertFilters(servletContext, new OncePerRequestFilter(){ @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { filterChain.doFilter(request, response); } catch (MaxUploadSizeExceededException e) { // 处理文件过大异常 } catch (Throwable th) { throw th; } } }); insertFilters(servletContext, new MultipartFilter()); } }
解决方案3:升级Servlet规范版本
Tomcat 10.1.7支持Jakarta Servlet 6.0,该版本对getServletRegistrations()的调用限制有所放宽。修改web.xml适配Servlet 6.0规范:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd" version="6.0"> <display-name>myapp</display-name> <session-config> <session-timeout>30</session-timeout> <cookie-config> <http-only>true</http-only> <secure>false</secure> </cookie-config> <tracking-mode>COOKIE</tracking-mode> </session-config> <error-page> <error-code>400</error-code> <location>/WEB-INF/jsp/errors/400.jsp</location> </error-page> <!-- 其他错误页面配置 --> </web-app>
确保pom.xml中Jakarta Servlet依赖版本已为6.0.0(当前配置已满足)。
内容的提问来源于stack exchange,提问作者Nextor

