SpringCloud-gateway-nacos-swagger
gateway聚合各服务模块,以及遇到的坑。
1. 背景
在gateway中集成各个模块,然后接入swagger方便测试各模块接口,其中sunshine-common
是放入一些通用组件和配置的,swagger
的配置就在这里面。sunshine-gateway
即网关模块,在网关通过nacos
服务注册发现,将请求路由到各个模块中。
2. 具体代码
2.1. swagger的配置
swagger
的配置放在common
模块中了,这里只是参考了一般的配置,并且加了Token的认证Header,之后各个模块只要引入了common
模块并扫描就默认配置了swagger
,具体如下:
/**
* Swagger2API文档的配置
*/
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket createRestApi() {
new ParameterBuilder().name("real_source_ip").description("本机局域网IP").
modelRef(new ModelRef("string")).parameterType("header").required(false).build();
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
//为当前包下controller生成API文档
.apis(RequestHandlerSelectors.basePackage("com.huashi.sunshine"))
//为有@Api注解的Controller生成API文档
.apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
//为有@ApiOperation注解的方法生成API文档
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.paths(PathSelectors.any())
.build()
.securitySchemes(securitySchemes())
.securityContexts(securityContexts());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("sunshine")
.description("阳光影院接口文档")
.contact("xxy")
.version("1.0.0")
.build();
}
private List<ApiKey> securitySchemes() {
// 设置请求头信息
// 这里是为了在测试请求中添加认证token用的,这里可不管
List<ApiKey> result = new ArrayList<>();
ApiKey apiKey = new ApiKey("Authorization", "Authorization", "header");
result.add(apiKey);
return result;
}
private List<SecurityContext> securityContexts() {
//设置需要登录认证的路径
List<SecurityContext> result = new ArrayList<>();
result.add(getContextByPath("/api/user/.*"));
result.add(getContextByPath("/auth/.*"));
result.add(getContextByPath("/sunshine-movie-comment/.*"));
result.add(getContextByPath("/sunshine-cinema-comment/.*"));
return result;
}
private SecurityContext getContextByPath(String pathRegex){
return SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex(pathRegex))
.build();
}
private List<SecurityReference> defaultAuth() {
List<SecurityReference> result = new ArrayList<>();
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
result.add(new SecurityReference("Authorization", authorizationScopes));
return result;
}
}
2.2. gateway聚合各模块接口
由于系统是采用nacos做服务中心的,因此gateway要去服务中心拉取各个服务的api信息,生成文档。springfox-swagger
提供的分组接口是swagger-resource
,重写该接口,具体代码如下:
@Component
@Primary
@AllArgsConstructor
public class SwaggerProvider implements SwaggerResourcesProvider {
public static final String API_URI = "/v2/api-docs";
private final RouteLocator routeLocator;
@Value("${spring.application.name}")
private String self;
public SwaggerProvider(RouteLocator routeLocator) {
this.routeLocator = routeLocator;
}
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> routeHosts = new ArrayList<>();
// 由于我的网关采用的是负载均衡的方式,因此我需要拿到所有应用的serviceId
// 获取所有可用的host:serviceId
routeLocator.getRoutes().filter(route -> route.getUri().getHost() != null)
.filter(route -> !self.equals(route.getUri().getHost())) //排除自己
.subscribe(route -> routeHosts.add(route.getUri().getHost()));
// 记录已经添加过的server,存在同一个应用注册了多个服务在nacos上
Set<String> dealed = new HashSet<>();
routeHosts.forEach(instance -> {
// 注意,本次坑出现在这
// 拼接url,样式为/serviceId/v2/api-info,当网关调用这个接口时,会自动通过负载均衡寻找对应的主机
String url = "/" instance API_URI;
if (!dealed.contains(url)) {
dealed.add(url);
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setUrl(url);
swaggerResource.setName(instance);
resources.add(swaggerResource);
}
});
return resources;
}
}
swagger
接口:
@RestController
public class SwaggerHandler {
@Autowired(required = false)
private SecurityConfiguration securityConfiguration;
@Autowired(required = false)
private UiConfiguration uiConfiguration;
private final SwaggerResourcesProvider swaggerResources;
@Autowired
public SwaggerHandler(SwaggerResourcesProvider swaggerResources) {
this.swaggerResources = swaggerResources;
}
@GetMapping("/swagger-resources/configuration/security")
public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@GetMapping("/swagger-resources/configuration/ui")
public Mono<ResponseEntity<UiConfiguration>> uiConfiguration() {
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@GetMapping("/swagger-resources")
public Mono<ResponseEntity> swaggerResources() {
return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
}
@GetMapping("/")
public Mono<ResponseEntity> swaggerResourcesN() {
return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
}
@GetMapping("/csrf")
public Mono<ResponseEntity> swaggerResourcesCsrf() {
return Mono.just((new ResponseEntity<>(swaggerResources.get(), HttpStatus.OK)));
}
}
2.3. gateway路由配置
系统中会对接口进行token
认证,但有些接口是不需要Header Token的,比如用户登陆注册,还有就是本文中的swagger
接口。在2.2
中看到每个服务模块的暴露swagger
接口都为{服务名}/v2/api-docs
的格式,这些路径都必须走自定义的不认证Token过滤器。具体的相关配置代码如下
spring:
cloud:
gateway:
routes:
#放行swagger配置,匹配具体路径,因为swagger发请求会把/{服务名}带上,这样子会走了swagger的过滤配置从而跳过了token鉴权
- id: sunshine-user-swagger-ui
uri: lb://sunshine-user
predicates:
- Path=/sunshine-user/v2/api-docs # user模块的接口信息
filters:
# 访问时跳过第一部分,即真实路径是user模块里面的'/v2/api-docs',因为注册时加了服务名作为标识,防止各个服务的api信息都为'/v2/api-docs'冲突
- StripPrefix=1
- IgnoreGlobalFilter
# 用户不需要权限api
- id: sunshine-user-public
uri: lb://sunshine-user
predicates: # 路由断言
- Path=/user/** # 不需要token认证的路径
filters:
- IgnoreGlobalFilter
- id: sunshine-user-public
uri: lb://sunshine-user
predicates: # 路由断言
- Path=/sunshine-user/user/** # 同样是不需要token认证的路径,为了兼容swagger的接口测试添加的
filters:
- StripPrefix=1
- IgnoreGlobalFilter
# 用户需要权限api
- id: sunshine-user-auth
uri: lb://sunshine-user
predicates: # 路由断言
- Path=/api/user/** # 需要token认证的路径
- id: sunshine-user-auth
uri: lb://sunshine-user
predicates: # 路由断言
- Path=/sunshine-user/api/user/** # 同样是需要token认证的路径,为了兼容swagger的接口测试添加的
filters:
- StripPrefix=1
上面只列出了user
模块的配置,实际配置文件还有其他模块的配置,但整体大同小异,具体效果如下:
3. 一些小坑
以user
模块为例,在实际的业务代码中,需要认证的请求路径为/api/user/**
,但实际在swagger
测试这些请求时都会默认在路径前面带上服务名,这里即变为/sunshine-user/api/user/**
。
但在一开始为了能够各个模块的swagger-api
信息不被全局过滤器过滤,这里以user
模块为例,配置了如下信息:
spring:
cloud:
gateway:
routes:
#放行swagger配置,匹配具体路径,因为swagger发请求会把/{服务名}带上,这样子会走了swagger的过滤配置从而跳过了token鉴权
- id: sunshine-user-swagger-ui
uri: lb://sunshine-user
predicates:
- Path=/sunshine-user/** # user模块的接口信息
filters:
- StripPrefix=1
- IgnoreGlobalFilter
这就导致了在swagger
中测试接口时所有的请求都走了上面这个路由配置,实际能运行,因此这里还配置了- StripPrefix=1
,只是每次请求都跳过了全局过滤器,即跳过token认证。因此后面为了解决这个问题,修改配置文件的修改如下:
spring:
cloud:
gateway:
routes:
- id: sunshine-user-swagger-ui
uri: lb://sunshine-user
predicates:
- Path=/sunshine-user/v2/api-docs # 这里改成了api接口的具体路径
filters:
- StripPrefix=1
- IgnoreGlobalFilter
# 用户不需要权限api
- id: sunshine-user-public
uri: lb://sunshine-user
predicates: # 路由断言
- Path=/user/** # 实际业务的请求路由
filters:
- IgnoreGlobalFilter
- id: sunshine-user-public
uri: lb://sunshine-user
predicates: # 路由断言
- Path=/sunshine-user/user/** # 这里添加了swagger发送请求时的路由项
filters:
- StripPrefix=1
- IgnoreGlobalFilter
开发环境中为了能够测试接口就可以这么处理,但应该也有更好的方法。但生产环境中各模块的接口信息是不可以暴露出来的,因此不配置这些也无所谓了。