You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.28 19:55:54