0. 引言
上一章我们之前讲解了在单个服务中部署swagger,但每次都需要在不同的端口中访问不同服务的swagger-ui。那么本期我们就来讲解一下,如何从一个统一的入口访问不同服务的swagger
1. 思路
我们之前讲过网关的概念,如果不清楚的可以查看之前的博文。那么想象一下,我们是不是可以从一个统一的入口访问接口文档,然后通过路由转发将实际请求转发到对应的微服务上
如果看过之前讲网关gateway这篇内容的同学,听到这里是不是有点思路了?这不就是网关路由转发吗?统一的入口就是网关的入口。再根据服务名转发到不同的微服务中的swagger-ui。
那么我们就可以把网关作为统一入口,同时也在网关服务中配置上swagger,将网关作为swagger-server。各个微服务中也部署各自的单机版的swagger,作为swagger-client。之后会通过路由转发将对swagger的请求转发到各个微服务中。
1.1 什么是 v2/api-docs?
在开始具体实现之前,先要给大家说明几个概念,帮助大家理解后续的代码。首先我们的swagger文档信息实际上是通过v2/api-docs
这个接口获取的,这个接口是swagger自带的。
我们可以调用一个微服务的v2/api-docs
接口试试:
会发现他返回的json数据,就是我们要在页面中展示的接口文档数据。所以我们通过网关来实现swagger的接口转发,实际上转发的就是v2/api-docs
接口
1.2 什么是swagger-resources?
这也是一个接口地址,默认这个接口获取的是本服务的api-docs访问路径,我们可以通过重写这个接口实现获取到所有微服务的api-docs访问路径。
本机服务的swagger-resources接口调用
网关中重写后的swagger-resources接口调用
具体针对这个接口的实现,我们在下面的的实操中讲解
2. 完整实现
1、在各微服务中部署单机版swagger,不清楚怎么部署的请看上一篇:
接口文档自动生成器swagger详解 上篇
之后的操作均在网关服务中进行!!!
2、网关服务中引入依赖
目前swagger官方已经更新到了swagger3了,但是大多数开发中仍然在使用swagger2,所以我们这里使用swagger2
<!-- swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!-- swagger-ui -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
3、创建swagger配置文件SwaggerConfig,该类主要用于提供两个bean: securityConfiguration和uiConfiguration。这两个bean在后续会被调用
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.swagger.web.SecurityConfiguration;
import springfox.documentation.swagger.web.SecurityConfigurationBuilder;
import springfox.documentation.swagger.web.UiConfiguration;
import springfox.documentation.swagger.web.UiConfigurationBuilder;
/**
* swagger配置类
* @author whx
* @date 2022/4/22
*/
@Configuration
public class SwaggerConfig {
@Bean
public SecurityConfiguration securityConfiguration(){
return SecurityConfigurationBuilder.builder().build();
}
@Bean
public UiConfiguration uiConfiguration(){
return UiConfigurationBuilder.builder().build();
}
}
4、再创建swagger的数据资源类,这个类主要用于提供swagger各种资源。
在访问swagger-ui.html页面的时候,主要就是通过访问以下接口来获取文档数据
import lombok.AllArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import springfox.documentation.swagger.web.*;
import java.util.List;
import java.util.Optional;
/**
* swagger的数据接口
* 在访问swagger-ui中会拉去此接口的数据
* @author whx
* @date 2022/4/22
*/
@RestController
@RequestMapping("/swagger-resources")
@AllArgsConstructor
public class SwaggerHandler {
private final SecurityConfiguration securityConfiguration;
private final UiConfiguration uiConfiguration;
private final SwaggerResourcesProvider swaggerResourcesProvider;
@GetMapping("/configuration/security")
public Mono<ResponseEntity<SecurityConfiguration>> securityConfiguration(){
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(securityConfiguration).orElse(SecurityConfigurationBuilder.builder().build()), HttpStatus.OK));
}
@GetMapping("configuration/ui")
public Mono<ResponseEntity<UiConfiguration>> uiConfiguration(){
return Mono.just(new ResponseEntity<>(
Optional.ofNullable(uiConfiguration).orElse(UiConfigurationBuilder.builder().build()),HttpStatus.OK));
}
@GetMapping
public Mono<ResponseEntity<List<SwaggerResource>>> swaggerResources(){
return Mono.just((new ResponseEntity<>(swaggerResourcesProvider.get(),HttpStatus.OK)));
}
}
5、创建swagger资源配置类,该类主要用于聚合其他微服务中Swagger的api-docs访问路径
import lombok.AllArgsConstructor;
import org.springframework.cloud.gateway.config.GatewayProperties;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.support.NameUtils;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import springfox.documentation.swagger.web.SwaggerResource;
import springfox.documentation.swagger.web.SwaggerResourcesProvider;
import java.util.*;
/**
* 聚合swagger配置类
* @author whx
* @date 2022/4/22
*/
@Primary
@Component
@AllArgsConstructor
public class Swagger2ResourceProvider implements SwaggerResourcesProvider {
/**
* swagger默认的url后缀
*/
private static final String API_URI = "v2/api-docs";
/**
* 网关配置项,对应配置文件中配置的spring.cloud.gateway所有子项
*/
private final GatewayProperties gatewayProperties;
/**
* 网关路由
*/
private final RouteLocator routeLocator;
@Override
public List<SwaggerResource> get() {
List<SwaggerResource> resources = new ArrayList<>();
List<String> routes = new ArrayList<>();
routeLocator.getRoutes().subscribe(route -> routes.add(route.getId()));
// 遍历配置文件中配置的所有服务
gatewayProperties.getRoutes().stream()
// 过滤同名服务
.filter(routeDefinition -> routes.contains(routeDefinition.getId()))
.forEach(route -> route.getPredicates().stream()
// 忽略配置文件中断言中配置的Path为空的配置项
.filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
// 将Path中的路由地址由**改为v2/api-docs,swagger就是通过这个地址来获取接口文档数据的,可以通过访问:ip:port/v2/api-docs来体会接口数据
.forEach(predicateDefinition -> resources
.add(swaggerResource(route.getId(), predicateDefinition.getArgs()
.get(NameUtils.GENERATED_NAME_PREFIX + "0").replace("**", API_URI)))));
return resources;
}
private SwaggerResource swaggerResource(String name, String location) {
SwaggerResource swaggerResource = new SwaggerResource();
swaggerResource.setName(name);
swaggerResource.setLocation(location);
swaggerResource.setSwaggerVersion("2.0");
return swaggerResource;
}
}
6、修改网关配置文件,将需要聚合swagger的微服务路由配置上
spring:
cloud:
routes:
# id 显示到页面上的名称
- id: 商品服务 product-server
# lb://xxx, xxx为服务名
uri: lb://product-server
predicates:
# Path=/xxx/**,xxx为服务名
- Path=/product-server/**
- id: 订单服务 order-server
uri: lb://order-server
predicates:
- Path=/order-server/**
7、如果网关没有配置鉴权的话,到这里就配置完成了,但是因为我们的网关里一般都配置了鉴权,所以我们还需要swagger的相关路径忽略鉴权。这里根据之前博客中的网关模块来演示
添加无需鉴权的路径 "/**/v2/api-docs","/**/swagger-ui.html","/**/swagger-resources/**"
private final String[] skipAuthUrls = new String[]{"/login/check","/user/register",
"/**/v2/api-docs","/**/swagger-ui.html","/**/swagger-resources/**"};
并且将之前的过滤方法调整为正则匹配
public boolean isSkipUrl(String url) {
if(StringUtils.isEmpty(url)){
return false;
}
AntPathMatcher matcher = new AntPathMatcher();
for (String skipAuthUrl : skipAuthUrls) {
if(matcher.match(skipAuthUrl, url)){
return true;
}
}
return false;
}
完整代码
import com.example.gatewaytoken.util.JWTUtil;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
/**
* @author whx
* @date 2022/4/12
*/
@Component
public class TokenFilter implements GlobalFilter, Ordered{
private final String[] skipAuthUrls = new String[]{"/login/check","/user/register",
"/**/v2/api-docs","/**/swagger-ui.html","/**/swagger-resources/**"};
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String url = exchange.getRequest().getURI().getPath();
// 跳过不需要验证的路径
if (isSkipUrl(url)) {
return chain.filter(exchange);
}
ServerHttpResponse response = exchange.getResponse();
// 从请求头中取得token
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
// token是否为空
if (StringUtils.isEmpty(token)) {
return fail(response,"token为空,鉴权失败");
}
// 请求中的token是否有效
String userId = JWTUtil.getVal(token,"userId").toString();
if(StringUtils.isEmpty(userId)){
return fail(response,"token不合法");
}
// 校验token是否过期
if(JWTUtil.isExpiration(token)){
return fail(response,"token已过期");
}
//如果各种判断都通过,执行chain上的其他业务逻辑
return chain.filter(exchange);
}
private Mono<Void> fail(ServerHttpResponse response,String message){
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type","application/json;charset=UTF-8");
DataBuffer buffer = response.bufferFactory().wrap(message.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Flux.just(buffer));
}
/**
* 判断当前访问的url是否开头URI是在配置的忽略url列表中
*
* @param url
* @return
*/
public boolean isSkipUrl(String url) {
if(StringUtils.isEmpty(url)){
return false;
}
AntPathMatcher matcher = new AntPathMatcher();
for (String skipAuthUrl : skipAuthUrls) {
if(matcher.match(skipAuthUrl, url)){
return true;
}
}
return false;
}
@Override
public int getOrder() {
return 0;
}
}
8、启动gateway及其他添加了swagger的微服务
9、访问:http://localhost/swagger-ui.html
如图所示,我们可以在右上角切换文档服务。至此我们的gateway聚合swagger就配置完成了。
当然我们还可以把swagger的配置封装成一个工具服务,只需要引入这个服务,就不用再单独配置了,这一点大家可以先尝试看看,我们会在后续的讲解中演示
演示代码地址
关注公众号 Elasticsearch之家,了解更多新鲜内容