SpringCloud网关下Springdoc聚合API配置

之前使用SpringBoot一般会使用SpringFox Swagger进行API接口管理,但是如果基于Spring Cloud开发,SpringFox 库最重要的问题是缺乏对WebFlux 构建的反应式 API的支持,并且也没有API聚合功能。通过搜索,我们发现了SpringDoc,支持WebMvc支持,WebFlux支持,Javadoc 支持等,支持非常广,可以参考SpringDoc官网。下面就来测试下

微服务整合

只需要配置gradle依赖springdoc-openapi-ui即可.

1
implementation 'org.springdoc:springdoc-openapi-ui'

然后我们可以在Nacos中定义common.yaml,将springdoc配置一下,如下

1
2
3
4
5
6
7
8
9
springdoc:
api-docs:
enabled: true ## 文档生成
swagger-ui:
enabled: true ## 页面展示
packages-to-scan: com.mayahx.stcm # 配置扫描的包
cache:
disabled: false
pre-loading-enabled: true

Swagger UI 页面将http://server:port/context-path/swagger-ui.html在以下 url 提供,OpenAPI 描述将在以下 json 格式的 URL 中提供:http://server:port/context-path/v3/api-docs。
我们打开下页面看看

可以看到文档已经生成,已经实现单体Swagger一样的功能。但是微服务下会有多个模块,我们可以将不同模块的API给前端使用,但是这种方式有点麻烦,所以我们希望给一个聚合页面,让前端可以在一个主页面中就将接口调试完成。

Spring Cloud Gateway集成

因为Spring Cloud Gateway使用的响应式编程,所以我们需要集成webflux,如下所示

1
2
implementation 'org.springdoc:springdoc-openapi-webflux-ui'
implementation 'org.springdoc:springdoc-openapi-webmvc-core'

Springdoc支持使用GroupedOpenApi分组机制实现将OpenAPI 定义分成具有给定名称的不同组,
我们需要声明一个GroupOpenAPI Bean列表。这是网关服务中负责创建由网关处理的 OpenAPI 资源列表的代码片段。首先,我们使用RouteDefinitionLocator。然后我们获取每个路由的 id 并将其设置为组名。因此,我们在 path 下有多个 OpenAPI 资源/v3/api-docs/{SERVICE_NAME},例如/v3/api-docs/order。配置如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
@Slf4j
@Profile("!prod")
public class ApiConfig {
@Bean
@Lazy(false)
public List<GroupedOpenApi> apis(SwaggerUiConfigParameters swaggerUiConfigParameters, RouteDefinitionLocator locator) {
List<GroupedOpenApi> groups = new ArrayList<>();
List<RouteDefinition> definitions = locator.getRouteDefinitions().collectList().block();
if (definitions!=null&&definitions.size()>0) {
for (RouteDefinition definition : definitions) {
if (!Objects.equals(definition.getId(), "openapi")) {
swaggerUiConfigParameters.addGroup(definition.getId());
}
}
}
return groups;
}
}

通过上面的代码实现分组聚合后,地址确是/v3/api-docs/{SERVICE_NAME}结尾,比如订单服务的API就变成了http://192.168.0.107:8000/v3/api-docs/order,这样是无法访问到我们子服务的API DOC的,所以需要在网关配置路由重写。如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedMethods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
discovery:
locator:
lower-case-service-id: true
routes:
- id: order #路由的ID
uri: lb://order #匹配后路由地址
predicates: # 断言
- Path=/order/**
- id: pay
uri: lb://pay
predicates:
- Path=/pay/**
- id: stock
uri: lb://stock
predicates:
- Path=/stock/**
- id: openapi
uri: http://localhost:${server.port}
predicates:
- Path=/v3/api-docs/**
filters:
- RewritePath=/v3/api-docs/(?<path>.*), /$\{path}/v3/api-docs

globalcors是网关的跨域配置,openapi路由代表路由规则的重写。下面我们就可以看看集成后的样子

这样Springdoc就可以在右上角看到我们在网关中配置的端点,并且可以选择端点进行测试了。

跨域配置

API聚合完成后,我们进行接口调试会发现有两个问题

  • servers的API地址是内网地址,远程调内网地址肯定是不通的。
  • 接口调试会产生跨域访问cors问题。

Server内网地址解决

发生这个问题时,我尝试修改/ets/hosts域名映射的方式,但是没有生效,所以可以选择另外一种方式,在服务中定义server,将配置放在公共模块中。代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
@Profile("!prod")
@Configuration
public class OpenApiConfig {
public static final String LOCAL_HOST = "127.0.0.1";
public static final String DEV_HOST = "150.158.188.244";
public static final String DEV = "dev";
public static final String DEBUG = "debug";
public static final String TEST = "test";
public static final String URL_SEPARATOR = "/";
public static final String URL_HTTP = "http://";
public static final String URL_PORT = ":";

@Value("${server.port}")
public int port;

@Value("${spring.application.name}")
public String path;

@Value("${spring.profiles.active}")
public String active;

@Bean
public OpenAPI openAPI(){
Server server = new Server();
StringBuilder builder = new StringBuilder();
builder.append(URL_HTTP);
switch (active){
case DEV:
builder.append(LOCAL_HOST);
break;
case DEBUG:
case TEST:
builder.append(DEV_HOST);
break;
}
builder.append(URL_PORT);
builder.append(port);
builder.append(URL_SEPARATOR);
builder.append(path);
server.setUrl(builder.toString());
return new OpenAPI().addServersItem(server)
.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION))
.schemaRequirement(HttpHeaders.AUTHORIZATION,new SecurityScheme().type(SecurityScheme.Type.HTTP)
.scheme("bearer").bearerFormat("JWT").in(SecurityScheme.In.HEADER).name(HttpHeaders.AUTHORIZATION));
}
}

代码中将dev环境的server配置为localhost,然后将debug调试版本,test测试版本的server设置为公网IP。这样在Gateway聚合时,针对不同的版本就可以展示不同的Server。

跨域解决

针对本地通过网关访问子服务的跨域,有四种方式解决

  • CorsFilter
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class CorsConfig {
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setMaxAge(3600L);
corsConfiguration.setAllowCredentials(true);
return corsConfiguration;
}

@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig());
return new CorsFilter(source);
}
}
  • 重写WebMvcConfigurer
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Configuration
public class WebMvcConfg implements WebMvcConfigurer {

@Override
public void addCorsMappings(CorsRegistry registry) {
//设置允许跨域的路径
registry.addMapping("/**")
//设置允许跨域请求的域名
//当**Credentials为true时,**Origin不能为星号,需为具体的ip地址【如果接口不带cookie,ip无需设成具体ip】
.allowedOrigins("http://localhost:9527", "http://127.0.0.1:9527", "http://127.0.0.1:8082", "http://127.0.0.1:8083")
//是否允许证书 不再默认开启
.allowCredentials(true)
//设置允许的方法
.allowedMethods("*")
//跨域允许时间
.maxAge(3600);
}
}

  • 使用注解(@CrossOrigin)
1
2
3
4
5
6
@Controller
@RequestMapping("/admin/sysLog")
@CrossOrigin
public class SysLogController {

}
  • 手工设置响应头(HttpServletResponse )
1
2
3
4
5
6
@RequestMapping("/test")
@ResponseBody
public String test(){
response.addHeader("Access-Control-Allow-Origin", "http://localhost:8080");
return "success";
}

这里我使用的是@CrossOrigin注解,因为项目中定义了抽象类BaseController,对HttpServletRequest,HttpServletResponse做了一些操作。

SpringDoc注解介绍

如果觉得使用注解的方式侵入性较强,我们还可以使用注释的方式。后面有时间再来搞一下。

作者

Labradors

发布于

2022-03-21

更新于

2022-03-25

许可协议

评论