Vert.x OpenAPI

Vert.x OpenAPI 继承了 Vert.x Web 用以支持 OpenAPI 3 ,同时为您提供了简便的接口来构建一个符合您接口协议的Vert.x Web 路由器。

Vert.x OpenAPI 可以做到如下事情:

  • 解析并校验您的 OpenAPI 3 协议

  • 根据您的约束来生成路由器(带有正确的路径以及方法)

  • 提供基于您接口协议的请求解析和校验的功能,该功能用 Vert.x Web Validation 实现。

  • 挂载必要的安全处理器

  • 在 OpenAPI 风格和 Vert.x 风格之间转换路径

  • Vert.x Web API Service 来将请求路由到事件总线

使用 Vert.x OpenAPI

要使用Vert.x OpenAPI,您需要添加如下依赖:

  • Maven (在您的 pom.xml 文件):

<dependency>
 <groupId>io.vertx</groupId>
 <artifactId>vertx-web-openapi</artifactId>
 <version>4.1.8</version>
</dependency>
  • Gradle (在您的 build.gradle 文件):

dependencies {
 compile 'io.vertx:vertx-web-openapi:4.1.8'
}

RouterBuilder

RouterBuilder 是这个模块的主要元素,它提供了用来挂载请求处理器的接口,并且生成最终的 Router

要使用 Vert.x Web OpenAPI ,您必须用 RouterBuilder.create 方法并传入您的接口协议来实例化 RouterBuilder

例如从本地文件系统来加载一个约束:

RouterBuilder.create(vertx, "src/main/resources/petstore.yaml")
  .onSuccess(routerBuilder -> {
    // 约束加载成功
  })
  .onFailure(err -> {
    // router builder 初始化失败
  });

您可以从一个远程约束构建一个 router builder :

RouterBuilder.create(
  vertx,
  "https://raw.githubusercontent" +
    ".com/OAI/OpenAPI-Specification/master/examples/v3.0/petstore.yaml")
  .onSuccess(routerBuilder -> {
    // 约束加载成功
  })
  .onFailure(err -> {
    // router builder 初始化失败
  });

您可以通过配置 OpenAPILoaderOptions 以获取私有的远程约束:

OpenAPILoaderOptions loaderOptions = new OpenAPILoaderOptions()
  .putAuthHeader("Authorization", "Bearer xx.yy.zz");
RouterBuilder.create(
  vertx,
  "https://raw.githubusercontent" +
    ".com/OAI/OpenAPI-Specification/master/examples/v3.0/petstore.yaml",
  loaderOptions)
  .onSuccess(routerBuilder -> {
    // 约束加载成功
  })
  .onFailure(err -> {
    // router builder 初始化失败
  });

您可以用 RouterBuilderOptions 来修改 router builder 的各种行为:

routerBuilder.setOptions(new RouterBuilderOptions());

获取operation

为了获取协议中定义的 Operation ,您需要用 operation 方法。 这个方法返回了一个 Operation 对象,您可以既可以用它来获取模型,又可以用来注册处理器。

使用 handler 为一个operation来挂载处理器, 使用 failureHandler 来挂载失败处理器。

您可以 在一个 operation 当中添加多个处理器 ,而不覆盖已经存在的处理器。

例如:

routerBuilder
  .operation("awesomeOperation")
  .handler(routingContext -> {
    RequestParameters params =
      routingContext.get(ValidationHandler.REQUEST_CONTEXT_KEY);
    RequestParameter body = params.body();
    JsonObject jsonBody = body.getJsonObject();
    // 处理请求体
  }).failureHandler(routingContext -> {
  // 处理失败
});
重要

没有 operationId 的话,那么您不能获取到这个operation。 没有 operationId 的operation,会被 RouterBuilder 忽略。

Vert.x OpenAPI 为您挂载了正确的 ValidationHandler ,所以您才可以获取到请求参数和请求体。 请参考 Vert.x Web 校验文档 来学习如何获取请求参数以及请求体,并学习如何管理校验失败的处理方式。

配置定义在 OpenAPI 文档中的 AuthenticationHandler

安全是任何API的一个重要部分。OpenAPI定义了如何在api文档中强调安全性。

所有安全约束信息都在 /components/securitySchemes 组件下。 对于每种类型的身份验证,该对象中的信息是不同的而且是特别指定的 为了避免重复配置,这个模块允许您为从原文档中读取源配置的身份验证处理器提供工厂

举个例子,下面是一个定义了 Basic Authentication 的文档:

openapi: 3.0.0
...
components:
 securitySchemes:
   basicAuth:     # <-- 为这个security scheme随便提供一个名字
     type: http
     scheme: basic

可以这样配置一个工厂:

routerBuilder
  .securityHandler("basicAuth")
  .bindBlocking(config -> BasicAuthHandler.create(authProvider));

尽管这个例子配置起来很简单,但是创建一个身份验证处理器需要配置 例如从一个API Key处理器中获取配置:

 openapi: 3.0.0
 ...
 # 1) 定义key的名字和位置
 components:
   securitySchemes:
     ApiKeyAuth:        # 为这个安全约束随便提供一个名字
       type: apiKey
       in: header       # 可以是"header", "query" 或者 "cookie"
       name: X-API-KEY  # "header", "query" 或者 "cookie"中的名字
routerBuilder
  .securityHandler("ApiKeyAuth")
  .bindBlocking(config ->
    APIKeyHandler.create(authProvider)
      .header(config.getString("name")));

或者你可以配置更多复杂场景的情况,比如需要服务发现的OpenId Connect

openapi: 3.0.0
...
# 1) 定义安全约束的类型和属性
components:
 securitySchemes:
   openId:   # <--- 为这个安全约束随便提供一个名字。就可以利用该名从别处引用。
     type: openIdConnect
     openIdConnectUrl: https://example.com/.well-known/openid-configuration
routerBuilder
  .securityHandler("openId")
  .bind(config ->
    OpenIDConnectAuth
      .discover(vertx, new OAuth2Options()
        .setClientId("client-id") // user provided
        .setClientSecret("client-secret") // user provided
        .setSite(config.getString("openIdConnectUrl")))
      .compose(authProvider -> {
        AuthenticationHandler handler =
          OAuth2AuthHandler.create(vertx, authProvider);
        return Future.succeededFuture(handler);
      }))
  .onSuccess(self -> {
    //创建成功
  })
  .onFailure(err -> {
    // 出了一些问题
  });

这个API被设计为流式的,所以它用起来很简洁,举个例子:

routerBuilder
  .securityHandler("api_key")
  .bindBlocking(config -> APIKeyHandler.create(authProvider))
  .operation("listPetsSingleSecurity")
  .handler(routingContext -> {
    routingContext
      .response()
      .setStatusCode(200)
      .setStatusMessage("Cats and Dogs")
      .end();
  });

// 非阻塞绑定
routerBuilder
  .securityHandler("oauth")
  .bind(config -> OpenIDConnectAuth.discover(vertx, new OAuth2Options(config))
    .compose(oidc -> Future.succeededFuture(
      OAuth2AuthHandler.create(vertx, oidc))))

  .onSuccess(self -> {
    self
      .operation("listPetsSingleSecurity")
      .handler(routingContext -> {
        routingContext
          .response()
          .setStatusCode(200)
          .setStatusMessage("Cats and Dogs")
          .end();
      });
  });

阻塞 vs 非阻塞

从上面的例子可以看出,处理器可以以阻塞或非阻塞的方式添加。 使用非阻塞方式的原因不仅仅是为了支持像 OAuth2 这样的处理器。 非阻塞方式对于JWT或基本身份验证之类的处理器很有用,因为其中身份验证提供者需要加载密钥或配置文件。

这是一个使用JWT的例子

routerBuilder
  .securityHandler("oauth")
  .bind(config ->
    // 当读取公钥的时候,我们不想阻塞
    // 我们可以使用非阻塞绑定
    vertx.fileSystem()
      .readFile("public.key")
      //  我们把future映射为身份验证提供程序
      .map(key ->
        JWTAuth.create(vertx, new JWTAuthOptions()
          .addPubSecKey(new PubSecKeyOptions()
            .setAlgorithm("RS256")
            .setBuffer(key))))
      // and map again to create the final handler
      .map(JWTAuthHandler::create))

  .onSuccess(self ->
    self
      .operation("listPetsSingleSecurity")
      .handler(routingContext -> {
        routingContext
          .response()
          .setStatusCode(200)
          .setStatusMessage("Cats and Dogs")
          .end();
      }));

AuthenticationHandler 映射到 OpenAPI 安全约束

您已经知道了您如何将 AuthenticationHandler 映射为一个定义在约定中的安全约束 前面的示例会验证配置,如果找不到配置就会导致您的路由构建失败

在某些情况下,约定是不完整的,您需要显式地定义安全处理器。 在这种情况下API略有不同,不会强制验证任何约束。 但是,无论如何,安全处理器对构建器都是可用的。

例如,给出一个名为 security_scheme_name 接口约束:

routerBuilder.securityHandler(
  "security_scheme_name",
  authenticationHandler);

您可以挂载包含在Vert.x Web中模块中的 AuthenticationHandler ,例如:

routerBuilder.securityHandler("jwt_auth",
  JWTAuthHandler.create(jwtAuthProvider));

当您生成 Router 之后,router builder会解析operation所必须的安全约束。 如果一个operation所必须的 AuthenticationHandler 缺失,则这个过程会失败。

调试/测试时,您可以用 setRequireSecurityHandlers 来禁用这个检验。

未实现的错误

如果未指定处理器,那么Router builder会为一个operation自动挂载一个默认的处理器。 这个默认的处理器会让 routing context 处于 405 Method Not Allowed 或者 501 Not Implemented 错误状态。 您可以用 setMountNotImplementedHandler 启用/禁用它,并且您可以用 errorHandler 自定义这个错误的处理方式。

响应内容类型处理器

当接口协议需要的时候,Router builder 自动挂载一个 ResponseContentTypeHandler 处理器。 您可以用 setMountResponseContentTypeHandler 禁用这个特性。

operation 模型

如果您在处理请求的时候需要获取到operation模型,那么您可以配置router builder,从而用 setOperationModelKey 将其放入 RoutingContext

options.setOperationModelKey("operationModel");
routerBuilder.setOptions(options);

// 添加一个用这个operation模型的处理器
routerBuilder
  .operation("listPets")
  .handler(
    routingContext -> {
      JsonObject operation = routingContext.get("operationModel");

      routingContext
        .response()
        .setStatusCode(200)
        .setStatusMessage("OK")
        // 以"listPets"为 operation id 回写响应
        .end(operation.getString("operationId"));
    });

请求体处理器

Router builder自动挂载一个 BodyHandler 用以管理请求体。 您可以用 bodyHandler 来配置 BodyHandler 对象(例如,更换上传目录)

multipart/form-data 校验

校验处理器像如下描述来区分文件上传和表单属性:

  • 如果参数中没有编码相关的字段:

    • 如果参数存在 type: stringformat: base64 ,或者存在 format: binary ,那么它就是 content-type请求头为 application/octet-stream 的一个请求。

    • 否则是一个表单请求

  • 如果参数存在编码相关字段,则是一个文件上传的请求。

对于表单属性,他们被解析、转换为Json、然后校验, 然而对于文件上传请求,校验处理器仅仅检查存在性和Content-Type。

自定义全局处理器

如果您需要挂载一个处理器,而这个处理器在您路由器中每个operation执行之前都需要执行特定操作,那么您可以用 rootHandler

Router builder 处理器的挂载顺序

router builder以如下顺序加载处理器:

  1. 请求体处理器

  2. 自定义全局处理器

  3. 已配置的 AuthenticationHandler

  4. 生成的 ValidationHandler

  5. 用户处理器 或者 "未实现的"处理器(如果启用)

生成路由器

万事俱备,生成路由器并使用:

Router router = routerBuilder.createRouter();

HttpServer server =
  vertx.createHttpServer(new HttpServerOptions().setPort(8080).setHost(
    "localhost"));
server.requestHandler(router).listen();

这个方法可能会失败并抛出 RouterBuilderException

提示

如果您需要挂载所有router builder生成的具有相同父级路径的路由器,您可以用 mountSubRouter

Router global = Router.router(vertx);

Router generated = routerBuilder.createRouter();
global.mountSubRouter("/v1", generated);