上一章我们之前讲解了在单个服务中部署swagger,但每次都需要在不同的端口中访问不同服务的swagger-ui。那么本期我们就来讲解一下,如何从一个统一的入口访问不同服务的swagger
1. 思路我们之前讲过网关的概念,如果不清楚的可以查看之前的博文。那么想象一下,我们是不是可以从一个统一的入口访问接口文档,然后通过路由转发将实际请求转发到对应的微服务上
如果看过之前讲网关gateway这篇内容的同学,听到这里是不是有点思路了?这不就是网关路由转发吗?统一的入口就是网关的入口。再根据服务名转发到不同的微服务中的swagger-ui。
那么我们就可以把网关作为统一入口,同时也在网关服务中配置上swagger,将网关作为swagger-server。各个微服务中也部署各自的单机版的swagger,作为swagger-client。之后会通过路由转发将对swagger的请求转发到各个微服务中。
在开始具体实现之前,先要给大家说明几个概念,帮助大家理解后续的代码。首先我们的swagger文档信息实际上是通过v2/api-docs这个接口获取的,这个接口是swagger自带的。
我们可以调用一个微服务的v2/api-docs接口试试:
会发现他返回的json数据,就是我们要在页面中展示的接口文档数据。所以我们通过网关来实现swagger的接口转发,实际上转发的就是v2/api-docs接口
这也是一个接口地址,默认这个接口获取的是本服务的api-docs访问路径,我们可以通过重写这个接口实现获取到所有微服务的api-docs访问路径。
本机服务的swagger-resources接口调用
网关中重写后的swagger-resources接口调用
具体针对这个接口的实现,我们在下面的的实操中讲解
1、在各微服务中部署单机版swagger,不清楚怎么部署的请看上一篇: 接口文档自动生成器swagger详解 上篇
之后的操作均在网关服务中进行!!!
2、网关服务中引入依赖
目前swagger官方已经更新到了swagger3了,但是大多数开发中仍然在使用swagger2,所以我们这里使用swagger2
io.springfoxspringfox-swagger22.9.2io.springfoxspringfox-swagger-ui2.9.2
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 Listget() { Listresources = new ArrayList<>(); Listroutes = 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 Monofilter(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 Monofail(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的配置封装成一个工具服务,只需要引入这个服务,就不用再单独配置了,这一点大家可以先尝试看看,我们会在后续的讲解中演示
gateway聚合swagger代码
关注公众号 Elasticsearch之家,了解更多新鲜内容