Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Web API The Good Parts 読書メモ

Web API とは何か

「HTTP プロトコルを利用してネットワーク越しに呼び出す API」を本書では Web API と呼ぶ
言い換えると、ある URI にアクセスすることで、サーバ側の情報を書き換えたり、サーバ側に置かれた情報を取得したりできる Web システムで、プログラムからアクセスしてそのデータを機械的に利用するためのもの

さまざまな API のパターン

開発者が Web API を設計しなければならない機会は、例えば以下の通り

  • 公開している Web サービスのデータや機能の API 公開
  • 他のページに貼り付けるウィジェットの公開
  • モダンな Web アプリケーションの構築
  • スマートフォンアプリケーションの開発
  • ソーシャルゲームの開発
  • 社内システムの連携

Web API を美しく設計する重要性

なぜ、 API は美しく設計しなければならないのか?

  • 設計の美しい Web API は使いやすい
  • 設計の美しい Web API は変更しやすい
  • 設計の美しい Web API は頑強である
  • 設計の美しい Web API は恥ずかしくない

Web API を美しくするには

思想の根幹をなす重要な原則は以下の2つ

  • 仕様が決まっていないものに関しては仕様に従う
  • 仕様が存在していないものに関してはデファクトスタンダードに従う

標準仕様やスタンダードに従うことで、API を利用する他の開発者の手間やストレスを軽減できる
利用方法が容易に類推可能になったり、あるいは既存のクライアントライブラリの流用が可能になったりする

エンドポイントの設計とリクエストの形式

API として公開する機能を設計する

公開した API がどのように使われるのか、ユースケースをきちんと考えることがポイント

API エンドポイントの考え方

良い URI の設計原則は以下の通り

  • 覚えやすく、どんな情報をもつ URI なのかがひと目でわかる

以降、この「覚えやすく」「わかりやすい」について具体的に考えていく

短く入力しやすい URI

不要な情報が入っていたり、意味が重複していたりする URI は避けるべし
たとえば、

http://api.example.com/service/api/search

とかは api が重複しているし、類似した意味の service もあったりして冗長

http://api.example.com/search

だけで意味は通じるはず

人間が読んで理解できる URI

意味不明な略語や、一般的に使用される英語以外を URI に用いるのは避けるべし
一般的に API で使われる単語を知るには、実際に他の API や ProgrammableWeb を参照するのが良い
単語の複数形や過去形については間違いが混入しやすいので特に注意する

大文字小文字が混在していない URI

大文字小文字の混在は API をわかりづらく、間違えやすくする
大文字の URI で呼び出された場合には、単に 404 NotFound で返すのが良い(小文字の URI にリダイレクトする必要ナシ)

改造しやすい(Hackable な)URI

URI を修正して別の URI にするのが容易、ということ
ある URI から他の URI を想像することが可能であれば、あまりドキュメントを読まなくても開発を進めることができ、利用者の負担を軽減できる

サーバ側のアーキテクチャが反映されていない URI

例えば、以下のようなエンドポイントは NG

http://api.example.com/cgi-bin/get_user.php?user=100

PHP で書かれていて CGI として動作しているんだろうな、ということが想像できてしまう
利用者にとっては、PHP で書かれていようが COBOL で書かれていようがどうでもいい
また、アーキテクチャが反映されていると、攻撃者に対して脆弱性を突くためのヒントを与えてしまうことにもなる

ルールが統一された URI

例えば、以下のようなルールがバラバラな API は使いにくい

http://api.example.com/friends?id=100
http://api.example.com/friend/100/message

HTTP メソッドとエンドポイント

URI とメソッドは、「操作するもの」と「操作方法」の関係にある
1つの URI に異なるメソッドでアクセスできるようにすることで、リソースと、それをどう扱うかを分離して扱うことができる
Web API で利用するメソッドを以下に示す

  • GET・・・リソースの取得
  • POST・・・リソースの新規登録
  • PUT・・・既存リソースの更新
  • DELETE・・・リソースの削除
  • PATCH・・・リソースの一部変更

API のエンドポイント設計

エンドポイントを設計する中で注意すべき点は以下の通り

  • 複数形の名詞を利用する
  • 利用する単語に気をつける
  • スペースエンコードを必要とする文字を使わない
  • 単語をつなげる必要がある場合はハイフンを利用する

検索とクエリパラメータの設計

ページネーション

ページネーションの仕組みを実現する方法は、大きく分けて以下の2つ

  • per_page と page で取得数と取得位置を指定
  • limit と offset で取得数と取得位置を指定

相対位置でデータを取得する方法には、パフォーマンスの問題がある
(offset を使った場合はレコードを先頭から数えてしまう可能性があるため)

また、更新頻度の高いデータにおいてデータに不整合が生じるという問題もある
最初の20件を取得してから、次の20件を取得する間にデータの更新が入ってしまった場合、実際に取得したい情報と取得された情報にズレが生じてしまう

offset で相対位置を指定する代わりに、これまで取得した最後のデータの ID や時刻を記録しておいて、「この ID よりも前のもの」「この ID よりも後のもの」といった指定を行う方法もある(絶対位置による指定)

絞り込みのためのパラメータ

完全一致で検索する場合、以下のような URI とするのが直感的

http://api.example.com/v1/users?name=ken

検索するフィールドがほぼ一つに決まる場合は q というパラメータが使われる場合もある
こちらは部分一致での検索、というニュアンスが強くなる

http://api.example.com/v1/users?q=ken

クエリパラメータとパスの使い分け

クエリパラメータに入れる情報は、URI 中のパスの中に入れることも設計上は可能
クライアントが指定する特定のパラメータをクエリパラメータに入れるか、パスに入れるかを決める際の基準は以下の通り

  • 一意なリソースを表すのに必要な情報かどうか
  • 省略可能かどうか

ログインと OAuth 2.0

ログイン周りの API を考える際に、真っ先に検討すべきは OAuth 仕様
OAuth は基本的に広く第三者に公開される API において認可(authorization)を行うために用いられる

OAuth でのアクセスに成功すると、あなたのサービスは FaceBook 等からアクセストークンを受け取る
このトークンを利用することで、Facebook 等にユーザが保持している情報のうち、認可されたものだけにアクセスが可能となる

OAuth には 1.0 と 2.0 があり、2.0 は 2012年10月に RFC6749 として標準化されている
OAuth 1.0 を利用する理由は特にないため、2.0 を利用するのが良い

OAuth 2.0 にはリソースにアクセスするための認可を得る手順が4つ定められている(Grant Type)

  • Authentication Code・・・サーバサイドで多くの処理を行う Web アプリケーション向け
  • Implicit・・・スマートフォンアプリや JavaScript を用いた、クライアントサイドで多くの処理を行うアプリケーション向け
  • Resource Owner Password Credentials・・・サーバサイド(サイトB)を利用しないアプリケーション向け
  • Client Credentials・・・ユーザ単位での認可を行わないアプリケーション向け

Resource Owner Password Credentials の認証を行う場合は、以下のようなパラメータを指定する

  • grant_type・・・password という文字列。Resource Owner Password Credentials であることを表す
  • username・・・ログインするユーザ名
  • password・・・ログインするパスワード
  • scope・・・アクセスのスコープを指定する(省略可)

最後の scope は、どんな権限にアクセスをさせるかを指定するもの
スコープを使うことで、外部サービスがトークンを得る際にアクセス内容を制限し、またユーザに「このサービスは以下の情報にアクセスできますよ」と表示することができる

クライアントのリクエストは(たとえば)以下のようになる

POST /v1/oauth2/token HTTP/1.1
Host: api.example.com
Authorization: Basic Y2xpZW50X21kOmNsaWVudF9zZWNyZXQ
Content-Type: application/x-www-form-urlencoded

grant_type=password&username&takaaki&password&abcde&scope=api

正しい情報がサーバに送られると、サーバは以下のような JSON をレスポンスとして返す

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
  "access_toke": "b77yz37w7kzy8v5fuga6zz93",
  "token_type": "bearer",
  "expires_in": 2620743,
  "refresh_token": ""
}

bearer トークンの送信方法は RFC 6750 によれば以下の3種類ある

  • リクエストヘッダに入れる方法
  • リクエストボディに入れる方法
  • URI にクエリパラメータとして入れる方法

expires_in は、アクセストークンが後何秒で有効期限切れになるかを表したもの
有効期限が切れた場合、サーバは invalid_token というエラーを 401 で返すことになっている

invalid_token が発生した場合にはリフレッシュトークンを使ってアクセストークンを再度要求することができる
リフレッシュトークンは返さないことも可能(その場合は再ログインが必要となる)

ユーザ名やパスワードによらず API を利用させたい場合は Client Credentials を使うのが良い
ClientID と ClientSecret さえ取得してアプリケーションに埋め込んでおけば public な情報にアクセスできる

SSKDs と API デザイン

Web API は大きく以下の二種類に分けられる

  • LSUDs 向け・・・一般に公開し多くの人に使ってもらうための API
  • SSKDs 向け・・・自社のスマホアプリなど、特定の人だけが使う API

SSKDs 向けの API では、必ずしも汎用的な美しい API を提供する必要はない
ユーザ体験を考えると、ホーム画面に表示する情報を1つに詰め込んだ API を1個提供する方がいい場合もある
「1 screen 1 API call、1 save 1 API call」という言葉もある

レスポンスデータの設計

データフォーマット

JSON にデフォルトとして対応し、必要があれば XML などに対応する、という方針が良いと思われる

データフォーマットの指定方法としては、以下の3種類ある

  • クエリパラメータを使う方法・・・これがもっとも多い
  • 拡張子を使う方法
  • リクエストヘッダでメディアタイプを指定する方法・・・HTTP 仕様に厳格に合わせようと思ったらコレ

JSONP の取り扱い

対応する必要がないのであれば、無理に対応しない方がいい
同一生成元ポリシーによって守られている攻撃手法の対象となってしまう、というのが理由

データの内部構造の考え方

API で返すレスポンスデータを決定する際にまず考えることは、API のアクセス回数がなるべく減るようにすること
そのためには API のユースケースをきちんと考えることが重要になる

ひとつの作業を完了させるために複数回のアクセスが必要となる API は Chatty API と呼ばれる
Chatty API はネットワークのトラフィックを増加させ、クライアントの実装の手間も増やしてしまうためいいことなし

レスポンスの内容をユーザが選べるようにする

もっともシンプルな解決策は全ての API でできる限り多くのデータを返す、というもの
送受信されるデータサイズはできるだけ小さい方が望ましいため、取得する項目を利用者が選択可能にする、という手法が取られることがある
(クエリパラメータを使って、ユーザ情報のうち名前と年齢を取得したい、みたいなことを指定できるようにする、など)
Small、Medeium、Large などのレスポンスグループを指定して、必要なデータだけを取得させる、というやり方もある

エンベロープは必要か

レスポンスが、正常時もエラー時も 200 OK で返ってきて、本当に成功したかどうかはヘッダに載せる、というやり方
これは HTTP の仕様を無視しているし、冗長なので絶対にやるべきではない
HTTP 自体がエンベロープの役割を果たしており、エラーかどうかの判断はステータスコードに基づいて行うのが良い

データはフラットにすべきか

なるべくフラットな方が良いけど、階層構造を持った方がわかりやすいケースについてはそうするべき(by Google JSON Style Guide)

配列とフォーマット

配列をそのまま返すべきか?オブジェクトに包んで返すべきか?
オブジェクトで包んで方が以下のメリットがあり、おすすめとのこと

  • レスポンスデータが何を示しているのかがわかりやすくなる
  • レスポンスデータをオブジェクトに統一することができる
  • セキュリティ上のリスクを避けるこができる(トップレベルが配列だと JSON インジェクションの危険がある)

配列の件数、あるいは続きがあるかをどう返すべきか

本当に件数を返す必要があるかどうかはしっかり見極めた方がいい
全件数を取る必要がなかったとしても「次の20件」のようなリンクを表示するために、今取得したデータに続きはあるのか、は返した方が良い
例えば、続きがあれば "hasNext": "true" をつけて返す、など

各データのフォーマット

各データ項目の名前づけについてポイントを列挙

  • 多くの API で同じ意味に利用されている一般的な単語を用いる
  • なるべく少ない単語数で表現する
  • 複数の単語を連結する場合、その連結方法は API 全体を通して統一する
  • 変な省略形は極力利用しない
  • 単数形・複数形に気をつける

日付のフォーマットについては、広く一般に公開する場合 RFC 3339 を使うのが良い
このフォーマットが、読みやすく使いやすいものを目指してインターネット上で用いる標準形式として定められたものだから

エラーの表現

ステータスコードでエラーを表現する

エラーを返す際にまず真っ先にやっておかねばならないことは、適切なステータスコードを使うこと

  • 100 番台・・・情報
  • 200 番台・・・成功
  • 300 番台・・・リダイレクト
  • 400 番台・・・クライアントサイドに起因するエラー
  • 500 番台・・・サーバサイドに起因するエラー

エラーの詳細をクライアントに返す

エラーの内容を返す方法は大きく分けて二つある

  • HTTP のレスポンスヘッダに入れて返す
  • レスポンスボディで返す(こちらの方が主流)

例えば Twitter API のレスポンスボディはこんな感じ

{
  "errors":[
    {
      "message":"Bad Authentication data",
      "code":215
    }
  ]
}

HTTP の仕様を最大限利用する

ステータスコードを正しく使う

主に API で利用する可能性のあるステータスコードは以下の通り

ステータスコード 名前 説明
200 OK リクエストは成功した
201 Created リクエストが成功し、新しいリソースが作られた
202 Accepted リクエストは成功した
204 No Content コンテンツなし
300 Multiple Choices 複数のリソースが存在する
301 Moved Permanently リソースは恒久的に移動した
302 Found リクエストしたリソースは一時的に移動している
303 See Other 他を参照
304 Not Modified 前回から更新されていない
307 Temporary Redirect リクエストしたリソースは一時的に移動している
400 Bad Request リクエストが正しくない
401 Unauthorized 認証が必要
403 Forbidden アクセスが禁止されている
404 Not Found 指定したリソースが見つからない
405 Method Not Allowd 指定されたメソッドは使うことができない
406 Not Acceptable Accept 関連のヘッダに受理できない情報が含まれている
408 Request Timeout リクエストが時間以内に完了しなかった
409 Conflict リソースが矛盾した
410 Gone 指定したリソースは消滅した
413 Request Entity Too Large リクエストボディが大きすぎる
414 Request-URI Too Long リクエストされた URI が長すぎる
415 Unsupported Media Type サポートしていないメディアタイプが指定された
429 Too Many Requests リクエスト回数が多すぎる
500 Internal Server Error サーバ側でエラーが発生した
503 Service Unavailable サーバが一時的に停止している

200 番台:成功

201 は Created、つまりリクエストの結果サーバ側でデータ作成が行われた場合に返す
202 の Accepted は、リクエストした処理が非同期で行われ、処理は受け付けたけれど完了していない場合に利用する
204 は No Conent という言葉の通り、レスポンスが空のときに返す

PUT や PATCH の場合は 200 とともに操作したデータを返し(POST の場合は 201)、DELETE の場合は 204 を使うのが良い
こうしておけば、どちらの場合も返ってきたデータを見れば変更が正しく行われたことが理解できる

300 番台:追加で処理が必要

300 番台のステータスコードでよく知られている利用目的は「リダイレクト」
リダイレクトの場合は Location というレスポンスヘッダにリダイレクト先の新しい URI が含まれる

API の場合もリダイレクトを利用することはありえるが、Web サイトのように URI の変更、サイトの移転や一時的な移動に伴ってリダイレクトを行うことはあまり好ましくない(クライアントの実装によっては、動かなくなってしまうため)

クライアントのリクエストに問題があった場合

400 番台はクライアントのリクエストに起因するエラー

400 Bad Request はその他のエラーコード
送られてきたパラメータに間違いがある場合など、他のステータスコードに該当しない場合は 400 を使う

401 Unauthorized は認証のエラー(あなたが誰かわからない)
403 Forbidden は認可のエラー(あなたが誰かはわかったけど、この操作は許可されていない)

404 Not Found はアクセスしようとしたデータが存在しない場合に返す
ただし、何が存在しないかはケースバイケースなので、エラーメッセージをきちんと返す必要がある

405 Method Not Allowed はエンドポイントは存在するがメソッドが許可されていない
(GET の API に POST でアクセスしようとした場合など)
406 Not Acceptable はクライアントが指定してきたデータ形式に API が対応していない
(JSON と XML しか対応していないのに YAML を指定した場合など)

408 Request Timeout は、リクエストをクライアントがサーバに送るのに時間がかかりすぎて、サーバ側でタイムアウトを起こした場合

409 Conflict は、リソース競合が発生した際のエラー
(重複した ID のデータを登録しようとした場合など)

410 Gone は 404 と同じく、リソースが存在しない場合に返すコードだが、こちらは単に存在しないのではなく、かつて存在したけれど今はもう存在しない、ということを表す

413 Request Entity Too Large はリクエストボディが大きすぎる時のエラー
ファイルアップロードに、許容されるサイズ以上のデータが送られてきたような時に発生する
414 Request-URI Too Long は GET 時のクエリパラメータに長すぎるデータが指定された場合などに発生する

415 Unsupported Media Type は、リクエストヘッダの Content-Type で指定されているデータ形式にサーバが対応していないケースで発生する
例えば、XML に対応していない API に XML を送り、Content-Type に application/xml を指定している場合などが該当する

429 Too Many Requests は、アクセスの許容範囲の限界を超えた場合に返るエラー

500 番台:サーバに問題があった場合

500 Internal Server Error は、サーバ側のコードにバグがあってエラーを吐いている場合
503 Service Unavailable は、サーバが一時的に利用できない状態になっていることを示すエラー

キャッシュと HTTP の仕様

HTTP のキャッシュには以下の二つのタイプがある

  • Expiration Model(期限切れモデル)・・・レスポンスデータに保存期限を決めておき、期限が切れたら再アクセスさせる
  • Validaton Model(検証モデル)・・・今保持しているキャッシュが最新であるかを問い合わせ、更新されていた場合にのみ取得を行う

Expiration Model(期限切れモデル)

HTTP 1.1 の定義によると、実現方法は以下の2つ

  • Cache-Control レスポンスヘッダを使う
  • Expires レスポンスヘッダを使う

特定の日時に変更されることがあらかじめわかっているデータの場合は Expires で日時を指定する
今後更新される可能性のない静的なデータの場合は、一年後の日時を指定することで、キャッシュをしばらく有効にできる
Cache-Control は定期更新ではないものの更新頻度がある程度限られているものや、更新頻度は低くないものの、あまり頻繁にアクセスして欲しくない場合に利用できる

max-age の計算には Date ヘッダを使う
HTTP の仕様により 500 番台のエラーの場合などいくつかの例外を除き、必ずつけなければならない

Validation Model(検証モデル)

検証モデルを行うには、条件付きリクエストに対応する必要がある
条件付きリクエストとは、「もし今保持している情報が更新されていたら情報をください」というもの
更新されていたときのみデータを返し、更新されていなかったら 304 Not Modified を返す

条件付きリクエストを行うには、「クライアントが現在保持している情報の状態」をサーバに伝える必要がある
そのためには、最終更新日付とエンティティタグのどちらかを指標として使う

キャッシュをさせたくない場合

API の性格によってはキャッシュを全くさせたくない場合もある
そうした場合は HTTP ヘッダを使って明示的に「キャッシュをして欲しくない」と伝えることができる

Cache-Control: no-cache

no-cache は厳密には「キャッシュをしない」という指定ではなく、最低限「検証モデルを用いて必ず検証を行う」必要があることを意味する
機密情報などを含むデータで、中継するプロキシサーバには保存をして欲しくない、という場合には no-store を返す

Vary でキャッシュの単位を指定する

キャッシュを行う際に、URI 以外にどのリクエストヘッダ項目をデータを一意に特定するために利用するかを指定する
例えば、緯度経度から住所に変換できる API が、返す住所情報の表示言語を Accept-Language の内容によって切り替える、といったケースで必要になる(URI だけでは内容が同一ではなくなるため、キャッシュに残った誤った情報が表示されてしまう)

そこで、Vary ヘッダを使い、キャッシュするかどうかの判断条件にどのリクエストヘッダを使うかを指定する

Vary: Accept-Language

Cache-Control ヘッダ

Cache-Control ヘッダに指定できるディレクティブを以下に示す

  • public・・・キャッシュはプロキシにおいてユーザが異なっても共有することができる
  • private・・・キャッシュはユーザごとに異なる必要がある
  • no-cache・・・キャッシュしたデータは検証モデルによって確認が必要
  • no-store・・・キャッシュをしてはならない
  • no-transform・・・プロキシサーバはコンテンツのメディアタイプやその他内容を変更してはならない
  • must-revalidate・・・いかなる場合もオリジナルのサーバへの再検証が必要
  • proxy-revalidate・・・プロキシサーバはオリジナルのサーバへの再検証が必要
  • max-age・・・データが新鮮である期間を示す
  • s-maxage・・・max-age と同様だが中継するサーバでのみ利用される

メディアタイプの指定

レスポンスでは Content-Type というヘッダを利用してメディアタイプを指定する
例えば以下のような感じ

Content-Type: application/json
Content-Type: image/png

メディアタイプを Content-Type で指定する必要性

全ての API は適切なメディアタイプをクライアントに返すべき
なぜならクライアントの多くは、Content-Type の値を使ってデータ形式をまずは判断しており、その指定を間違えるとクライアントが正しくデータを読み出すことができないケースが出てくるから

x- で始まるメディアタイプ

サブタイプが x- で始まるメディアタイプがある
これはそのメディアタイプが IANA に登録されていないことを意味する

データ形式が新しく登場したものであったり、あまり一般的ではない場合には、IANA に登録されていないケースがある

  • application/x-msgpack
  • application/x-yaml
  • application/x-plist

また、現在は IANA に登録済みであっても、かつて登録前に x- で始まるサブタイプが利用されていて、現在もその歴史的経緯が残っている、という場合もある

  • application/x-javascript
  • application/x-json
  • application/x-png

自分でメディアタイプを定義する場合

インターネット上に広く API を公開する場合はベンダツリーを使うのが最も適している
vnd. に続いて団体名などがきて、具体的なフォーマット名を指定するような書式になる

application/vnd.companyname.awesomeformat

JSON や XML を用いた新しいデータ形式を定義する場合

+xml や +json のように、用いたデータ形式を + に続けて記述するべき、とされている
RSS や Atom のデータ形式はこのルールにしたがっている

  • application/rss+xml
  • application/atom+xml

GitHub ではこんな感じで定義している

HTTP/1.1 200 OK
Server: GitHub.com
Content-Type: application/json; charset=utf-8
X-GitHub-Media-Type: github.v3

リクエストデータとメディアタイプ

リクエストの際にもメディアタイプは利用される
主に使われるヘッダは以下の2つ

  • Content-Type
  • Accept

Content-Type は、レスポンスヘッダの場合と同様、リクエストボディがどんなデータ形式で送られているのかを示す
Accept は、クライアントが「どんなメディアタイプを受け入れ可能か」をサーバに伝えるために利用する

独自の HTTP ヘッダを定義する

適切なヘッダが存在しないメタデータを送りたい場合は、独自の HTTP ヘッダを定義する
例えば以下のような感じ

X-AppName-PixelRatio: 2.0

HTTP ヘッダを新しく定義する場合はこのように X- という接頭辞を最初につけて、次にサービスやアプリケーション、組織などの名前をつける、というのが一般的

設計変更をしやすい Web API を作る

API をバージョンで管理する

古い形式でアクセスしてきているクライアントに対してはそれまでと変わらないデータを送り、新しい形式でのアクセスには、新しい形式のデータを返す(複数のバージョンの API を提供する)

例えば Tumblr の API はこんな感じ

http://api.tumblr.com/v2/blog/good.tumblr.com/info

ほかにも以下のような指定方法があるが、特に強いこだわりがなければ URI にパスで指定する方式が無難と思われる

  • バージョンをクエリ文字列に入れる方法
  • メディアタイプでバージョンを指定する方法

バージョン番号をどうつけるか

バージョニングのルールとしては、セマンティックバージョニングが広く知られている
メジャー、マイナー、パッチの数値を繋いで 1.2.3 のような表記で表現され、以下のようなルールが適用される

  • パッチバージョンはソフトウェアの API に変更がないバグ修正などを行った時に増える
  • マイナーバージョンは後方互換性のある機能変更、あるいは特定の機能が今後廃止されることが決まった場合に増える
  • メジャーバージョンは後方互換性のない変更が行われた場合に増える

Facebook や Twitter はマイナーバージョンまでを含めているが、このパターンは少数派
URI に含めるのはメジャーバージョンまででいいのでは?というのが筆者の意見

API の提供を終了する

API のバージョンを増やすと、API を公開する側のメンテナンスコストも、それを利用する側のメンテナンスコストも増えてしまうため、古いバージョンのサポートを終了していく必要がある

広く一般に公開している API の場合、事前に終了日をアナウンスして、それまでに対応してくれるように周知徹底する必要がある
(Twitter が 1.0 を廃止する際にやった対応がケーススタディとして参考になりそう)
API の終了を告知してから、最低6ヶ月は公開を続けるのがいいのではないか、というのが筆者の考え

堅牢な Web API を作る

Web API を安全にする

API でのセキュリティの問題に注目し、API について最低限やっておくべき対策をあげる

  • サーバとクライアント間での情報の不正入手
  • サーバの脆弱性による情報の不正入手や改ざん
  • ブラウザからのアクセスを想定している API における問題

サーバとクライアント間での情報の不正入手

HTTPS による HTTP 通信の暗号化

最も簡単で、なおかつ効果のある方法は HTTP による通信を暗号化すること
HTTP 通信を暗号化する方法として最も多く使われ、簡単に導入できるのが HTTPS という TLS による暗号化
HTTPS を利用すると、サーバとクライアントの間の通信は暗号化され、途中で経由する中継サーバやネットワーク上でその中身を見ることができなくなる

HTTPS による通信を行う場合には、サーバが送ってきた SSL サーバ証明書を受け取るが、その際にその証明書が不正なものでないかをきちんと確かめる必要がある
それを確かめていない場合、中間者攻撃(MITM)による盗聴などが行われる危険性がある

MITM とはクライアントとサーバの通信経路の間に入り込んで中継を行うことで情報を盗み出す手法
証明書の発行元が信頼できるか、証明書の有効期限、サーバの証明書のコモンネームが接続しようとしているサーバと一致しているかどうか、といった証明書の検証を行う必要がある

ブラウザでアクセスする API における問題

XSS

XSS は Web アプリケーションにおいて HTML にデータが埋め込まれる場合だけでなく、API として JSON のようなデータを返す場合でも注意する必要がある
例えばユーザ名に埋め込まれた JavaScript が入力のチェックをすり抜けて JSON にも格納されてしまい、それを受け取ったブラウザが画面上に表示してしまう、みたいなケースがありうる

一般的には、ユーザからの入力をチェックし、データをユーザに返す際におかしな値を取り除く必要がある
API の場合は、

  • Content-Type に application/json を必ず返す
  • Content Sniffering を無効にするため X-Content-Type-Options: nosniff を指定する

X-Content-Type-Options は IE7 以前のブラウザには効果がない
そこで、さらなる対策として「追加のリクエストヘッダのチェック」と「JSON 文字列のエスケープ」を施す必要がある

XSRF

サイトをまたいで偽造したリクエストを送りつけることにより、ユーザが意図していない処理をサーバに実行させてしまうこと

一般的に取られる XSRF 対策は、XSRF トークンを使う方法
送信元となる正規のフォームに、そのサイトが発行したワンタイムトークン、あるいは少なくともセッションごとにユニークなトークンを埋め込んでおき、そのトークンがないアクセスは拒否する、というもの

JSON ハイジャック

JSON ハイジャックとは、API から JSON で送られてくる情報を悪意ある第三者が盗み取ることをいう

JSON ハイジャックを防止するためには、現在のところ以下のような対策が有効

  • JSON を SCRIPT 要素では読み込めないようにする
  • JSON をブラウザが必ず JSON と認識するようにする
  • JSON を JavaScript として解釈不可能、あるいは実行時にデータを読み込めないようにする

悪意あるアクセスへの対策を考える

パラメータの改ざん

サーバに送信するパラメータを勝手に変更してサーバに送信することで、本来取得できない情報を取得したり、サーバ側のデータを本来ならありえない側に変更したりすること

こうしたことを避けるために重要なのは、本来アクセスができないはずの情報はサーバ側できちんとチェックし、アクセスを禁止するようにしておくということ

セキュリティ関連の HTTP ヘッダ

X-Content-Type-Options

繰り返しになるけれど、大事なところなのでもう一度

X-Content-Type-Options: nosniff

X-XSS-Protection

ブラウザが備えている XSS の検出、防御機能を有効にするヘッダ
IE8 以上、Chrome と Safari にこの機能が実装されている

X-XSS-Protetion: 1; mode=block

X-Frame-Options

このヘッダを設定することで、指定したページがフレーム内で読み込まれるかどうかを制御することができる
IE8 以上、Chrome や Safari、FireFox などのブラウザが対応している

X-Frame-Options: deny

Content-Security-Policy

読み込んだ HTML 内の img 要素、script 要素、link 要素などの読み込み先としてどこを許可するのかを指定するためのヘッダ
XSS の危険性を低減することができる

Content-Security-Policy: default-src `none`

Strict-Transport-Security

Http Strict Transport Security(HSTS) を実現するためのヘッダ
このヘッダを利用すると、あるサイトへのブラウザからのアクセスを HTTPS のみに限定させることができる

Strict-Transport-Security: max-age=15768000

Public-Key-Pins

HTTP-based public key pinning(HPKP) のためのヘッダ
SSL 証明書が偽造されたものでないかをチェックするために利用する

Public-Key-Pins: max-age=2592000;
      pin-sha256="(省略)";
      pin-sha256="(省略)"

Set-Cookie ヘッダとセキュリティ

ブラウザでセッションを扱う場合はクッキーをセッション管理に使う場合が多いが、その際にもセキュリティを考慮しておくことが可能
そのために使うことができるのが Secure および HttpOnly という属性である

Set-Cookie: session=(省略); Path=/; Secure; HttpOnly

Secure 属性をつけることで、そのクッキーは HTTPS での通信の際のみサーバに送り返される
HttpOnly 属性をつけることで、そのクッキーは HTTP の通信のみで使われ、ブラウザで JavaScript などのスクリプトを使ってアクセスすることができないものであることを示せる

大量アクセスへの対策

ユーザごとのアクセスを制限する

一度の大量のアクセスがやってきてしまう問題を解決するための最も現実的な方法は、ユーザごとのアクセス数を制限することである
単位時間あたりの最大アクセス回数(レートリミット)を決め、それ以上のアクセスがあった場合にエラーを返すようにする

レートリミットを行うにあたっては、以下のようなことを決める必要がある

  • 何を使ってユーザを識別するか
  • リミット値をいくつにするか
  • どういう単位でリミット値を設定するか
  • リミットのリセットをどういうタイミングで行うか

制限値を超えてしまった場合の対応

レートリミットを超えた場合は 429 Too Many Requests を返す
RFC の中でこのステータスコードについては以下のように書かれている

  • エラーの詳細をレスポンスに含めるべきである(SHOULD)
  • Retry-After ヘッダを使って次のリクエストをするまでにどれくらい待てば良いかを指定しても良い(MAY)

レートリミットをユーザに伝える

レートリミットを行なった場合、現在のリミットアクセス数やどれくらいすでにアクセスしているのか、それがリセットされるのはいつか、などの情報をユーザに知らせてあげた方が親切

Twitter や GitHub では、レートリミットを知るための専用の API を用意している

HTTP のレスポンスでレートリミットを渡す場合は、HTTP ヘッダに入れるのが現時点でのデファクトスタンダード

  • X-RateLimit-Limit・・・単位時間あたりのアクセス上限
  • X-RtaeLimit-Remaining・・・アクセスできる残り回数
  • X-RateLimit-Reset・・・アクセス数がリセットされるタイミング
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.