WebAPI初心者用ガイドライン(URI設計、セキュリティ、ステータスコード、キャッシュ)
渋谷のIT企業でWebエンジニアをしています。
野澤です。
普段は自社パッケージの開発及び、そのパッケージを導入していただいた企業の機能開発や保守運用などをしています。
(たまにパッケージ関係ない案件でEC-CUBEなどの案件にも携わったりしています)
今回有志で自社のパッケージをフロントとバックエンドに分離するというプロジェクトを走らせる事になりました!
そこで自分がバックエンド側(API側)の旗振りをする事になったためWebAPIの勉強をしてガイドラインを作成しました。
事業部にWebAPIに精通している人が少なく…
事業部の人たちにまずはこの記事を読んでもらって、その後わからないことがあれば一緒に考えながら、開発を進めていければいいなと思っています。
参考にしたもの(勉強したもの)
↑の【REST WebAPI サービス設計(応用)の章】までの内容
↑の【Re:ゼロから始めるWeb API入門【基礎編】】
命名やURI設計について
・URIには動詞は含めてはいけない。
URIはサーバーに保存されたデータを識別するもの。
データの操作に関してはHTTPメソッド(主にGET、POST、PUT、DELETE)で表現する。
例. GET https://test.com/users/1 ←userのid1のデータを取得。
・短く入力しやすいく、冗長なパスを含まない、シンプルで覚えやすいようなURIにする。
・人が理解できるURIにする。できる限り省略した単語は使わない。
・大文字、小文字は混在させない。すべて小文字。
・単語はハイフンでつなげる。ただし、単語を結合する前にURIを見直す必要がある。
例:https://test.com/popular-users → https://test.com/users/popular
・単語は複数形を利用する。URIで表現しているのは「リソースの集合」のため。
❌user/12345 ⭕users/12345
・エンコードを必要とする文字は使わない(URIに全角文字など含まない)。→URIから意味が理解できないというのを避けるため。
・サーバー側のアーキテクチャを反映しない。悪意あるユーザーに脆弱性を突かれる危険性がある。
例えばURIにファイルの拡張子(.phpとか)が含まれているなど。
・システム依存のURIの設計はしない。意味がURIだけからでは理解できないため。
https://test.com/items/alpha/12345やhttps://test.com/items/beta/12345はだめ。https://test.com/items/12345で良い。alphaやbetaは何のことかぱっとわからない。
・ルールを統一する。
例えば友達情報取得は「https://test.com/friends?id=12345」なのにメッセージ投稿だと「https://test.com/friends/12345/message」となっているなど。
この例だと「https://test.com/friends/12345」で友達情報取得できるべき。
・複数のAPIを見比べて、最もよく使われている、最もしっくりくる単語を使うようにする。
実装に近いところ
・POSTはリソースの新規登録。データ作成時にリソース名が決まっていない時に利用。
・PUTは既存リソースの更新/リリースの新規登録に利用。新規登録時はPOSTと違い、データ作成時にリソース名が決まっている場合に利用。
・POSTとPUTの違いの例
POST:「/usersに対してPOST」←IDが決まっていない状態で新規登録。新規登録後IDが振られる。
PUT:「/users/12345に対してPUT」←IDを指定して登録。
・クエリ「例:/users?id=12345」とパス「/users/12345」の使い分け。
一意なリソースを表すのに必要な場合はパスを利用。省略可能であればクエリパラメータを利用。
例えば検索条件は省略可能なものであるためクエリパラメータを利用。
・Getで返す可能性があるステータスコードは成功時は200、304(Not Modified)。
失敗時400、401、403、404、429、500、503。
・POSTで返す可能性があるステータスコード:成功時は200(レスポンスに登録済みデータを含む)、201 Created(レスポンスボディが空でLocationに新しいリソースへのURL)、202 Accepted。
失敗時は400、401、403、409 Conflict(データが衝突)、429、500、503。
・PUTで返す可能性があるステータスコード:成功時は200(データ新規登録時。レスポンスに登録済みデータを含む)、201 created(データ新規登録時でリクエストボディにデータがなく、Locationに新しいリソースへのURLが入っている)、204 No Content(データ更新でレスポンスボディにデータが無い)。
失敗時、400、401、403、404(データ更新対象が存在しない)、409(データ登録時にその内容が衝突した)、429、500、503。
・Deleteで返す可能性があるステータスコード:成功時は200(200は通常レスポンスにデータが入っているときに使うため、Delete時はあまり使わない。)、202 Accepted、204 No Content(正常。200よりこっちを使う)。
失敗時は400、401、403、404(403を隠したいときの404)、429、500、503。
・返却するjsonにメタデータは入れない。(HTTPヘッダと役割がかぶるため冗長)
・時間あたりのアクセス制限=レートリミット を考慮する。
・キャッシュ制御を考慮する。
Expires、Cache-Control 、Last-Modified、ETagを利用。
・ 一つの作業を完結させるために複数回のアクセスを必要とするようなAPIの設計は【Chatty(おしゃべりな)API】と呼ばれる。
ChattyなAPI設計は避ける。
ChattyなAPIはネットワークのトラフィックを増加させ、クライアントの処理の手間を増やし、利用者になんだかめんどくさい仕様であるという印象を抱かせる。
・できる限り少ないアクセス回数ですむAPI設計を心がける。
・レスポンスの内容をユーザーが選べるようにする必要があるかどうか考える。
クエリパラメータでレスポンスの内容を選ぶことができるというふうにした方が良い場面もある。
・なるべく階層が浅いようなフラットなデータが返ってくるような設計にする。
階層化した方がわかりやすい場合は階層化する。
・レスポンスのjsonは配列ではなくオブジェクトで返すようにする。
メリット:
①レスポンスデータが何を示しているものかがわかりやすくなる。
②レスポンスデータをオブジェクトに統一することができる。
③セキュリティ上のリスクを避けることができる(JSONインジェクションを防ぐことができる)。
・APIの実装の変更には気を使うこと。
クライアントで利用されているということを忘れないこと。
APIの実装を変更して、クライアントアプリがバグるということは避けたい。
例えばクライアントがスマホアプリであれば、AppStoreとかAndroidマーケットとかの関係ですぐに変更を反映させることは難しいということと、たとえ新しいバージョンのアプリを出せたとしても、アップデートを必ずユーザーがしてくれるとは限らない。
知っておきたいステータスコードのこと
・201 Created:リクエストが成功し、新しいリソースが作成されたことを示す。ヘッダーのlocationに新しいリソースへのURLを含める。
・202 Accepted:非同期ジョブを受け付けたことを示す。実際の処理の結果は別途受け取る。
・204 No Content:リクエストは成功したが、レスポンスデータがない。クライアント側のビューを変更する必要が無いことを意味する。
・304 Not Modified:リクエストされたリソースを再送する必要がないことを示します。これはキャッシュされたリソースへの暗黙のリダイレクト。304が返ってきたらキャッシュがヒットしたということ。
・400 Bad Request:その他エラー。エラーの原因はリクエストの内容にあるよということしかわからない。
・401 Unauthorized:認証されていない。
・403 Forbidden:リソースに対するアクセスが許可されていない。(認可されていない)。
403を出してしまうとリソースが存在することがわかってしまうため404を出すようにするパターンもある。
・409 Conflict:リソースが競合して処理が完了できなかったことを示す。
・429 Too Many Requests:アクセス回数が制限回数を超えたため処理できなかったことを示す。
・503 Service Unavailable:サービスが一時的に利用できないことを示す。メンテナンス期間や過負荷で応答できないようなケース。
・API利用者はリダイレクトを実装していないことが多いのでREST APIでは基本的に3xxは利用しない。
知っておきたいキャッシュのこと
・キャッシュ制御に利用するヘッダーは2分類3パターン存在する。
有効期限による制御(Expires,Cache-Control + Date)、検証による制御(Last-Modified + ETag)。
・Expires:キャッシュとしていつまで利用可能かの期限を指定。
過去日を指定するとキャッシュが削除される。Cache-Controlが同時指定されている場合はExpiresは無視。
・Cache-Control+Date:キャッシュの可否、期限を指定。no-cacheはクライアントでキャッシュが保存されているが、それを利用していいかどうかをサーバーにきくという挙動。
・Last-ModifiedとETagに関して:
https://blog.redbox.ne.jp/http-header-tuning.html
・Varyというヘッダーを使えば、URL+そこで指定したヘッダの単位でキャッシュをすることができる。
・キャッシュについて考える際には中継するプロキシサーバーについても意識する必要がある。
プロキシサーバーがネットワーク通信量を減らすためにレスポンスデータをキャッシュする場合がある。
・天気情報が毎日同じ時間に更新される場合などはExpiresでその日時を指定することができる。
また今後更新される可能性がないデータや静的データの場合には遠い将来の日時を指定することで、一度取ったキャッシュデータをずっと保存しておくように指示を出すことができる。
・Cache-Controlは「毎日何時」などの定期更新ではないものの更新頻度がある程度限られているものや、更新頻度は低くないものの、あまり頻繁にアクセスしてはほしくない場合(例えばリアルタイム性がそれほど重要ではない情報やサーバーの負荷からアクセス頻度を下げて欲しい場合)に利用することができる。
知っておきたいセキュリティのこと
・XSS対策:レスポンスヘッダの追加。X-XSS-Protection(1でXSSフィルタリング)。X-Frame-Options(DENYでframeタグ呼び出し拒否)、X-Content-Type-Options(nosniffでIE脆弱性対応。IEは中身のデータをみてContent-Typeを判定する機能がありその機能を無効化する)。
Content-Typeをapplication/jsonで必ず返す。Json文字列のエスケープ(「+」も)。
・CSRF対策:許可しないアクセスもとからのリクエストを拒否(X-API-Keyヘッダ(システム単位)やAuthenticationヘッダ(ユーザー単位)を使って対応)。
攻撃者に推測されにくいトークンの発行、照合処理(X-CSRF-TOKENヘッダを使う)。
※X-API-KeyとX-CSRF-TOKENは独自ヘッダ。標準仕様ではないため、独自に設計する。
・JSONファイルを間違えてtext/htmlとして配信されてしまうとする。
このとき、URIを直接叩いてアクセスした場合、HTMLとして表示されてしまうためXSSのリスクが増えている。
だからJSONをちゃんとContent-Typeでapplication/jsonを指定してjsonを返却することが大切。
・HTTPSを使うことでAPIのやり取りの内容はもとより、エンドポイント、ヘッダに含められて送られるセッション情報などすべてが暗号化される。
HTTPSが正しく使われていれば、通信の盗聴、セッションハイジャックなどは不可能になる。
・scriptタグのsrcでWebAPIを叩かれるケース、FormからWebAPIを叩かれるケース、直接WebAPIを叩かれるケースの対策をしたいときは独自のヘッダーを追加してあげることが効果的。
現状srcやFormからヘッダーを追加してAPIを叩くことができないため。
リクエストヘッダーに独自のヘッダーが入っていなかったら不正な使われ方をしているという判定ができる。
・JSONハイジャック対策:
①JSONをscript要素から読み込めないようにする(前述のように独自ヘッダーを追加)。
②JSONをブラウザが必ずJSONと認識するようにする(Content-Typeをapplication/jsonと指定することを忘れない}。
③JSONをjavascriptとして解釈不可能、あるいは実行時にデータを読み込めないようにする(JSONを配列ではなくオブジェクトで返却するようにする。JSONの仕様ではJSONデータのトップレベルに存在できるのは配列[・・・]かオブジェクト{・・・})。
・第三者に関するセキュリティ以外にもちゃんとユーザー認証を行い、正しい利用者として認識されているクライアントが不正を働こうとしているというところに対しても対策を打つ必要がある。
・クライアントから送られてきた情報を信頼せず、サーバーでも整合性をきちんとチェックする必要がある。
・同じアクセスが同時に何度も実行されても大丈夫かどうかを考慮する必要がある。
例えば、クーポン券の処理のリクエストが本来一人に対して1リクエストだが、このリクエストが複数きたときなど(不正利用)。
・Set-Cookieヘッダーでは、Secure属性とHttpOnly属性をつけること。
Secure属性をつけると、HTTPSでの通信の際のみサーバーに送られるようになる。
HttpOnly属性をつけるとCookieがHTTPの通信のみで使われ、ブラウザでJavaScriptなどのスクリプトを使ってアクセスすることができなくなる。
XSSなどによってそのCookieに含まれたセッション情報が読み出されることを防止できる。