Spring Boot 2迁移至3调用接口出现PatternParseException问题排查
Spring Boot 2迁移至Spring Boot 3后接口调用触发PatternParseException
问题场景
Spring Boot 2应用迁移到Spring Boot 3,已完成Spring Security适配修改,应用启动正常,但调用接口时抛出PatternParseException,怀疑由Security配置导致。
异常栈信息
org.springframework.web.util.pattern.PatternParseException: No more pattern data allowed after {*...} or ** pattern element at org.springframework.web.util.pattern.InternalPathPatternParser.peekDoubleWildcard(InternalPathPatternParser.java:250) ~[spring-web-6.0.11.jar!/:6.0.11] at org.springframework.web.util.pattern.InternalPathPatternParser.parse(InternalPathPatternParser.java:113) ~[spring-web-6.0.11.jar!/:6.0.11] at org.springframework.web.util.pattern.PathPatternParser.parse(PathPatternParser.java:129) ~[spring-web-6.0.11.jar!/:6.0.11] at org.springframework.web.servlet.handler.PathPatternMatchableHandlerMapping.lambda$match$0(PathPatternMatchableHandlerMapping.java:64) ~[spring-webmvc-6.0.11.jar!/:6.0.11] at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708) ~[na:na]
相关配置代码
SecurityConfig
package com.omb.restcore.security; import com.omb.core.security.utils.JasyptEncryptorDecryptor; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.env.AbstractEnvironment; import org.springframework.core.env.Environment; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.PropertySource; import org.springframework.http.HttpMethod; import org.springframework.security.config.Customizer; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configurers.provisioning.InMemoryUserDetailsManagerConfigurer; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.logging.Logger; @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig { private static final Logger LOG = Logger.getLogger(SecurityConfig.class.getName()); private static final String HEALTHCHECK_URL = "/**/healthCheck"; private static final String[] ALLOW_LIST = { HEALTHCHECK_URL, "/**/v3/api-docs", "/**/swagger-resources/**", "/**/swagger-ui.html", "/**/webjars/**", "/**/csrf", "/", "/**/css/**" }; @Value("${enableHttpAuthentication:true}") private boolean enableHttpAuthentication; @Value("${enableLoginEncryption}") private boolean enableLoginEncryption; @Value("${allowAnonymousAccessToSwaggerDoc}") private boolean allowAnonymousAccessToSwaggerDoc; @Autowired private Environment environment; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { if (enableHttpAuthentication) { // 仅接受BASIC认证请求,除了允许匿名访问的资源 if (allowAnonymousAccessToSwaggerDoc) { http.csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth.requestMatchers(ALLOW_LIST).permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .anyRequest().authenticated()) .httpBasic(Customizer.withDefaults()); } else { http.csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth.requestMatchers(HEALTHCHECK_URL).permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .anyRequest().authenticated()) .httpBasic(Customizer.withDefaults()); } LOG.info("BASIC HTTP authentication enabled"); } else { // 禁用API认证 http.csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth.anyRequest().anonymous()); LOG.info("BASIC HTTP authentication disabled"); } return http.build(); } @Bean CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(List.of("*")); configuration.setAllowedMethods(List.of("*")); configuration.setAllowCredentials(true); configuration.setAllowedHeaders(Arrays.asList("Authorization", "Requestor-Type")); configuration.setExposedHeaders(List.of("X-Get-Header")); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> inMemoryAuth = auth.inMemoryAuthentication(); for (PropertySource<?> propertySource : ((AbstractEnvironment) environment).getPropertySources()) { if (propertySource instanceof MapPropertySource) { Map<String, Object> properties = ((MapPropertySource) propertySource).getSource(); java.util.regex.Pattern pattern = java.util.regex.Pattern.compile("user\\.(.*)\\.password"); for (String k : properties.keySet()) { java.util.regex.Matcher matcher = pattern.matcher(k); if (matcher.find()) { String user = matcher.group(1); String pwd = properties.get(k).toString(); if (enableLoginEncryption) { user = JasyptEncryptorDecryptor.decrypt(user); } pwd = JasyptEncryptorDecryptor.decrypt(pwd); if (!StringUtils.isEmpty(pwd)) { LOG.finest("为用户 " + user + " 配置HTTP BASIC访问权限"); inMemoryAuth.withUser(user).password("{noop}" + pwd).roles("USER"); } else { LOG.finest("禁用用户 " + user + " 的HTTP BASIC访问权限"); } } } } } LOG.info("HTTP BASIC配置完成"); } }
WebConfig
package com.omb.gateway.security; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig { @Bean public WebMvcConfigurer corsConfigurer() { return new WebMvcConfigurer() { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedHeaders("Requestor-Type") .exposedHeaders("X-Get-Header") .allowCredentials(true); } }; } }
问题原因
Spring Boot 3基于Spring Framework 6,默认使用PathPatternParser处理路径匹配,该解析器的规则比旧版AntPathMatcher更严格:**通配符只能作为路径的最后一段,不能在其后追加任何路径内容。
你的配置中存在多个违规路径:
/**/healthCheck:**后跟随了/healthCheck/**/v3/api-docs:**后跟随了/v3/api-docs/**/swagger-resources/**:**出现在路径中间,前后都有其他内容
这些不符合规则的路径模式触发了PatternParseException。
解决方案
方案1:适配PathPattern规则(推荐)
修改所有违规路径,确保**仅出现在路径末尾:
private static final String HEALTHCHECK_URL = "/**healthCheck"; // 匹配任意路径下的healthCheck接口 private static final String[] ALLOW_LIST = { HEALTHCHECK_URL, "/v3/api-docs/**", "/swagger-resources/**", "/swagger-ui.html", "/webjars/**", "/csrf", "/", "/css/**" };
如果健康检查接口路径固定(如/actuator/healthCheck),直接写具体路径更安全:
private static final String HEALTHCHECK_URL = "/actuator/healthCheck";
方案2:兼容旧版AntPathMatcher
如果不想修改路径模式,可以配置Spring Security使用AntPathMatcher:
在filterChain方法中,使用AntPathRequestMatcher包装路径:
// 示例:替换原requestMatchers写法 auth.requestMatchers(AntPathRequestMatcher.antMatcher(HEALTHCHECK_URL)).permitAll() .requestMatchers(AntPathRequestMatcher.antMatcher("/**/v3/api-docs")).permitAll() // 其他路径同理
需要注意导入org.springframework.security.web.util.matcher.AntPathRequestMatcher类。
内容的提问来源于stack exchange,提问作者Ousmane MBINTE
相关产品推荐
相关产品推荐

