springcloud:gateway聚合swagger 下篇(十二)

3,773 阅读5分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

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的配置封装成一个工具服务,只需要引入这个服务,就不用再单独配置了,这一点大家可以先尝试看看,我们会在后续的讲解中演示 在这里插入图片描述

演示代码地址

gateway聚合swagger代码

关注公众号 Elasticsearch之家,了解更多新鲜内容