1

1. 问题

项目环境
jdk:21
springboot:3.2.3
springcloud:2023.0.0
springdoc-openapi-starter-webmvc-ui:2.5.0

项目引入了springdoc,本地开发测试时,http://localhost:8080/swagger-ui/index.html页面也能正常打开;发布到测试环境之后,通过网关(SpringCloud Gateway)访问页面http://xxx.com/SERVICENAME/swagger-ui/index.html,却无法打开。

2. 排查

通过F12可以发现,是因为页面请求了swagger-config文件,但是文件地址返回404404的原因是index.html里请求的地址是http://xxx.com/v3/api-docs/swagger-config,而不是http://xxx.com/SERVICENAME/v3/api-docs/swagger-config

3. 解决方案

3.1. 方案1(成功,但是作为公共包不合适)

通过官方文档发现可以通过修改配置,自定义路径:

springdoc:
  swagger-ui:
    configUrl: /SERVICENAME/v3/api-docs/swagger-config

配置完之后/swagger-ui/index.html请求swagger-config返回正常,但是页面还是无法打开,原因是页面又访问了/v3/api-docs,并且返回了404,原因跟swagger-config是一样的。
一样可以通过配置修改路径:

springdoc:
  api-docs:
    path: /SERVICENAME/v3/api-docs

可以看到configUrl的值其实就是在configUrl的值后面再加上/swagger-config,通过源码org.springdoc.core.utils.Constants也可以证实:

所以只需配置path即可,configUrl可以不用配置。
如果以一个项目的角度来看,问题是解决了。但是在开发的是一个平台公共包,SERVICENAME是未知的(不同的项目的值是不一样的),所以公共包里无法硬编码;而如果让每个项目引入公共包之后,在自己的项目上加上配置的话,增加了引入成本,并且请求走gateway时,路径上可能除了SERVICENAME之外,还会加上其他的内容。
\( \color{red} 所以需要尝试看是否还有其他通用的方案。 \)

3.2. 方案2(失败)

参考:404 error with spring doc open api 2.1.0
在项目里手动引入最新的webjars-locator

<dependency>
      <groupId>org.webjars</groupId>
      <artifactId>webjars-locator</artifactId>
      <version>0.52</version>
</dependency>

\( \color{red} 没有生效。 \)

3.3. 方案3(失败)

参考:Spring Boot 中访问 Swagger UI 报 404 not found 错误
在项目里添加addResourceHandler

@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {

    @Override
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/swagger-ui/**")
                .addResourceLocations("classpath:/META-INF/resources/webjars/swagger-ui/5.13.0/");
        super.addResourceHandlers(registry);
    }
}

其中swagger-ui后面的版本号,取自己依赖的webjars版本:

\( \color{red} 没有生效。 \)

3.4. 方案4(理论可行,但是没有尝试)

参考:springboot 搭配filebeat springboot 搭配 springdoc
SpringCloud Gateway里添加转发规则:

\( \color{red} 考虑到要去调整网关,影响比较大,并且网关不应该去关注下游系统具体使用的组件,所以没做尝试。 \)

3.5. 方案5(失败)

参考:configUrl cache issue when using Swagger-UI
添加以下代码:

@Bean
public SwaggerIndexPageTransformer computeConfigUrlSwagger(SwaggerUiConfigProperties swaggerUiConfig,
        SwaggerUiOAuthProperties swaggerUiOAuthProperties, SwaggerUiConfigParameters swaggerUiConfigParameters,
        SwaggerWelcomeCommon swaggerWelcomeCommon, ObjectMapperProvider objectMapperProvider) {
    return new SwaggerIndexPageTransformer(swaggerUiConfig, swaggerUiOAuthProperties, swaggerUiConfigParameters,
            swaggerWelcomeCommon, objectMapperProvider) {
 
        @Override
        public Resource transform(HttpServletRequest request, Resource resource, ResourceTransformerChain transformerChain)
                throws IOException {
            this.swaggerUiConfigParameters.setConfigUrl(null);
            return super.transform(request, resource, transformerChain);
        }
    };
}

\( \color{red} 没有生效。 \) 从参考的文档上其他的人也反馈没有生效。

3.6. 方案6(成功)

通过方案5,知道可以在代码里动态调整路径:每次打开/swagger-ui/index.html页面时,都会调用SwaggerIndexPageTransformertransform方法,方法返回的Resource对象的内容是页面的html内容

3.6.1. 首先要获取从HOSTURI中间这一段Gateway加上的路径

  1. 通过request.getHeader("Host")获取到请求的域名是IP:8080,而不是期望中的域名,因为从Gateway转发到业务服务时,是从注册中心中取到了IP并且通过IP+端口来转发。
  2. 通过request.getContextPath()获取到应用的上下文路径是空的,因为服务名是Gateway转发时加上的,项目本身没有配置。
  3. 通过debug发现,Gateway在转发请求时,会在header里加上一些x-forwarded-*信息:
  4. 非通过Gateway请求时(直接通过IP访问服务),header信息如下:
  5. 所以可以通过x-forwarded-*信息获取到Gateway添加的前缀,并且还可以判断当次请求是否走了Gateway

3.6.2.

实现代码:

@Configuration
@ConditionalOnClass(SpringDocWebMvcConfiguration.class)
public class SwaggerConfiguration {

    @Bean
    public SwaggerIndexTransformer indexPageTransformer(SwaggerUiConfigProperties swaggerUiConfig,
            SwaggerUiOAuthProperties swaggerUiOAuthProperties, SwaggerUiConfigParameters swaggerUiConfigParameters,
            SwaggerWelcomeCommon swaggerWelcomeCommon, ObjectMapperProvider objectMapperProvider,
            SpringDocConfigProperties springDocConfigProperties) {
        return new RewritePathSwaggerIndexPageTransformer(swaggerUiConfig, swaggerUiOAuthProperties,
                swaggerUiConfigParameters, swaggerWelcomeCommon, objectMapperProvider, springDocConfigProperties);
    }

    /**
     * 重写swagger地址,通过浏览器访问/swagger-ui/index.html时,html页面会请求/v3/api-docs和/v3/api-docs/swagger-config
     * 1)本地(localhost)访问时,能正常返回
     * 2)测试环境&正式环境通过网关访问时,会出现404,因为访问的地址少了服务名:
     * 比如应该是“http://host:port/SERVICE-NAME/v3/api-docs”,但实际访问的是“http://host:port//v3/api-docs”
     *
     * @author
     * @date 2024/6/19 15:11
     */
    public class RewritePathSwaggerIndexPageTransformer extends SwaggerIndexPageTransformer {

        private SpringDocConfigProperties springDocConfigProperties;
        private SwaggerUiConfigParameters swaggerUiConfigParameters;
        /**
         * 上一次请求获取到的前缀
         */
        private String lastPrefix = Strings.EMPTY;

        public RewritePathSwaggerIndexPageTransformer(SwaggerUiConfigProperties swaggerUiConfig,
                SwaggerUiOAuthProperties swaggerUiOAuthProperties, SwaggerUiConfigParameters swaggerUiConfigParameters,
                SwaggerWelcomeCommon swaggerWelcomeCommon, ObjectMapperProvider objectMapperProvider,
                SpringDocConfigProperties springDocConfigProperties) {
            super(swaggerUiConfig, swaggerUiOAuthProperties, swaggerUiConfigParameters, swaggerWelcomeCommon,
                    objectMapperProvider);
            this.springDocConfigProperties = springDocConfigProperties;
            this.swaggerUiConfigParameters = swaggerUiConfigParameters;
        }

        @Override
        public Resource transform(HttpServletRequest request, Resource resource,
                ResourceTransformerChain transformerChain) throws IOException {
            ApiDocs apiDocs = springDocConfigProperties.getApiDocs();
            String apiDocsPath = apiDocs.getPath();
            String configUrl = swaggerUiConfigParameters.getConfigUrl();
            String prefix = getPrefix(request);
            // 考虑同一个服务,可能有时候会被“走网关访问”,有时候会被“走IP+端口直接访问”,所以这里做了区分判断
            // 通过网关转发
            if (StringUtils.isNotBlank(prefix)) {
                // 上一次不是通过网关转发,即需要重新调整;如果上一次是通过网关转发,则不能调整,否则就叠加两次了
                if (!StringUtils.equals(lastPrefix, prefix)) {
                    String newApiDocsPath = prefix + apiDocsPath;
                    apiDocs.setPath(newApiDocsPath);
                    String newConfigUrl = RegExUtils.replaceFirst(configUrl, apiDocsPath, newApiDocsPath);
                    swaggerUiConfigParameters.setConfigUrl(newConfigUrl);
                    lastPrefix = prefix;
                }
            } else {
                // 不是通过网关转发
                // 上一次是通过网关转发,即需要重新调整
                if (!StringUtils.equals(lastPrefix, prefix)) {
                    // 去掉转发时,网关添加的路径
                    String newApiDocsPath = RegExUtils.replaceFirst(apiDocsPath, lastPrefix, "");
                    apiDocs.setPath(newApiDocsPath);
                    String newConfigUrl = RegExUtils.replaceFirst(configUrl, apiDocsPath, newApiDocsPath);
                    swaggerUiConfigParameters.setConfigUrl(newConfigUrl);
                    lastPrefix = prefix;
                }
            }
            return super.transform(request, resource, transformerChain);
        }

        /**
         * 获取网关转发时,url上添加的前缀
         *
         * @param request
         * @return java.lang.String
         * @author
         * @date 2024/6/19 15:27
         */
        public String getPrefix(HttpServletRequest request) {
            // 获取网关转发的服务名
            return Optional.ofNullable(request.getHeader("x-forwarded-prefix")).orElse(Strings.EMPTY);
        }
    }
}

\( \color{red} 方案生效。 \)


noname
314 声望49 粉丝

一只菜狗