Bootstrap

swagger在微服务下的实战应用(微服务整合、token认证、规范化入参)

前言

6月19入的职到现在的7月27号,没错我已经实习了1个多月了,emmm怎么来说呢,拿我在学校敲代码的状态来比喻吧,如果把我在学校敲的代码比喻成一匹矫健驰骋在草原的黑马,那么我在公司敲的代码就更像是正规军,驰骋在赛场的汗血宝马了。

个人所感

以前在学校没怎么重视网络编程这块,即使以前有看过springCloud、duuble相关的视频,但是当时看视频的时候觉着这样开发项目太繁琐了,也没有坚持去系统化的去总结这一块的内容。以前我也刷到过牛客网上那些牛逼简历上写的项目是手写RPC框架、基于scoket开发的实时在线聊天系统相关的项目,当时觉着这些技术简直可以用俩个字“牛逼”来形容。当时本人痴迷于一个集合类、spring框架的源码分析就没有太注意这一块的技术。但是现在在公司开发的是一个微服务项目,核心业务都是涉及到网络编程相关的,迫于压力也是对于技术的一种热爱,于是我现在研究起了微服务相关框架的使用、源码、scoket、netty、dubbo、springCloud这些技术了,后续会陆续更新相关文章。

模块间依赖关系

email:依赖payment模块
gateway:网关负责路由相关到对应的实例(注册中心用的nacos)
payment:单独的模块
在这里插入图片描述

email、payment模块添加swagger依赖支持

 <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    <dependency>
        <groupId>io.swagger</groupId>
        <artifactId>swagger-annotations</artifactId>
        <version>1.5.20</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>

email、payment模块开启swagger模块配置

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.web.context.request.async.DeferredResult;
import springfox.bean.validators.configuration.BeanValidatorPluginsConfiguration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.ResponseMessageBuilder;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.ResponseMessage;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

import java.util.Arrays;

@Configuration
@EnableSwagger2
@Import(BeanValidatorPluginsConfiguration.class)
public class Swagger2Config {

	@Bean
	public Docket customDocket() {
		return new Docket(DocumentationType.SWAGGER_2)
				.genericModelSubstitutes(DeferredResult.class)
				.useDefaultResponseMessages(false)
				.forCodeGeneration(false)
				.pathMapping("/")
				.apiInfo(apiInfo())
				.select()
				.build();
	}

	private ApiInfo apiInfo() {
		return new ApiInfoBuilder()
				.title("接口文档title")
				.description("接口文档说明")
				.version("1.0.0")
				.build();
	}
}

网关模块配置(重点)

添加swagger接口支撑

/**
 * zzh
 */
@RestController
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)));
    }

}

对swagger请求进行过滤(可有可无)

/**
 * zzh
 */
@Component
public class SwaggerHeaderFilter extends AbstractGatewayFilterFactory {
 
    private static final String HEADER_NAME = "X-Forwarded-Prefix";

    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            String path = request.getURI().getPath();
            if (!StringUtils.endsWithIgnoreCase(path, SwaggerProvider.API_URI)) {
                return chain.filter(exchange);
            }
            String basePath = path.substring(0, path.lastIndexOf(SwaggerProvider.API_URI));
            ServerHttpRequest newRequest = request.mutate().header(HEADER_NAME, basePath).build();
            ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
            return chain.filter(newExchange);
 
        };
 
    }
}

全局swagger配置(放在网关模块)

/**
 * 全局swagger配置
 */
@Component
@Primary
@AllArgsConstructor
public class SwaggerProvider implements SwaggerResourcesProvider {
    //注意这个地址
    public static final String API_URI = "/v2/api-docs?urls.primaryName";
    private final RouteLocator routeLocator;
    private final GatewayProperties gatewayProperties;

    /**
     * swagger会与返回值中的SwaggerResource进行路径匹配,如果匹配不到那么会出现swagger页面404无法访问的情况
     *
     * @return
     */
    @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(routeDefinition -> routeDefinition.getPredicates().stream()
                        .filter(predicateDefinition -> ("Path").equalsIgnoreCase(predicateDefinition.getName()))
                        .forEach(predicateDefinition -> resources.add(swaggerResource(routeDefinition.getId(), predicateDefinition.getArgs().get(NameUtils.GENERATED_NAME_PREFIX + "0")
                                //swagger API地址
                                .replace("/**", API_URI + routeDefinition.getId())))));
        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;
    }
}

网关路由配置

server:
  port: 8005
spring:
  application:
    name: gateway
  cloud:
    discovery:
      locator:
        enabled: true
    gateway:
      routes:
        - id: email_id
          uri: lb://email
          predicates:
            - Path=/email/**
          filters:
            - name: SwaggerHeaderFilter
            - StripPrefix=1
        - id: payment_id
          uri: lb://payment
          predicates:
            - Path=/payment/**
          filters:
            - name: SwaggerHeaderFilter
            - StripPrefix=1
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
      config:
        server-addr: 127.0.0.1:8848
mybatis-plus:
  mapper-locations: classpath:mapperXml/*.xml

全局token验证、方式一

在swagger配置文件中加入如下配置,每次请求都会把 name为 X-Auth-Token 的参数放入请求中进行请求

/**
 * 对每个接口都添加token认证
 *
 * @return
 */
private List<Parameter> setHeaderToken() {
    ParameterBuilder tokenPar = new ParameterBuilder();
    List<Parameter> pars = new ArrayList<>();
    tokenPar.name("X-Auth-Token").description("token").modelRef(new ModelRef("string")).parameterType("header").required(false).build();
    pars.add(tokenPar.build());
    return pars;
}

加入swagger配置
在这里插入图片描述

全局token验证、方式二

一次添加模块下的所有接口下次请求都会携带 token


    @Bean
    public Docket customDocket() {
        // 配置全局参数返回状态
        java.util.List<ResponseMessage> resMsgList = Arrays.asList(
                new ResponseMessageBuilder().code(200).message("成功!").build(),
                new ResponseMessageBuilder().code(-1).message("失败!").build(),
                new ResponseMessageBuilder().code(401).message("参数校验错误!").build(),
                new ResponseMessageBuilder().code(403).message("没有权限操作,请后台添加相应权限!").build(),
                new ResponseMessageBuilder().code(500).message("服务器内部异常,请稍后重试!").build(),
                new ResponseMessageBuilder().code(501).message("请登录!").build());
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo()).select()
                .apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
                .paths(PathSelectors.any())
                .build()
                .globalResponseMessage(RequestMethod.GET, resMsgList)
                .globalResponseMessage(RequestMethod.POST, resMsgList)
                .globalResponseMessage(RequestMethod.PUT, resMsgList)
                .globalResponseMessage(RequestMethod.DELETE, resMsgList)
                .globalOperationParameters(setHeaderToken())
                .securityContexts(securityContexts());
    }
	/**
     * 以下代码添加全局token配置
     *
     * @return
     */
    private List<ApiKey> security() {
        ArrayList list = new ArrayList();
        list.add(new ApiKey("Authorization", "Authorization", "header"));
        return list;
    }

    private List<SecurityContext> securityContexts() {
        List<SecurityContext> securityContexts = new ArrayList<>();
        securityContexts.add(
                SecurityContext.builder()
                        .securityReferences(defaultAuth())
                        .forPaths(PathSelectors.regex("^(?!auth).*$"))
                        .build());
        return securityContexts;
    }

    private List<SecurityReference> defaultAuth() {
        AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
        AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
        authorizationScopes[0] = authorizationScope;
        List<SecurityReference> securityReferences = new ArrayList<>();
        securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
        return securityReferences;
    }

在这里插入图片描述

规范化入参

我们的post请求尽量使用vo接受参数,不要用map(网上有map接受的方法我也试过,但是放在生产线上会爆bug不推荐)、多个@Requestparam去接受参数,后期不好整理swagger文档!!!
在这里插入图片描述
在这里插入图片描述
vo接受参数的好处:前端调试可以使用json传值,并且有model注释可以看传的值是什么意思。
在这里插入图片描述

在这里插入图片描述

坑一 swagger页面404

在这里插入图片描述
观察到正确的swagger地址有尾缀
在这里插入图片描述
解决办法一:访问swagger的地址加上尾缀
在这里插入图片描述
解决办法二:去除前缀这个必须加上

在这里插入图片描述

坑二(swagger页面有了、访问接口出现碟名)

一开始我以为是由于网关的配置引起的,也就是下面这种情况:网关配置了路由/a/** , 请求/a/b/c经过网关,由于前缀 /a 匹配上了,/a/b/c的请求会替代 /** 的位置,经过网关转发后的请求实际上是 /a/a/b/c。但是后来想着网关的路由匹配规则也太变态了吧,都已经模糊匹配上了,还给我恶意拼接上去。经过本人实测 gateWay 不是这么个路由匹配规则,出现路径碟名的bug只存在于swagger中,下文有详细描述gateWay路由匹配规则。

在这里插入图片描述

gateWay路由匹配

下图对应payment模块controller中的一个方法,返回值包含了请求路径以及端口号

在这里插入图片描述

下图对应email模块controller中的一个方法,返回值包含了请求路径以及端口号
在这里插入图片描述

gateWay 中 payment 对应的路由配置 注释掉 - StripPrefix=1,而 email 对应的路由配置开启 - StripPrefix=1,我们通过直接走 gateWay 网关来调用我们的接口,来观察经过网关分发后,到达接口的真实请求路径、端口号是多少。并且我们通过 -Dserver.port=7009 指令,分别对email、payment模块开启端口不同的俩个实例,来检测 gateWay 的负载均衡功能是否生效
在这里插入图片描述

事实证明: 我们的请求路径为 /payment/getUser/1,经过网关转发之后的请求路径依然为 /payment/getUser/1 而不是/payment/payment/getUser/1 且负载均衡也生效
在这里插入图片描述
在这里插入图片描述
事实证明: 我们的请求路径为 /email/email/getUser/1、/email/getUser/1,由于配置了去除前缀的规则,经过网关转发之后的请求路径分别为 /email/getUser/1、/getUser/1 因此下图一为404,下图二、三为正常结果
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

警戒(总结)

不要被swagger蒙蔽了我们的双眼,gateWay路由匹配规则如下

  • 去除前缀执行的时机:经过网关分发后去除前缀。例如 /a/a/b/c 到达了网关,经过网关分发后到达接口的请求为 /a/b/c
  • 路由匹配规则: /a/** 会匹配以 /a 开头的请求,路由到对应的服务器实例上面去。但是不是这么一种情况, /a/b的请求打到网关 (gateWay) ,匹配上了 /a/** 后,经过网关分发后的请求依然是/a/b , 而不是/a/a/b,至于swagger页面中的碟名这个效果 个人认为是个 bug,上文有证实。

上文为我亲测后推导出来的个人想法的总结,如有不对之处,望各位读者斧正

小咸鱼的技术窝

关注不迷路,日后分享更多技术干货,B站、CSDN、微信公众号同名,名称都是(小咸鱼的技术窝)更多详情在主页
在这里插入图片描述

;