OpenAPI Specification 系列笔记 III

发布于:2025-09-09 ⋅ 阅读:(18) ⋅ 点赞:(0)

OpenAPI Specification 系列笔记 III

postman 的学习差不多也到了下一个阶段了,感觉目前对 swagger 的学习也到了一种自己比较满足的情况,所以差不多改一改这个笔记就可以放出来了

上两篇笔记分别是:

https://github.com/GoldenaArcher/open-api-3.x-study

security

security 这块就是验证相关的内容了,同样比较常见的做法,是在 components → securitySchemes 中定义对应的验证,随后在 security 中调用

security 可以在 root 下和 path 下使用,分别对应全局验证以及 API 验证,下面是登陆验证和 Bearer Token 的实现:

security:
  - BearerAuth: []
  - BasicAuth: []

components:
  securitySchemes:
    BasicAuth:
      type: http
      scheme: basic
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT

因为 security 是在 root 下使用的,所以 swagger 默认会要求全局验证:

点击 🔒 的时候可以看到验证方法:

⚠️:我尝试了一下,只是定义 security ,不是所有的 mock server 都会自动添加验证,比如说下面使用 prism mock 后,我尝试过没有添加 bearer token 直接访问,server 也会正常返回数据。这种情况,如果要强制验证其实没啥特别好的方法,只能检查 header 中是否包含对应的 token,比如说 bearer 有 Bearer XXXX 这个格式。具体需要用户登录的验证方法,我还真没想到有什么特别好的解决方法……

API Keys

这个功能需要对接第三方的会比较熟,就是使用别人提供的 API 时,需要传输的 API key 和/或 secret,这里的 API key 也可以进行验证:

security:
  - BearerAuth: []
  - BasicAuth: []
  - ApiKeyAuth: []

components:
  securitySchemes:
    BasicAuth:
      type: http
      scheme: basic
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-KEY

实现效果如下:

OAuth 2.0

这是另一种验证方法,大概配置如下:

security:
  - BearerAuth: []
  - BasicAuth: []
  - ApiKeyAuth: []
  - OAuth2AuthCode: [read, write, admin]

components:
  securitySchemes:
    BasicAuth:
      type: http
      scheme: basic
    BearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-API-KEY
    OAuth2AuthCode:
      type: oauth2
      flows:
        authorizationCode:
          authorizationUrl: "https://example.com/oauth/authorize"
          tokenUrl: "https://example.com/oauth/token"
          scopes:
            read: Grants read access
            write: Grants write access
            admin: Grants access to admin operations

path 层使用 security

这里就是在单独的 API 级别使用 security ,这里的实现会重载 root level 的实现

下面的案例中,会在 getCategories 这里使用 security: [],这样就会放弃所有的验证,同时也能看到,🔒 在 getCategories 这里消失了:

使用 github pages host 网页

在线预览地址在:https://goldenaarcher.github.io/open-api-3.x-study/

我放弃使用私有域名啦,继续用 GH 默认的了,然后发现之前的域名被人注册了 OTL

下面包含了 index.html 的实现方式,本质上来说就是通过 CDN,导入 swagger 的 JS 和 CSS,然后在对应的锚点完成渲染即可:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.27.1/swagger-ui.css"
    />
    <title>EazyShop API Documentation</title>
  </head>
  <body>
    <div id="eazyshop-oas"></div>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/swagger-ui/5.27.1/swagger-ui-bundle.js"></script>
    <script>
      const ui = SwaggerUIBundle({
        url: "eazyshop.yaml",
        dom_id: "#eazyshop-oas",
        deepLinking: true,
      });
    </script>
  </body>
</html>

最终效果如下:

具体的 endpoint:

mock api

这里就使用 prism 去 mock,具体的使用方式还是很简单的,下面是 package.json

{
  "name": "swagger",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "mock": "prism mock ./eazyshop.yaml -p 8080"
  },
  "repository": {
    "type": "git",
    "url": "git+https://github.com/GoldenaArcher/open-api-3.x-study.git"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "bugs": {
    "url": "https://github.com/GoldenaArcher/open-api-3.x-study/issues"
  },
  "homepage": "https://github.com/GoldenaArcher/open-api-3.x-study#readme",
  "description": "",
  "dependencies": {
    "@stoplight/prism-cli": "^5.14.2"
  }
}

运行 npm run mock 后:

对应的 endpoint 效果如下:

  • 网页
  • curl

如果实现了多个 examples,可以通过 prefer 去分别 query 不同的 example:

⚠️:这个案例是另一个项目的,学 postman 的时候稍微用了一下

生成服务端模板代码(server stub)

照了下,主流的选项有两个:Swagger Codegen 和 OpenAPI Generator

前者是 SmartBear——收购 swagger 的公司做的维护,后者是社区开源维护。我现在用的版本是 OAS3.1,而 Swagger Codegen 对 OAS3.1 的支持比较差,所以案例中选用了后者

调用的方式也有几种,我这里选择使用 node cli 搞定:

npm i @openapitools/openapi-generator-cli

added 146 packages, and audited 331 packages in 8s

53 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

生成对应的 stub 也就是通过 -g 控制

  • 生成 Java stub
    ❯ npx openapi-generator-cli generate \
      -i ./eazyshop.yaml \
      -g spring \
      -o ./out/spring
    [main] WARN  o.o.codegen.DefaultCodegen - OpenAPI 3.1 support is still in beta. To report an issue related to 3.1 spec, please kindly open an issue in the Github repo: https://github.com/openAPITools/openapi-generator.
    [main] INFO  o.o.codegen.DefaultGenerator - Generating with dryRun=false
    [main] INFO  o.o.c.ignore.CodegenIgnoreProcessor - Output directory (/Users/luhan/study/swagger/./out/spring) does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated.
    [main] INFO  o.o.codegen.DefaultGenerator - OpenAPI Generator: spring (server)
    [main] INFO  o.o.codegen.DefaultGenerator - Generator 'spring' is considered stable.
    [main] INFO  o.o.codegen.languages.SpringCodegen - ----------------------------------
    [main] INFO  o.o.c.languages.AbstractJavaCodegen - Environment variable JAVA_POST_PROCESS_FILE not defined so the Java code may not be properly formatted. To define it, try 'export JAVA_POST_PROCESS_FILE="/usr/local/bin/clang-format -i"' (Linux/Mac)
    [main] INFO  o.o.c.languages.AbstractJavaCodegen - NOTE: To enable file post-processing, 'enablePostProcessFile' must be set to `true` (--enable-post-process-file for CLI).
    [main] INFO  o.o.codegen.InlineModelResolver - Inline schema created as getOrders_200_response_inner. To have complete control of the model name, set the `title` field or use the modelNameMapping option (e.g. --model-name-mappings getOrders_200_response_inner=NewModel,ModelA=NewModelA in CLI) or inlineSchemaNameMapping option (--inline-schema-name-mappings getOrders_200_response_inner=NewModel,ModelA=NewModelA in CLI).
    
  • 生成 node stub
    ❯ npx openapi-generator-cli generate \
      -i ./eazyshop.yaml \
      -g nodejs-express-server \
      -o ./out/node
    [main] WARN  o.o.codegen.DefaultCodegen - OpenAPI 3.1 support is still in beta. To report an issue related to 3.1 spec, please kindly open an issue in the Github repo: https://github.com/openAPITools/openapi-generator.
    [main] INFO  o.o.codegen.DefaultGenerator - Generating with dryRun=false
    [main] INFO  o.o.c.ignore.CodegenIgnoreProcessor - Output directory (/Users/luhan/study/swagger/./out/node) does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated.
    [main] INFO  o.o.codegen.DefaultGenerator - OpenAPI Generator: nodejs-express-server (server)
    [main] INFO  o.o.codegen.DefaultGenerator - Generator 'nodejs-express-server' is considered beta.
    [main] INFO  o.o.c.l.NodeJSExpressServerCodegen - Environment variable JS_POST_PROCESS_FILE not defined so the JS code may not be properly formatted. To define it, try 'export JS_POST_PROCESS_FILE="/usr/local/bin/js-beautify -r -f"' (Linux/Mac)
    [main] INFO  o.o.c.l.NodeJSExpressServerCodegen - NOTE: To enable file post-processing, 'enablePostProcessFile' must be set to `true` (--enable-post-process-file for CLI).
    [main] INFO  o.o.codegen.InlineModelResolver - Inline schema created as getOrders_200_response_inner. To have complete control of the model name, set the `title` field or use the modelNameMapping option (e.g. --model-name-mappings getOrders_200_response_inner=NewModel,ModelA=NewModelA in CLI) or inlineSchemaNameMapping option (--inline-schema-name-mappings getOrders_200_response_inner=NewModel,ModelA=NewModelA in CLI).
    [main] INFO  o.o.codegen.InlineModelResolver - Inline schema created as _orders_put_request. To have complete control of the model name, set the `title` field or use the modelNameMapping option (e.g. --model-name-mappings _orders_put_request=NewModel,ModelA=NewModelA in CLI) or inlineSchemaNameMapping option (--inline-schema-name-mappings _orders_put_request=NewModel,ModelA=NewModelA in CLI).
    [main] INFO  o.o.codegen.InlineModelResolver - Inline schema created as _orders_post_request. To have complete control of the model name, set the `title` field or use the modelNameMapping option (e.g. --model-name-mappings _orders_post_request=NewModel,ModelA=NewModelA in CLI) or inlineSchemaNameMapping option (--inline-schema-name-mappings _orders_post_request=NewModel,ModelA=NewModelA in CLI).
    [main] INFO  o.o.codegen.InlineModelResolver - Inline schema created as _orders_post_201_response. To have complete control of the model name, set the `title` field or use the modelNameMapping option (e.g. --model-name-mappings _orders_post_201_response=NewModel,ModelA=NewModelA in CLI) or inlineSchemaNameMapping option (--inline-schema-name-mappings _orders_post_201_response=NewModel,ModelA=NewModelA in CLI).
    

生成客户端代码(Client SDK)

使用方法和生成后段代码一样,都是通过 -g 控制,不过可以添加 --additional-properties 去添加不同的集成,具体用什么框架,还是得查看官方文档

下面的案例是使用 TS+axios:

❯ npx openapi-generator-cli generate \
  -i ./eazyshop.yaml \
  -g typescript-axios \
  -o ./out/ts-axios \
  --additional-properties=supportsES6=true,useSingleRequestParameter=true,withSeparateModelsAndApi=true,apiPackage=apis,modelPackage=models,npmName=@acme/api-axios

[main] WARN  o.o.codegen.DefaultCodegen - OpenAPI 3.1 support is still in beta. To report an issue related to 3.1 spec, please kindly open an issue in the Github repo: https://github.com/openAPITools/openapi-generator.
[main] INFO  o.o.codegen.DefaultGenerator - Generating with dryRun=false
[main] INFO  o.o.c.ignore.CodegenIgnoreProcessor - Output directory (/Users/luhan/study/swagger/./out/ts-axios) does not exist, or is inaccessible. No file (.openapi-generator-ignore) will be evaluated.
[main] INFO  o.o.codegen.DefaultGenerator - OpenAPI Generator: typescript-axios (client)
[main] INFO  o.o.codegen.DefaultGenerator - Generator 'typescript-axios' is considered stable.
[main] INFO  o.o.c.l.AbstractTypeScriptClientCodegen - Hint: Environment variable 'TS_POST_PROCESS_FILE' (optional) not defined. E.g. to format the source code, please try 'export TS_POST_PROCESS_FILE="/usr/local/bin/prettier --write"' (Linux/Mac)
[main] INFO  o.o.c.l.AbstractTypeScriptClientCodegen - Note: To enable file post-processing, 'enablePostProcessFile' must be set to `true` (--enable-post-process-file for CLI).
[main] INFO  o.o.codegen.InlineModelResolver - Inline schema created as getOrders_200_response_inner. To have complete control of the model name, set the `title` field or use the modelNameMapping option (e.g. --model-name-mappings getOrders_200_response_inner=NewModel,ModelA=NewModelA in CLI) or inlineSchemaNameMapping option (--inline-schema-name-mappings getOrders_200_response_inner=NewModel,ModelA=NewModelA in CLI).

⚠️:这里生成的是 client SDK,不是 web client

这个生成的部分完全可以只作为 API 调用的接口

如果要实现 auto-refresh 的功能,可以在这之上进行一个包装——如封装 axios interceptor,这样可以将这部分的代码作为 API 调用的 single source of truth,也不用担心 merge conflict 的问题

额外补充

3.1 中 null 的使用

在第二篇笔记的时候我简单的提了一下 null 作为数据类型的存在,并且说明了 nullable 在 OAS3.1 中已经废弃了

这里简单的提一下 null 的具体用法

Example:
	name:
		type: [string, 'null']

3.1 中 unevaluatedProperties

这个是对 allOf, anyOf 这种组合子进行的兜底验证,我现在也用过一两次,大概的感觉就是,用 unevaluatedProperties 会拦截没有被子组合验证过的属性,比如:

Base:
  type: object
  properties: { name: { type: string } }

Addr:
  type: object
  properties: { address: { type: string } }

Person:
  type: object
  allOf:
    - $ref: "#/Base"
    - $ref: "#/Addr"
  unevaluatedProperties: false

{ "name": "A", "address": "X", "state": "ANY" } 这种例子,在没有 unevaluatedProperties 的情况下是一个合法的对象,但是使用了 unevaluatedProperties 就不合法,因为它包含了一个 schema 中不存在的属性

⚠️:在具体的实现过程中,我发现用 additional-properties 和 optional fields 会有一些冲突,特别是在这种 nested 的情况下,所以用 unevaluatedProperties 会更好一些

3.1 中 patternProperties

如其名,如果满足对应的 pattern 即可,经常与 unevaluatedPropertiesadditional-properties 搭配使用

3.1 中 inheritance & polymorphism

  • const 的使用
    完全相等的一个判断,规定某个值必须是指定的参数
    下面 if / then / else 里会有例子
    这个搭配新的一些语法,用起来能够组不少非常活的 combo

  • 支持 if / then / else 的语法
    语法如下:

    components:
      schemas:
        Pet:
          type: object
          properties:
            kind:
              type: string
              enum: [dog, cat]
            bark:
              type: boolean
            meow:
              type: boolean
          required: [kind]
          allOf:
            - if:
                properties:
                  kind: { const: dog }
              then:
                required: [bark]
              else:
                required: [meow]
          additionalProperties: false
    
  • unevaluatedProperties 可以对额外的性进行验证

  • dependentSchemas 的存在
    这是一个满足了 A 就必须要有 B 的存在,如下面这个例子:

    type: object
    properties:
      paymentMethod: { enum: [card, paypal] }
      cardNumber: { type: string }
      cardCvv: { type: string }
      paypalId: { type: string }
    
    dependentSchemas:
      paymentMethod:
        allOf:
          - if: { properties: { paymentMethod: { const: card } } }
            then: { required: [cardNumber, cardCvv] }
          - if: { properties: { paymentMethod: { const: paypal } } }
            then: { required: [paypalId] }
    

    这个的用法就是,如果当前的 schema 中出现了 paymentMethod,那么就要满足下面这个 allOf 的需求,如果没出现,则略过不做检查
    allOf 则是用了一个 if/then 定义了,如果是信用卡支付,则必须要卡号、cvv,如果是 paypal,则需要 paypalId,所以同时满足的是 {paymentMethod: card, cardNumber: '', cardCvv: ''}{paymentMethod: paypal, paypalId: ‘'} 两种格式
    上面的这个约束其实可以有其他的实现方法,但是用 dependentSchemas 的实现会更加的直观一些

  • dependentRequired ,和上面的 schema 类似,如果没有对 schema 的要求,用这个属性即可

3.0 discriminator

这是之前学习没有学到的部分——大概率是因为之前学的内容比较简单,没有太多 mapping 的需求吧

我大概学了一下,理解就是,如果要做 mapping 的话,在 3.0 就只能用 **discriminator,**以上面的支付方法为例, discriminator 的用法为:

components:
  schemas:
    Pet:
      type: object
      required: [petType]
      properties:
        petType:
          type: string
          enum: [dog, cat]
      oneOf:
        - $ref: "#/components/schemas/Dog"
        - $ref: "#/components/schemas/Cat"
      discriminator:
        propertyName: petType
        mapping:
          dog: "#/components/schemas/Dog"
          cat: "#/components/schemas/Cat"

    Dog:
      type: object
      required: [petType, bark]
      properties:
        petType: { const: dog }
        bark: { type: boolean }

    Cat:
      type: object
      required: [petType, meow]
      properties:
        petType: { const: cat }
        meow: { type: boolean }

当然,这里还是加了一些 3.1 的新用法……

同样,如果有些对象比较大,需要分文件处理的话,这时候也可以用 discriminator 去处理,如:

Pet:
  oneOf:
    - $ref: "./Dog.yaml"
    - $ref: "./Cat.yaml"
  discriminator:
    propertyName: petType
    mapping:
      dog: "./Dog.yaml"
      cat: "./Cat.yaml"

这样所有的定义都不需要被限制在单一的文件中

x-* 的使用

在上一篇的时候,只是简单地提了一下 **custom info,**但是实际折腾一下之后发现,这里能够折腾的东西远比单纯储藏一个 custom info 要多

下面的用法就是将两个对象存在 x-snippets 中,作为可以重复引用的参数:

需要注意的是,这种使用 snippet 的方法只能将引用值作为纯 data input 使用,而且我目前没有找到能够让 custom snippet 跨文件传输的方法,所以如果不同的文件需要用同一个引用,还是尽可能的放到 schema 或者 example 里去复用比较好

external ref

在文件 A 中引用文件 B,我这里使用了一个学习 postman 的例子,因为 endpoints 还是比较多的,所以我每个 endpoints 都分了对应的文件去实现,最后在 root 中 reference,这样可以导出一个完整的 specs 文件,具体用法如下:

需要注意的是,endpoint 中的 / 会被转译成 ~1 防止和文件路径产生冲突

在其他的 schema 中引用的方法如下:

基本上可以看到都是依赖于 $ref + 路径 + identifier 去找到对应的 reference


网站公告

今日签到

点亮在社区的每一天
去签到