よしたろうブログ

設計・人文知・歴史・哲学・漫画とかの話が好きです。

SpringBootとスキーマ駆動開発で始めるWeb API 設計開発入門:後編

image.png

初めに

前編ではスキーマ駆動開発において必要な前提知識や周辺ツールの記述を行いました。
以下の様なものたちです(抜粋)。

  • スキーマとは
  • ❐ OpenAPI Specification
  • ❐ OAI:OpenAPI Initiative
  • ❐ OpenAPI (Swagger) Editor
  • ❐ OpenAPI Generator (Gradle Plugin)
  • ❐ ドキュメントの自動生成
  • スキーマから Spring コードの自動生成を行う
  • ❐ Gradle タスクの依存関係設定
  • API記述言語
  • スキーマ駆動開発とは(詳細)
  • スキーマ活用シーン
  • API 設計 『設計はなぜ重要か?』
  • ❐ 提供機能を決める
  • ❐ リソースを特定する
  • ❐ エンドポイント(URI)の定義
  • ❐ HTTPメソッドを決める
  • ❐ 『安全性』と『冪等性』
  • ❐ リクエストを設計する
  • ❐ レスポンスを設計する

また、この記事は一応3部作です。特に第1部で紹介する ROA と REST の関係はめっちゃ重要ですし、REST API の実装のための前提知識をまとめています。読んでいただけると幸いです。

1部:HTTPとRESTの基本 『網羅版:HTTPメソッドとレスポンスコード』

2部 SpringBootとスキーマ駆動開発で始めるWeb API 設計開発入門:前編

3部 SpringBootとスキーマ駆動開発で始めるWeb API 設計開発入門:後編

この記事で紹介しないこと

  • SpringBoot 自体の細かい話

この記事で紹介すること

  • WebAPI / RESTAPI / ROA などの普遍的な思想
  • 上記に基づく設計・開発
  • スキーマの定義方法
  • スキーマ定義がどう反映されるのか
  • エラーハンドリング
  • セキュリテイ(紹介)

本編では具体的な開発を行う上で必要な部分などを中心に紹介していきます。
それでは宜しくお願いします。

9. Schema の構造と書き方

ここでは前編の『APIに必要なリソースを見つける』で紹介した各リソースの詳細を書いています。

  • リソースの名前
    • ユニークかつ自明であればあるほどよい
  • リソースの詳細文(オプション:必要に応じて)
    • 名前や型からはわからないオプションの追加情報
  • 他のリソースとの関連
  • リソースの状態
  • 各種日時
  • 必須かどうか

こういった情報をスキーマに記述していきますが、スキーマを定義するためにはまずスキーマの構造を理解しましょう。

9-1. Open API で定義されたスキーマの構造

Open APIでは、スキーマの各要素を「フィールド」という単位で役割を定め、それらは階層構造を成しています。

スキーマ全体を OpenAPI オブジェクトと呼びます。OpenAPI オブジェクトの openapi フィールドの値は3.0.0、info フィールドの値として info オブジェクトが存在しています。

OpenAPI オブジェクトはファイルシステムでいうところのルート(/)のようなもので、 OpenAPI オブジェクトの各フィールドにさまざまなオブジェクト が属することでスキーマ全体が構成されています。 openapi フィールドは、そのスキーマが採用している Open API のバージョンを定義しています。 また OpenAPIでは大文字小文字が区別されます。

openapi: "3.0.0"
info:
  title: TODO API Document
  version: "0.0.1"
  description: TODO API のドキュメントです
tags:
  - name: opts
    description: 運用監視関連API
  - name: tasks
    description: タスク関連API
paths:
  /health:
    get:
      tags:
        - opts
      responses:
        '200':
          description: OK
  /tasks/1:
    get:
      tags:
        - tasks
      # メソッド名の設定
      operationId: showTask
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                # 参照先を指定
                $ref: "#/components/schemas/TaskDTO"
# component & $ref で再利用可能な記述をする
components:
  schemas:
    TaskDTO:
      type: object # {}で始まるjson オブジェクトを示す
      properties:
        id:
          type: integer
          format: int64 # long 型を表現
        title:
          type: string
      required:
        - id
        - title

各フィールドに属するオブジェクトを簡単に解説します。

❐ Infoオブジェクト

Info オブジェクトは、 APIメタデータを記述します。

info:
  title: TODO API Document
  version: "0.0.1"
  description: TODO API のドキュメントです

title フィールドはAPIの名称 version フィール ドにはAPIのバージョンを定義します。 ここで定義するバージョンは、前述した openapi フィールド(採用 しているOpenAPIのバージョン)とは異なることに注意してください。

❐ Server オブジェクト

Server オブジェクトは、 APIをホストするサーバへの接続情報を記述します。 servers フィールドに、配列で複数の Server オブジェクトを記述します。 上記のサンプルには登場していなので以下に別個でサンプルを示します。

servers :
- url: http://localhost:8080
  description: Development server 
- url: https://staging.example.com
  description: Staging server
- url: https://production.example.com 
  description: Production server

配列として渡せることから、上記のよう にステージングサーバや本番サーバなど複 数環境への接続情報を表現できます。

❐ Paths オブジェクト

Paths オブジェクトは、各エンドポイントの仕様を記述します。 パス(/tasks/1 など)をキーにしたPath Itemオブジェクトを持ち、1つ以上のPath Item オブジェクトで構成されます。 Path Item オブジェクトは複数 Operation オブジェクトを内包しています。

paths:
    ~~中略~~ 
    /tasks/1:
# ここから $ref までが Path Item オブジェクト
    get:
      tags:
        - tasks
      # メソッド名の設定
      operationId: showTask
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                # 参照先を指定
                $ref: "#/components/schemas/TaskDTO"

❐ Operation オブジェクト

OpenAPIでは、1つのパス (Path Item オブジェクト)に対する1つのHTTPメソッド による操作を「オペレーション」 と呼び、 Operation オブジェクトで定義します。

    /tasks/1:
    get:
# ここから $ref までが Operation オブジェクト
      tags:
        - tasks
      # メソッド名の設定
      operationId: showTask
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                # 参照先を指定
                $ref: "#/components/schemas/TaskDTO"
# component & $ref で再利用可能な記述をする
components:
  schemas:
    TaskDTO:
      type: object # {}で始まるjson オブジェクトを示す
      properties:
        id:
          type: integer
          format: int64 # long 型を表現
        title:
          type: string
      required:
        - id
        - title

❐ Reference オブジェクト

Reference オブジェクトは $ref というフィールド名を持つオブジェクトで、別のオブジェクトへの参照を表現しています。上記は、 Components オブジェクト配下に 「TaskDTO」という名前で定義した Response Body オブジェクトを参照しています。

Reference オブジェクトを使用して再利用することで、定義を1ヵ所にまとめて重複を避けることができます。なお、 Referenceオブジェクトの$ref という名前のフィールドをキーにするスタイルは OpenAPI 独自のものではなく、JSON Reference から採り入れられた構文です。 OpenAPIには、このほかにも JSON Schema を参考にした構文があります。

❐ Components オブジェクト

Components オブジェクトは、スキーマ内のさまざまな場所で再利用するオブジェクトを定義しておく要素です。 サンプルではPaths オブジェクト配下で扱うリクエストボディやレスポンスなどは Components オブジェクトに定義し、それらをReference オブジェクトで参照する形をとっています。 Components オブジェクトを使用せずにリクエストボディなどの仕様をPathsオブジェクトにインラインで記述することも可能です が、スキーマ全体の見通しの良さや再利用性の観点から、 Components オブジェクトを使用することが推奨されます。

❐ Schema オブジェクト

Schema オブジェクトは、リクエストやレスポンスで扱うデータの型や構造を定義するオブジェクトです。 type フィールドは、データの型を定義します。type フィールドをformat フィールドと組み合わせることで、さまざまなデータ型の定義を行うことができます。

          type: integer
          format: int64 # long 型を表現

引用元:OpenAPI 4.4 Data Types §

Common Name type format Comments
integer integer int32 signed 32 bits
long integer int64 signed 64 bits
float number float
double number double
string string
byte string byte base64 encoded characters
binary string binary any sequence of octets
boolean boolean
date string date As defined by full-date - [!RFC3339]
dateTime string date-time As defined by date-time - [!RFC3339]
password string password A hint to UIs to obscure input.

❐ Request Body オブジェクト、 Response オブジェクト

Request Body オブジェクトと Response オブジェクトは、名称のとおりAPIサーバへ送信するリクエストボディと返ってくるレスポンスを定義するオブジェクトです。これらのオブジェクトは、 content フィールドに MIMEタイプ (application/json) をキーにしてペイロードの構造を定義しています。 このペイロードの部分 は Media Type オブジェクトとして規定されています。 サンプルでは application/json のみですが、 content フィールドは複数の Media Type オブジェクトを持つことができます。

paths:
    ~~中略~~ 
    /tasks/1:
    get:
      tags:
        - tasks
      operationId: showTask
      responses:
# ここから $ref までが Response オブジェクト
        '200':
          description: OK
          content:
            application/json:
# ここから $ref までが Media Type オブジェクト
              schema:
                $ref: "#/components/schemas/TaskDTO"

9-2. 公式ドキュメントからスキーマを記述していく方法

公式ドキュメントをみて自力で書いていくのには結構きついものがあります。以下に補助となるサイトの見方を紹介します。

スキーマオブジェクトの視覚化

OpenAPI Object MAP

一つ一つのオブジェクトに階層構造があり、スキーマはその階層構造に応じて必要なリソースを記述していく形になります。階層構造を視覚化する便利なサイトがあります。赤字は必須なオブジェクトを示します。ちなみにバージョンごとで互換性がない物も多いので注意してください。

https-::openapi-map.apihandyman.io:.gif

❐ 各オブジェクトを公式ドキュメントで深ぼっていく

具体的になにを記述すべきかはOpenAPI Specification v3.0.0 をみて一個一個確認していきます。ちょっとサイトに癖があって使いづらく感じるかもです。

OpenAPI Specification v3.0.0.gif

  • openapi: 必須。 Open APIのバージョンを指定するオブジェクトです。
  • info: 必須。 APIメタデータを定義します。
  • tags: APIを分類するタグを定義する。
  • paths: 必須。 APIとして利用可能なパスおよび操作を定義する。
  • components: Open APIの中で利用する様々なオブジェクトをコンポーネント化して再利用可能にする。

これらはファーストレベルオブジェクトと呼ばれ、これらのオブジェクトに対応したオブジェクトやフィールトが、配下に設置されていきます。 上記で紹介しているスキーマに登場するオブジェクトの定義がどの様に反映されるのか見ていきましょう。

❐ operationId オブジェクト

これは自動生成されるインターフェイス内のメソッド名が変更されます。
default ResponseEntity<Void> showTask() の部分です。 あとはこれを Controller として実装します。

もし、operationId:でメソッド名を指定しない場合はpaths: 配下の /task/1 が使用され、task1Api()みたいなメソッド名になります。

/**
 * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech) (6.2.1).
 * https://openapi-generator.tech
 * Do not edit the class manually.
 */
package com.example.todoapi.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.Parameters;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.multipart.MultipartFile;

import javax.validation.Valid;
import javax.validation.constraints.*;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.annotation.Generated;

@Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "2022-11-08T11:04:50.798504+09:00[Asia/Tokyo]")
@Validated
@Tag(name = "tasks", description = "タスク関連API")
public interface TasksApi {

    default Optional<NativeWebRequest> getRequest() {
        return Optional.empty();
    }

    /**
     * GET /tasks/1
     *
     * @return OK (status code 200)
     */
    @Operation(
        operationId = "showTask",
        tags = { "tasks" },
        responses = {
            @ApiResponse(responseCode = "200", description = "OK")
        }
    )
    @RequestMapping(
        method = RequestMethod.GET,
        value = "/tasks/1"
    )
    default ResponseEntity<Void> showTask(
        
    ) {
        return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);

    }

}

❐ info: tag: components: content:

自動生成された api ドキュメントに反映されています。

スクリーンショット 2022-11-08 15.32.33.png

tags: によって api がグルーピングされています。
- 運用監視関連API(/health) - タスク関連API(/task/1)

componets: components オブジェクト Schemas TaskDTO というオブジェクトが key で value としてレスポンスボディを定義しています。レスポンスボディは JSON Schema に従って記述され、 JSON 形式のリソースが定義されます。

  • データ構造 JSON
  • 記述方法 JSON Schema  
components:
  schemas:
    TaskDTO:
      type: object # {}で始まるjson オブジェクトを示す
      properties:
        id:
          type: integer
          format: int64 # long 型を表現
        title:
          type: string
      required:
        - id
        - title

また、GET /tasks/1 というエンドポイントには TaskDTO で定義されたレスポンスボディが紐づいています。

スクリーンショット 2022-11-08 15.38.02.png

これは path: オブジェクトの中に指定された下記の内容によって参照先に component を使用することが定義されているからです。
$ref: "#/components/schemas/TaskDTO" の部分ですね。content ヘッダーはリソースの表現(形式)として JSON を使用する。リソースは schema(定義)はコンポーネントを参照する、という記述ですね。 $ref:はリファレンスオブジェクトと呼ばれます。その名の通りですね。

paths:
    〜〜中略〜〜
  /tasks/1:
    get:
      tags:
        - tasks
      # メソッド名の設定
      operationId: showTask
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                # 参照先を指定
                $ref: "#/components/schemas/TaskDTO"
components:
  schemas:
    TaskDTO:
      type: object # {}で始まるjson オブジェクトを示す
      properties:
        id:
          type: integer
          format: int64 # long 型を表現
        title:
          type: string
      required:
        - id
        - title

9-3. レスポンスボディのスキーマを定義 (JSON Schema)

レスポンスボディのスキーマを定義しますが、データ構造として JSON を使用します。
apiスキーマファイルないに JSON のスキーマを定義します。この際、 JSON Schema という記述方法に則って記述する必要があります。 おさらいですが、スキーマとは「構造を定義して、ある定められた形式で記述したもの」という意味です。

component オブジェクト
Open APIの中で利用する様々なオブジェクトをコンポーネント化して再利用可能にします。

このオブジェクトの配下に

  • schemas

というオブジェクトを定義します。

4.7.25 Schema Object § The Schema Object allows the definition of input and output data types. These types can be objects, but also primitives and arrays. This object is an extended subset of the JSON Schema Specification Wright Draft 00.

4.7.25 スキーマオブジェクト§ スキーマ・オブジェクトは、入力および出力データ型を定義することができます。これらの型は、オブジェクトだけでなく、プリミティブや配列も可能である。このオブジェクトは、JSON スキーマ仕様ライトドラフト 00 の拡張サブセットである。

ということで簡単に言えばこのオブジェクト配下にレスポンスボディを記述してってことですね。以下に例を記述します。

左が JSON、右が JSON Schema

スクリーンショット 2022-11-08 12.54.08.png

9-4. JSON Schema を使うメリット

  • 既存のデータ型を記述できる
  • 明確で、人にも機械にも読める文書
  • 完全に構造化されたデータの検証ができる

JSON Schema は人に対してわかりやすく、機械にも読める文書です。 また、JSON は構造化されたデータであるため、階層が深くなったとしてもデータの検証を行いやすい再利用性を持っています。制約を表現し、さらにそれをプログラム中から用いることのできる JSON Schema は、 リソースを定義するのにてきしています。

9-5. JSONXML の違い

文字コード

JSON は RFC4627 で UTF-8 を使用することと取り決められています。 XMLはその点取り決めがない代わりに、データフォーマット内で文字コードを指定することができます。現在 Unicode で JIS第3水準・JIS第4水準すべてサポートされています。そのため、UTF-8を使っていて困る場面はあまりないと思います。

❐ 表現力

JSONvalue に様々な変数型を使用することができます。そのため、数値は数値として、bool は bool として受け取ることができます。一方 XML は全て文字列扱いとなります。その代わり、データを要素と属性の二種類で表現することができます。

❐ 処理速度

この点においては JSON に軍配が上がります。どちらも開始タグ・終了タグで内容を囲むフォーマットですが、タグの長さに違いがあります。 XML のタグは最低でも3bite(閉じタグは最低4bite)あります。運用面を考えれば要素名を1bite文字1文字に統一することはしいでしょう。ということで、実際にはもっと増えることになります。その点、JSONのタグは「[]{}:”」のどれか1文字1biteと、非常にシンプルです。その為、構文解析の処理がXMLよりも速くなります。

❐ 使い分け

JSON はファイルサイズが小さく、 XML と比較してデータをWebに効率的に送信できます。JSON はコードの構造がより整理されているため、読みやすくなっています。 一方、XMLは構造が複雑なため、解釈が困難です。 JSONは処理を必要としないためデータの転送に適していますが、XMLはデータの送信だけでなく、ファイルの処理とフォーマットも可能にするため複雑になる可能性があります。

XML形式は可読性が悪い、タグが多く手続きが面倒、文字が多い分送受信コストが高いなどのデメリットがあります。これに対して JSONは、単純さゆえに表現力が高く、可読性・変更のしやすさがが高く、軽いです。小さくて単純なデータセットを送信するのに向いています。Web API での使用には XML より JSON が向いています。


10. 指定の id を取得できる様に スキーマを拡張する

上記では paths:オブジェクト配下で指定していた path は固定の値でした(/tasks/1)。動的な形にします。それに伴って動的なパラメータにバインドするための設定をスキーマに加えています。get:オブジェクト配下のparameters:オブジェクトです。

paths:
  ~~中略~~
  /tasks/{taskId}:
    get:
      tags:
        - tasks
      # メソッド名の設定
      operationId: showTask
      parameters:
        - name: taskId
          in: path
          required: true
          description: "詳細を取得するタスクのID"
          schema:
            type: integer
            format: int64
      responses:
        '200':
          description: OK
          content:
            application/json:
              schema:
                # 参照先を指定
                $ref: "#/components/schemas/TaskDTO"
# component & $ref で再利用可能な記述をする
components:
  schemas:
    TaskDTO:
      type: object # {}で始まるjson オブジェクトを示す
      properties:
        id:
          type: integer
          format: int64 # long 型を表現
          description: "タスクID"
        title:
          type: string
          description: "タスクのタイトル"
      required:
        - id
        - title

10-1. サンプルコードでの parameters オブジェクトの中身

~~中略~~
      parameters:
        - name: taskId
          in: path
          required: true
          description: "詳細を取得するタスクのID"
          schema:
            type: integer
            format: int64
~~中略~~

❐ parameters オブジェクト

Parameter オブジェクトは、APIサーバに送信するパラメータを定義し、一意のリソースを特定するためのオブジェクトです。 name フィールドはパラメータ名を定義します。 in フィールドはパラメータの場所で、 query header path cookieが指定可能です。 Parameter オブジェクトはname フィールドとin フィールドの組み合わせで一意になりますので、たとえば taskId という名前の Parameter オブジェクトをin: pathin: queryで2つ定義できます。 今回使用するフィールドを以下に抜粋。

引用元:OpenAPI Specification v3.0.0 / 4.7.12 Parameter Object §

フィールド名 タイプ 説明
name string 必須パラメータの名前。パラメータ名は大文字と小文字が区別されます。
  • in"path"である場合nameフィールドはPaths オブジェクトのパスフィールドからの関連付けられたパス セグメントに対応している必要があります。詳細については、パス テンプレートを参照してください。
  • in"header"で、nameフィールドが"Accept""Content-Type"または"Authorization"の場合、パラメータ定義は無視されます。
  • それ以外の場合はすべてnameinプロパティで使用されるパラメーター名に対応します。
in string 必須パラメータの場所。可能な値は、「query」、「header」、「path」、または「cookie」です。
description string パラメータの簡単な説明。これには使用例が含まれる場合があります。 CommonMark 構文 は、リッチ テキスト表現に使用できます ( MAY )。
required boolean このパラメータが必須かどうかを決定します。パラメータの場所「パス」の場合、このプロパティは必須であり、その値はtrueである必要がありますそれ以外の場合、プロパティを含めることができ、そのデフォルト値はfalseです。
schema Schema Object |
Reference Object
パラメータに使用される型を定義するスキーマ

簡単に解説します。

❐ in フィールド / name フィールド

in フィールド

パラメータの場所。可能な値は、「query」、「header」、「path」、または「cookie」です。

name フィールド

in"path"である場合、nameフィールドはPathsオブジェクトのパスフィールドからの関連付けられたパスセグメントに対応している必要があります。

in フィールド で指定したのは URI のどこに記述するのか?そして何を記述するのかは name フィールドが決定します。

ここは/tasks/{taskId}の中括弧の中身のtaskId- name:オブジェクトで指定する文字列を一致させる様に書けとい うことです。

❐ schema フィールド

ここでパスパラメータに使用される変数がどの様なものなのかを定義します。つまりtaskIdのことです。OpenAPI Specification(略称OAS)は、プリミティブデータ型をサポートしています。データ型を詳細に定義するために、フォーマットで指定します。下記の指定で、 long型 がデータ型として定義されました。他の指定方法は上記で紹介した図をご覧ください。

  • type: integer
  • format: int64

10-2. どう変わったのか?

見てみましょう。

『指定の id を取得できる様にする』前

スクリーンショット 2022-11-08 15.38.02.png

『指定の id を取得できる様にする』後 変更箇所

  1. エンドポイント
  2. Paramerters タブ内
    1. taskId というパラメータ
    2. (path) パラメータという種類が指定
    3. *requiredという必須属性を示す表記
    4. int64 というデータ型表記(long型)
    5. description にパラメータの説明

スクリーンショット 2022-11-08 19.46.01.png

11. HTTP API エラーハンドリング

IETF(Internet Engineering Task Force)が発表した RFC 7807 は、HTTPレスポンスの中で問題の詳細をモデル化する方法として JSON オブジェクトを使用する方法を概説しています。より具体的な機械可読メッセージをエラーレスポンスで提供することで、APIクライアントがより効果的にエラーに対処できるようになるというものです。 RFC 7807 は 「Problem Details for HTTP APIs」 と呼ばれ、エラーレスポンスの規格になる設計集です。

一般に、エラー・レスポンスの目標は、ユーザーに問題を知らせるだけでなく、その問題の解決策を知らせるための情報源を作ることです。単に問題を述べるだけでは、それを解決することはできません。同じことがAPIの障害にも当てはまります。

11-1. RFC 7807 の概要

HTTP APIの問題詳細

概要

この文書では、HTTP レスポンスに含まれるエラーの詳細を機械的に読み取るための方法として "problem detail" を定義している。 HTTP API のための新しいエラーレスポンスフォーマットを定義する必要性を回避するために、HTTP レスポンスのエラーの詳細を機械的に読み取る方法としてHTTP APIのための新しいエラーレスポンスフォーマットを定義する必要性を回避するために、HTTPレスポンスで機械的に読めるエラーの詳細を伝える方法として "problem detail "を定義する。

RFC 7807 より

❐ For example, an HTTP response carrying JSON problem details:(抜粋)

   HTTP/1.1 403 Forbidden
   Content-Type: application/problem+json
   Content-Language: en

   {
    "type": "https://example.com/probs/out-of-credit",
    "title": "You do not have enough credit.",
    "detail": "Your current balance is 30, but that costs 50.",
    "instance": "/account/12345/msgs/abc",
    "balance": 30,
    "accounts": ["/account/12345",
                 "/account/67890"]
   }

ここでは、(type: URI で識別される)クレジット失墜問題は "title" で 403 の理由を示し、 "instance" で特定の問題発生のための参照を与え、 "detail" で発生固有の詳細を与え、2つの拡張子を追加しています: "balance" は口座の残高を伝え、"accounts" は口座へのリンクを与えます。口座の残高を伝える "balance "と、口座の残高を補充するためのリンクを与える "accounts"です。accounts は、accounts に追加できるリンクを提供する。

"invalid-params"
下記で使用されている"invalid-params"はバリデーションエラーの際に使用される。   以下のサンプルでは

  • 年齢が「正の整数である必要がある」
  • 色は「「緑」、「赤」、または「青」でなければならない」

というエラー文が設定されています。ここでは各フィールドに対する、ヴァリデーションエラーの理由を説明していますね。

 HTTP/1.1 400 Bad Request
   Content-Type: application/problem+json
   Content-Language: en

   {
   "type": "https://example.net/validation-error",
   "title": "Your request parameters didn't validate.",
   "invalid-params": [ {
                         "name": "age",
                         "reason": "must be a positive integer"
                       },
                       {
                         "name": "color",
                         "reason": "must be 'green', 'red' or 'blue'"}
                     ]
   }

11-2. エラー処理の概要

エラー応答には、一般的な HTTP ステータスコード、開発者へのメッセージ、エンドユーザーへのメッセージ (該当する場合)、内部エラーコード (内部で決定された特定の ID に対応する)、開発者が詳細情報を見つけることができるリンクを含める必要があります。

適切なエラーコードが真に役立つためには、3つの基本的な基準を満たしている必要があります。品質エラーコードには次のものが含まれます。

  1. 問題の原因と領域を簡単に確認できるHTTP ステータスコード
  2. ドキュメント固有のエラー表記用の内部参照 ID
  3. 目前のエラーのコンテキスト、原因、および一般的な解決策を要約した人間が判読できるメッセージ。

11-3.悪いエラー処理と良いエラー処理

参考:Best Practices for API Error Handling

❐ 良くないエラーの例

以下のサンプルは、エラーコードは適切ですが、優れたものではありません。特定のタイプの障害が何であるかを確認できるため、ユーザーが問題解決プロセスをどこから開始できるかを示しています。さらに、非常に重要なことに、内部参照できる「BR0x0071」の形式で内部参照 ID も提供しています。ですが、これは機械にわかりやすく人間にはわかりづらいものです。

必要なのは『機械にも人間にもわかりやすい』ということです。JSON Schema のメリットでもあげましたが『明確で、人にも機械にも読める文書』である必要があるのです。

コンテキストは内部エラードキュメントへの機械読み取り可能な参照コードという形になっています。ユーザーはドキュメントを見つけ、リクエストコード "BRx0071 "を調べ、何が問題だったかを理解しなければなりません。 コードは簡潔で、文脈を提供する限りは有用ですが、人間にとってそれが有用であるかどうかは別の問題です。

HTTP/1.1 400 Bad Request
Date: Wed, 09 Nov 2022 05:08:11 GMT
Server: tsa_m
Connection: close
Transfer-Encoding: chunked
Content-Type: application/json{ 
    "error" :
         "REQUEST - BR0x0071"
}

何よりもまず、エラーコードは文脈(コンテキスト)を示すものでなければなりません。上記の 400 Bad Request 単体では何の意味もありません。エラーコードにさらにコンテキストを与える必要があります。これを行う方法のひとつは、レスポンスのボディで、リクエストに共通する言語でこの情報を渡すことです。

例えば、400 Bad Requestというエラーコードは、クライアントに有用な情報を与えるレスポンスボディを持つことができます。

HTTP/1.1 400 Bad Request
Date: Wed, 09 Nov 2022 05:08:11 GMT
Server: tsa_m
Connection: close
Transfer-Encoding: chunked
Content-Type: application/json
{ 
    "error" : 
        "Bad Request 
            - Your request is missing parameters. 
              Please verify and resubmit. 
              Issue Reference Number BR0x0071" 
}

これでもまだ不十分です。良い点は、コンテキストを提供していることです。具体的にどのような障害が発生したのかがわかるので、ユーザーは問題解決のプロセスを開始することができます。さらに、重要なことですが、「BR0x0071」という形で内部参照IDも表示され、内部で参照することができます。

レスポンスステータスしか返さない様なエラーレスポンスよりは遥かに良いエラーコードですが、これだけではまだ求められていることのほんの一部しか実現できていません。

  • 「リクエストにパラメータがありません。 確認して再送信してください。 発行参照番号 BR0x0071"」

これだけでは、なんのパラメメータが不足しているか分かりませんね。デバッグ作業など、二次的な作業が要求されます。

❐ 良いエラーの例

Twitter
Twitter APIは、記述的なエラー報告コードの素晴らしい例です。メンションタイムラインを取得するためにGETリクエストを送信してみます。

  • https://api.twitter.com/1.1/statuses/mentions_timeline.json

これをTwitter APIに送信すると、次のようなレスポンスが返ってきます。

HTTP/2 400
date: Wed, 09 Nov 2022 05:17:30 GMT
perf: 7626143928
server: tsa_m
set-cookie: guest_id_marketing=xxxxxxxxxx; Max-Age=63072000; Expires=Fri, 08 Nov 2024 05:17:30 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
set-cookie: guest_id_ads=xxxxxxxxxx; Max-Age=63072000; Expires=Fri, 08 Nov 2024 05:17:30 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
set-cookie: personalization_id="xxxxxxxxxx; Max-Age=63072000; Expires=Fri, 08 Nov 2024 05:17:30 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
set-cookie: guest_id=xxxxxxxxxx; Max-Age=63072000; Expires=Fri, 08 Nov 2024 05:17:30 GMT; Path=/; Domain=.twitter.com; Secure; SameSite=None
content-type: application/json; charset=utf-8
cache-control: no-cache, no-store, max-age=0
content-length: 62
x-transaction-id: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
strict-transport-security: max-age=631138519
x-response-time: 105
x-connection-hash: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx 
{
    "errors": [
        {
            "code": 215,
            "message": "Bad Authentication data."
        }
    ]
}

このデータを見ると、何が問題なのかが大体わかります。まず、400 Bad Request を送信したことがわかり、問題がリクエストのどこにあるかを示しています。コンテンツの長さは許容範囲内であり、レスポンスタイムも通常の範囲内。また、Twitterが独自に定義したエラーコード「215」と、「Bad Authentication data」と書かれたメッセージが表示されていることがわかります。

このエラーコードは、エラーが発生した理由とその修正方法に関する貴重な情報を提供していて、認証データを渡さなかったことに原因があることを示します。エラー215が参照され、認証データを渡すことが修正方法であることがわかると同時に、Twitter APIの内部文書で参照するための番号も提供されています。

12. APIセキュリティチェックリスト

APIを設計、テスト、リリースするときの最も重要なセキュリティ対策のチェックリスト 正直、今の自分には理解できない事ばっかりでした。ここの解説で一記事書けるようになりたいところです。

引用元:https://github.com/shieldfy/API-Security-Checklist/blob/master/README-ja.md

❐ 認証

  • [ ] Basic認証を利用せず、標準的な認証を利用する(例: JWT、OAuth)。
  • [ ] 認証、トークンの生成、パスワードの保管において「車輪の再発明」をしないこと。
  • [ ] すでに標準化されているものを利用する。
  • [ ] ログインにおいては最大リトライ回数(Max Retry)とjail機能を利用する。
  • [ ] 全ての機微情報において暗号化を活用する。

❐ JWT (JSON Web Token)

  • [ ] ランダムで複雑なキー(JWT Secret)を使用する。これはブルートフォース攻撃を困難にするため。
  • [ ] ペイロードからアルゴリズムを抽出しないこと。アルゴリズムは必ずバックエンド処理のみとする(HS256またはRS256)。
  • [ ] トークンの有効期限(TTL, RTTL)を可能な限り短くする。
  • [ ] JWTのペイロードに機密情報を格納してはいけない。それは簡単に復号できる。
  • [ ] あまり多くのデータを保存するに避けるください。JWTは通常header「ヘッダー」に共有され、サイズ制限があります。

❐ OAuth

  • [ ] サーバサイドで常にredirect_uriを検証し、ホワイトリストに含まれるURLのみを許可する。
  • [ ] 常にtokenではなくcodeを交換するようにする(response_type=tokenを許可しない)。
  • [ ] stateパラメータをランダムなハッシュと共に利用し、OAuth認証プロセスでのCSRFを防ぐ。
  • [ ] デフォルトのscopeを定義し、アプリケーション毎にscopeパラメータを検証する。

❐ アクセス

  • [ ] DDoSやブルートフォース攻撃を回避するため、リクエストを制限(スロットリング)する。
  • [ ] MITM(Man in the Middle Attack)を防ぐため、サーバサイドではHTTPSを使用する。
  • [ ] SSL Strip attackを防ぐため、SSL化とともにHSTSヘッダを設定する。
  • [ ] ディレクトリ・リストをオフにしてください。
  • [ ] プライベートAPIの場合、ホワイト・リストに登録されたIP/ホストからのアクセスのみを許可します。

❐ 入力

  • [ ] 操作に応じて適切なHTTPメソッドを利用する。GET(読み込み), POST(作成), PUT/PATCH(置き換え/更新), DELETE(単一レコードの削除)。リクエストメソッドがリソースに対して適切ではない場合、405 Method Not Allowedを返す。
  • [ ] リクエストのAcceptヘッダ(コンテンツネゴシエーション)のcontent-typeを検証する。サポートしているフォーマット(例: application/xml, application/json等)は許可し、そうでない場合は406 Not Acceptableを返す。
  • [ ] POSTされたデータのcontent-typeが受け入れ可能(例: application/x-www-form-urlencoded, multipart/form-data, application/json等)かどうかを検証する。
  • [ ] ユーザーの入力に一般的な脆弱性が含まれていないことを検証する(例: XSS, SQLインジェクション, リモートコード実行等)。
  • [ ] URLの中に機密情報(認証情報, パスワード, セキュリティトークン)を利用せず、標準的な認証ヘッダを使用する。
  • [ ] サーバー側の暗号化のみを使用してください。
  • [ ] キャッシュ、Rate Limit policies(例: Quota, Spike Arrest, Concurrent Rate Limit)を有効化し、APIリソースのデプロイを動的に行うため、APIゲートウェイサービスを利用する。

❐ 処理

  • [ ] 壊れた認証プロセスを回避するため、全てのエンドポイントが認証により守られていることを確かめる。
  • [ ] ユーザーに紐付いたリソースIDを使用してはならない。/user/654321/ordersの代わりに/me/ordersを利用する。
  • [ ] オートインクリメントなIDを利用せず、代わりにUUIDを利用する。
  • [ ] XMLファイルをパースする場合、XXE(XML external entity attack)を回避するため、entity parsingが有効でないことを確認する。
  • [ ] XMLファイルをパースする場合、exponential entity expansion attackによるBillion Laughs/XML bomb攻撃を回避するためentity expansion が有効でないことを確認する。
  • [ ] ファイルアップロードにはCDNを利用する。
  • [ ] 大量のデータを扱う場合、バックグラウンドでWorkerプロセスやキューを出来る限り使用し、レスポンスを速く返すことでHTTPブロッキングを避ける。
  • [ ] デバッグ・モードを無効にすることを忘れないでください。
  • [ ] 可能な場合は、実行不可能なスタックを使用してください。

❐ 出力

  • [ ] X-Content-Type-Options: nosniffをヘッダに付与する。
  • [ ] X-Frame-Options: denyをヘッダに付与する。
  • [ ] Content-Security-Policy: default-src 'none'をヘッダに付与する。
  • [ ] フィンガープリントヘッダを削除する - X-Powered-By, Server, X-AspNet-Version等。
  • [ ] content-typeを必ず付与する。もしapplication/jsonを返す場合、content-typeはapplication/jsonにする。
  • [ ] 認証情報, パスワード, セキュリティトークンといった機密情報を返さない。
  • [ ] 処理の終了時に適切なステータスコードを返す(例: 200 OK, 400 Bad Request, 401 Unauthorized, 405 Method Not Allowed等)。

❐ CI & CD (継続的インテグレーションと継続的デリバリー)

  • [ ] ユニットテスト/結合テストカバレッジで、設計と実装を継続的に検査する。
  • [ ] コードレビューのプロセスを採用し、自身による承認を無視する。
  • [ ] プロダクションへプッシュする前に、ベンダのライブラリ、その他の依存関係を含め、サービスの全ての要素をアンチウイルスソフトで静的スキャンする。
  • [ ] コードに対してセキュリティ・テスト(静的/動的分析)を継続的に実行して。
  • [ ] 既知の脆弱性について、依存関係(ソフトウェアとOSの両方)を確認して。
  • [ ] デプロイのロールバックを用意する。

❐ 参照:

yosriady/api-development-tools - RESTful HTTP+JSON APIを構築するための有用なリソースの集まり。

終わりに

網羅的にカバーするには遠く及びませんでしたが、API についてよくわかってない状態から始めた自分としては満足です(すみません)。
ただセキュリティに関しては全く掘り下げる事ができなかったのが心残りです。この事でいつか一記事書きたいところです。

また、上記で紹介しましたが、この記事は3部作です。他の記事も読んでいただけると幸いです。

1部:HTTPとRESTの基本 『網羅版:HTTPメソッドとレスポンスコード』

2部 SpringBootとスキーマ駆動開発で始めるWeb API 設計開発入門:前編

3部 SpringBootとスキーマ駆動開発で始めるWeb API 設計開発入門:後編