SpringCloud-gateway-nacos-swagger踩坑记录

2022-09-26 17:50:50 浏览数 (1)

SpringCloud-gateway-nacos-swagger

gateway聚合各服务模块,以及遇到的坑。

1. 背景

​ 在gateway中集成各个模块,然后接入swagger方便测试各模块接口,其中sunshine-common是放入一些通用组件和配置的,swagger的配置就在这里面。sunshine-gateway即网关模块,在网关通过nacos服务注册发现,将请求路由到各个模块中。

2. 具体代码

2.1. swagger的配置

swagger的配置放在common模块中了,这里只是参考了一般的配置,并且加了Token的认证Header,之后各个模块只要引入了common模块并扫描就默认配置了swagger,具体如下:

代码语言:javascript复制
/**
 * 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,重写该接口,具体代码如下:

代码语言:javascript复制
@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接口:

代码语言:javascript复制
@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过滤器。具体的相关配置代码如下

代码语言:javascript复制
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模块为例,配置了如下信息:

代码语言:javascript复制
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认证。因此后面为了解决这个问题,修改配置文件的修改如下:

代码语言:javascript复制
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

​ 开发环境中为了能够测试接口就可以这么处理,但应该也有更好的方法。但生产环境中各模块的接口信息是不可以暴露出来的,因此不配置这些也无所谓了。

0 人点赞