Subscribed unsubscribe Subscribe Subscribe

Web API: The Good Partsを読んでみて、ApiGatewayのベスト・プラクティスについて考えてみる

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

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 /guideCategories guide/categories
Facebook /me/books.quotes me/books/quotes/
LinkedIn /v1/people-search vi/people/search
Bit.ly /v3/user/popular_earned_by_clicks v3/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

Response Groups - Product Advertising API

  • エンベロープ

    HTTP プロトコルというエンベロープがあるので、2重に封筒に入れる意味はない

NG

{
  "header": {
    "status": "success",
    "errorCode": 0,
  },
  "response": {
    ... 実際のデータ ...
  }
}
配列にしない
※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が可能になる
  • 独自ヘッダーの例

    HTTP ヘッダを新しく定義する場合はこのように“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
※認証情報を認識しておりますということ
※なかった場合、ブラウザは受け取ったレスポンスを拒否してしまいます

セキュリティ

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

www.youtube.com

  • 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回のポイントの付与だけを行うようにチェック
→購入トークンの発行とサーバ側での保持、検証
  • ヘッダー
Content-Type: application/json
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: deny
Content-Security-Policy: default-src 'none'
Strict-Transport-Security: max-age=15768000
Public-Key-Pins: max-age=2592000;
          pin-sha256="E9CZ9INDbd+2eRQozYqqbQ2yXLVKB9+xcprMF+44U1g=";
          pin-sha256="LPJNul+wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ="
Set-Cookie: session=e827ea0c0fe8c109eb37a60848b5ed39; Path=/; Secure; HttpOnly
  • ヘッダーの例
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
• リミット値の設定
エンドポイント毎: 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にする

ドキュメント

GitHub - subosito/iglo: API blueprint's formatter

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

console.developers.google.com