Web API 設計の要点

『Web API: The Good Parts』(水野貴明, O’Reilly Japan) を読んで理解した内容を、 自分の言葉と自分の文脈(API Gateway での設定)に落として書いたもの。本文の引用はしていない。 HTTP ヘッダやステータスコードの仕様そのものは RFC / W3C に基づく事実。

ApiGatewayでAPIを設定するに当たっての要点をまとめてみます。 例は本の内容を参考に独自のものにしています。

[https://www.amazon.co.jp/Web-API-Parts-%E6%B0%B4%E9%87%8E-%E8%B2%B4%E6%98%8E/dp/4873116864]

URL

  • ドメインはapi.から始める
https://api.example.com/XXXXXXXX/XXXXXXXXXXXX/
  • すべて小文字にする
https://developer.github.com/v3/git/commits/
https://api.twitter.com/1.1/statuses/retweets/:id.json
↓
AWS::ApiGateway::ResourceのPathPartは全て小文字にする
  • 複数形の名詞を使用する
users
activities
※get等の動詞は含めない
↓
DynamoDBのテーブル名、AWS::ApiGateway::ResourceのLogicalIDを複数形の名詞
  • 複数単語はパスを分ける
サービスURL修正後URL
Twitter/statuses/user_timeline/statuses/user/timeline/
Youtube/guideCategoriesguide/categories
Facebook/me/books.quotesme/books/quotes/
LinkedIn/v1/people-searchvi/people/search
Bit.ly/v3/user/popular_earned_by_clicksv3/user/popular/clicks
Disqus/api/3.0/applications/listUsage.json/api/3.0/applications/list/usage.json
  • 取得数と取得位置
limitとoffsetにする
※SQLでもlimitとoffsetが使用されている
※取得数はcount,per_page,maxResultsを使っているのもあるが少数
※取得位置はstart,page,cursorを使っているのもあるが少数
  • 全文検索
q or query
※googleはqを使用している
  • パス or クエリ
一意なリソースを表すのに必要な情報の場合はパス
※id等

省略可能でデフォルトが存在するものはクエリ
※offsetやlimit
  • ドメインの次のパス
v1 or 1.0

api.twitter.com/1.1
api.foursquare.com/v2
www.googleapis.com/youtube/v3
api.linkedin.com/v1
developer.github.com/v3

※以下は通常のAPIとはURLが異なるようです
app.rakuten.co.jp/services/api
webservice.recruit.co.jp/hotpepper
webservice.recruit.co.jp/r25
jws.jalan.net/APIAdvance
api.mixi-platform.com/2
  • メソッド
X-HTTP-Method-Overrideヘッダーを使用する
メソッド名操作
GETリソースの取得
POSTリソースの新規登録
PUT既存リソースの更新
DELETEリソースの削除
PATCHリソースの一部変更
HEADリソースのメタ情報の取得
  • 個々のデータ取得
id or id.json
  • 一覧
XXX/list or XXX/list.json or XXX/XXXies
  • 更新
XXX/updates or XXX/id

レスポンス

  • 取得するフィールドを選べるようにする
https://api.exmample.com/v1/users/12345?fields=name,age
or
https://api.exmample.com/v1/users/12345?ResponseGroup=Medium

[http://docs.aws.amazon.com/AWSECommerceService/latest/DG/CHAP_ResponseGroupsList.html:title]

  • エンベロープ

HTTP 自体が既に封筒の役割を果たしているので、その中でもう一度包み直す意味はない。

NG

{
  "header": {
    "status": "success",
    "errorCode": 0,
  },
  "response": {
    ... 実際のデータ ...
  }
}
  • jsonのトップレベル
配列にしない
※JSONインジェクションを防ぐ
  • 件数や次の結果の存在の有無
{
  "timelines":[
    :
  ],
  hasNext: true
}
{
  "kind": "plus#activityFeed",
  "title": "Plus Public Activities Feed",
  "nextPageToken": "CKaEL",
  "items": [
    {
      "kind": "plus#activity",
      "id": "123456789",
      ...
    },
  ]
}

なるべく少ない単語数で表現する

NG:userRegistrationDateTime
※/users というユーザー情報を取得する API なら、最初の user はな くても問題ない

複数の単語を連結する場合、その連結方法は API 全体を通して統一する

キャメル
※jsonなので

変な省略形は極力利用しない

NG: timezone→tz
NG: location→lctn
  • 性別
gender: male|female
※maleを1等にはしない
  • 数値 誤差が出ないように文字列として扱う
462781738297483264がと462781738297483260される。
これはJavaScriptが大きな整数を扱うと誤差が出てしまうから

"id": 266031293949698048,
"id_str": "266031293949698048"
  • エラー

200番台はリクエストが成功したときにだけ返す。 パラメータ不正や権限不足でエラーなのに、ボディにエラー情報を入れて 200 を返す実装をたまに見るが、 これは誤りで、実際に問題を起こす。

  • エラーの詳細をクライアントに返す
    twitter
{
  "errors":[
    {
      "message":"Bad Authentication data",
      "code":215
    }
  ]
}

GitHub

{
  "message": "Not Found",
  "documentation_url": "https://developer.github.com/v3"
}
200を返して、レスポンスボディで独自ステータスコードを返す
  • 202(Accepted)の実例
承認を必要とする場合は202
ファイルのダウンロードをする際はRetry-After ヘッダとともに202
  • PUTやPATCH
200と共に生成したデータを返す
※賛否両論あり
  • DELETE
204(No Content)を返す
※賛否両論あり
  • Expires or Cache-Control
更新日時が決まっているものはExpires
他はCache-Controlを使用する
  • Dateヘッダを必ず返す(500番台以外)
Date: Wed, 20 Aug 2014 11:10:39 GMT
※必ずHTTP時間の形式でGMTで(日本でも)
  • Last-ModifiedとETagを使って、クライアントはキャッシュがfreshか確認
Last-Modified: Tue, 01 Jul 2014 00:00:00 GMT
ETag: "ff39b31e285573ee373af0d492aca581"
  • クライアントにキャッシュしてほしくない場合
Cache-Control: no-cache
※Expiresは使わない
  • クライアントにキャッシュしてほしい場合
Cache-Control: public, max-age=3600
  • 機密情報の場合でプロキシサーバー等にキャッシュしてほしくない場合
Cache-Control: no-store
  • 同じURLでも、リクエストヘッダによって結果が異なるものはVaryヘッダーを指定する
Vary: Accept-Encoding,User-Agent,Accept-Language

Vary: Accept
※Acceptヘッダーに任せる
  • 新鮮ではないキャッシュを持っているキャッシュサーバー
stale-while-revalidateによる非同期のキャッシュの更新を使う
  • Content-Typeは正しく設定する
Content-Type: application/json; charset=utf-8
※iOSのネットワーククライアント等では、Content-Typeが不正な場合はエラーになる
※jsonをtext/htmlにするとXSSが可能になる
  • 独自ヘッダーの例

独自ヘッダを定義するときは、X- の接頭辞に続けてサービス名・アプリ名・組織名を置くのが慣例。 ただし RFC 6648 でこの X- 慣習自体は非推奨とされたので、新規に決めるなら接頭辞なしも検討する。

X-GitHub-Media-Type: github.v3
  • CORS
リクエストヘッダーのOriginを検証して、同じものを以下のようにして返す
Access-Control-Allow-Origin: http://www.example.com
  • ブラウザで自動的に行われるプリフライトリクエスト
OPTIONS /v1/users/12345 HTTP/1.1
Host: api.example.com
Accept: application/json
Origin: http://www.example.com
Access-Control-Request-Method: GET
Access-Control-Request-Headers: X-RequestId


HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Access-Control-Allow-Origin: http://www.example.com
Access-Control-Allow-Methods: GET, OPTIONS
Access-Control-Allow-Headers: X-RequestId
Access-Control-Max-Age: 864000
Content-Length: 0
Content-Type: text/plain
  • CORSでのユーザー認証情報
Access-Control-Allow-Credentials: true
※認証情報を認識しておりますということ
※なかった場合、ブラウザは受け取ったレスポンスを拒否してしまいます

セキュリティ

  • HTTPSなら安全か
man-in-the-middle attack  
クライアントが証明書の警告を無視する場合は可能

[https://www.youtube.com/watch?v=lLC4OxplXuE:embed:cite]

  • Content-Type ユーザーからの入力を受け入れてJSONに埋め込んで返すようなAPIでContent-Typeがtext/htmlだとXSSが可能になる。 ※X-Content-Type-Options: nosniffもしておかないとブラウザによってはXSSが可能になる。
{"data":"<script>alert('xss');</script>"}
  • エスケープ
/(スラッシュ)
<: \u003C
>:  \u003E 
\: \u005C
\ や "、'などに関しても、\u005Cや\u0022、\u0027のように16進数のエスケープ
U+2028 と U+2029
+ を \u002B
  • XSRF

あらかじめ XSRF トークンを渡しておき、それがパラメータとして送られてこなければ拒否する。 あわせて X-Requested-With の有無を見て、無ければ許可しない。

  • JSONハイジャック用コード
<script type="application/javascript">
     var data;
     Array = function() {
       data = this;
     };
</script>

<script type="application/javascript">
       Object.prototype.__defineSetter__('id', function(obj){alert(obj);});
</script>

<script>
    window.onerror = function(e) {}
</script>
<script src="https://api.example.com/v1/users/me" language="vbscript"></script>
  • JSONハイジャックの対策
XMLHttpRequest or ブラウザ以外からのアクセスのみ
→X-Requested-Withがない場合はアクセスを許可しない

レスポンスヘッダー
→Content-Type: application/json
→X-Content-Type-Options: nosniff

トップレベルに配列を使用しない

JSONの先頭にfor (;;);
→Javascriptとして実行されても次にいけなくしている
※facebook
  • リクエストの再送信
同じアクセスが何度も行われたらエラーにする

1回の購入につき1回のポイントの付与だけを行うようにチェック
→購入トークンの発行とサーバ側での保持、検証
  • ヘッダー

この本が挙げていたセキュリティヘッダの一覧は、API 設計に統合した。 書籍の時点の推奨のうち X-XSS-ProtectionPublic-Key-Pins現在では非推奨になっているので、そちらに現行の一次情報(OWASP Secure Headers)で 更新したものを置いてある。

下の実例は、当時実際に観測されたレスポンスとして残す。 X-XSS-ProtectionStrict-Transport-Security が並んでいるのはそのときの状態であって、 いま真似する対象ではない。

  • ヘッダーの例
HTTP/1.1 200 OK
Date: Mon, 16 Jun 2014 21:39:06 GMT
Server: nginx
Content-Type: application/json; charset=utf-8
Access-Control-Allow-Origin: *
Tracer-Time: 43
X-RateLimit-Limit: 500
X-RateLimit-Remaining: 499
Strict-Transport-Security: max-age=864000
X-ex: fastly_cdn
Content-Length: 11074
Accept-Ranges: bytes
Via: 1.1 varnish
X-Served-By: cache-ty66-TYO
X-Cache: MISS
X-Cache-Hits: 0
Vary: Accept-Encoding,User-Agent,Accept-Language
HTTP/1.1 200 OK
Server: GitHub.com
Date: Mon, 16 Jun 2014 21:32:36 GMT
Content-Type: application/json; charset=utf-8
Status: 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 55
X-RateLimit-Reset: 1402957018
Cache-Control: public, max-age=60, s-maxage=60
Last-Modified: Mon, 16 Jun 2014 04:55:23 GMT
ETag: "cbd0cecf6295eba60adc4c06c7836b8d"
Vary: Accept
X-GitHub-Media-Type: github.v3
X-XSS-Protection: 1; mode=block
X-Frame-Options: deny
Content-Security-Policy: default-src 'none'
Content-Length: 1201
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit- Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
Access-Control-Allow-Origin: *
X-GitHub-Request-Id: 719794F7:01FB:299F044:539F6273 Strict-Transport-Security: max-age=31536000
X-Content-Type-Options: nosniff
Vary: Accept-Encoding
X-Served-By: 971af40390ac4398fcdd45c8dab0fbe7
  • DOS対策
• リミット値の設定
エンドポイント毎: 15分 15回(ユーザー)/180回(アプリ)
APIの種類(core,search等)毎: 1時間5000回/60回
※アクセス制限の緩和も想定しておく
  • 上限オーバーしたとき
HTTP/1.1 429 Too Many Requests
Retry-After: 3600
  • レスポンスヘッダーにレートリミットを渡す
X-RateLimit-Limit
X-RateLimit-Remaining
X-RateLimit-Reset
  • レートリミットの実装
redisにする

ドキュメント

[https://github.com/subosito/iglo:title]

[https://developers.facebook.com/tools/explorer/]

[https://console.developers.google.com/:embed:cite]

関連