升级至Spring Boot 3后Swagger UI无法访问问题求助
问题描述
原本运行的基于Spring Boot 2.7.6与Spring Security 5.7.5的应用,认证功能正常,可按角色访问页面,Swagger UI也能正常使用且无弃用警告。迁移至Spring Boot 3.0.0(对应Spring Security 6.0.0)后,服务器可正常启动,但所有URL均返回401状态码。
请求GET http://localhost:8080/swagger-ui/index.html时,客户端收到401响应,服务端日志显示404:
DEBUG o.s.web.servlet.handler.SimpleUrlHandlerMapping : Mapped to ResourceHttpRequestHandler [classpath [static/]] DEBUG o.s.web.servlet.handler.SimpleUrlHandlerMapping : Mapped to ResourceHttpRequestHandler [classpath [static/]] DEBUG o.s.web.servlet.handler.SimpleUrlHandlerMapping : Mapped to ResourceHttpRequestHandler [classpath [static/]] DEBUG o.s.web.servlet.handler.SimpleUrlHandlerMapping : Mapped to ResourceHttpRequestHandler [classpath [static/]] DEBUG org.springframework.web.servlet.DispatcherServlet : GET "/swagger-ui/index.html", parameters={} DEBUG o.s.web.servlet.handler.SimpleUrlHandlerMapping : Mapped to ResourceHttpRequestHandler [classpath [static/]] DEBUG o.s.w.servlet.resource.ResourceHttpRequestHandler : Resource not found DEBUG org.springframework.web.servlet.DispatcherServlet : Completed 404 NOT_FOUND DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse) DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse) DEBUG o.s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped to org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController#errorHtml(HttpServletRequest, HttpServletResponse)
相关配置文件
pom.xml
... <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.0</version> <relativePath /> <!-- lookup parent from repository --> </parent> ... <properties> <java.version>17</java.version> <jjwt.version>0.11.5</jjwt.version> <springdoc.version>1.6.0</springdoc.version> <docx4j.version>11.3.2</docx4j.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-core</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.liquibase</groupId> <artifactId>liquibase-core</artifactId> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-collections4</artifactId> <version>4.4</version> </dependency> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>${jjwt.version}</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>${jjwt.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>${jjwt.version}</version> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-ui</artifactId> <version>${springdoc.version}</version> </dependency> <dependency> <groupId>org.springdoc</groupId> <artifactId>springdoc-openapi-security</artifactId> <version>${springdoc.version}</version> </dependency> <dependency> <groupId>org.docx4j</groupId> <artifactId>docx4j-export-fo</artifactId> <version>${docx4j.version}</version> </dependency> <dependency> <groupId>org.docx4j</groupId> <artifactId>docx4j-JAXB-ReferenceImpl</artifactId> <version>${docx4j.version}</version> </dependency> <dependency> <groupId>jakarta.xml.bind</groupId> <artifactId>jakarta.xml.bind-api</artifactId> <version>3.0.1</version> </dependency> <dependency> <groupId>jakarta.xml.bind</groupId> <artifactId>jakarta.xml.bind-api-parent</artifactId> <version>3.0.1</version> <type>pom</type> </dependency> </dependencies> ... </project>
SecurityConfig类
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpStatus; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.context.RequestAttributeSecurityContextRepository; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; @Configuration @EnableWebSecurity @EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true) public class SecurityConfig { @Autowired UserDetailsService userDetailsService; @Autowired public void configureGlobal(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder.getDefaultUserDetailsService(); } // @Override // public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { // authenticationManagerBuilder.userDetailsService(this.userDetailsService) // .passwordEncoder(passwordEncoder()); // } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception { // @formatter:off http .headers() .frameOptions().disable() .and() .cors() .and() .csrf().disable() .exceptionHandling() .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED)).and() .formLogin().disable() .authorizeHttpRequests(authz -> authz.requestMatchers("/api/*/auth/**").permitAll() .requestMatchers("/api/*/public/**").permitAll() .requestMatchers("/api/*/catalogs/*/documents/*/file").permitAll() .requestMatchers(req -> req.getRequestURI() .contains("swagger-ui")).permitAll() .anyRequest().authenticated()); // @formatter:on return http.build(); } @Bean public WebSecurityCustomizer webSecurityCustomizer() { return (web) -> web.ignoring() .requestMatchers(req -> req.getRequestURI() .contains("mail-images")) .requestMatchers(req -> req.getRequestURI() .contains("api-docs")) // .requestMatchers(req -> req.getRequestURI() // .contains("swagger-ui")) .requestMatchers(req -> req.getRequestURI() .contains("h2-console")); } }
WebMvcConfig类
import java.util.List; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration @EnableWebMvc public class WebMvcConfig implements WebMvcConfigurer { @Value("#{'${cors.allowedOrigins}'.split(',')}") private List<String> allowedOrigins; @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowCredentials(true) .allowedHeaders("*") .allowedOriginPatterns("http://*", "https://*") .allowedOrigins(this.allowedOrigins.toArray(String[]::new)) .allowedMethods(HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.PUT.name(), HttpMethod.PATCH.name(), HttpMethod.DELETE.name(), HttpMethod.OPTIONS.name()); } private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {"classpath:/static/"}; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/**") .addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS); } }
排查方向
- springdoc版本兼容问题:当前使用的
springdoc.version=1.6.0仅支持Spring Boot 2.x,Spring Boot 3.x需要升级到springdoc-openapi 2.x版本(如2.0.2)。旧版本无法正确加载swagger-ui的静态资源,导致请求返回404,进而被Security拦截返回401。 - Security请求匹配逻辑优化:Spring Security 6.0对
requestMatchers的匹配逻辑做了调整,建议使用明确的路径匹配替换模糊的contains判断,比如将:
替换为:.requestMatchers(req -> req.getRequestURI().contains("swagger-ui")).permitAll()
同时可以在.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()WebSecurityCustomizer中添加swagger相关路径的忽略规则,确保资源不被拦截。 - UserDetailsService配置失效:当前
configureGlobal方法未绑定自定义的userDetailsService,导致认证逻辑未使用你的用户数据源,所有请求无法通过认证返回401。修改该方法:@Autowired public void configureGlobal(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception { authenticationManagerBuilder.userDetailsService(this.userDetailsService) .passwordEncoder(passwordEncoder()); } - WebMvc资源映射冲突:WebMvcConfig中
addResourceHandlers将/**都映射到classpath:/static/,覆盖了springdoc的swagger-ui资源映射,导致静态文件无法找到。可以修改为仅映射自定义静态资源:
或者直接删除该自定义配置,Spring Boot默认会处理静态资源映射。@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/static/**") .addResourceLocations(CLASSPATH_RESOURCE_LOCATIONS); } - SecurityContextRepository配置:Spring Security 6.0中默认的SecurityContext存储方式有变化,可在SecurityFilterChain中显式配置:
确保认证上下文能被正确存储和读取。http.securityContext(securityContext -> securityContext .securityContextRepository(new RequestAttributeSecurityContextRepository()) );
内容的提问来源于stack exchange,提问作者Nicolas
相关产品推荐
相关产品推荐

