Web API The Good Parts 読書メモ
2.4 APIエンドポイント設計
Restful APIではパスパラメータにリソースを識別する情報を入力する。ユーザIDならば基本的にはクエリパラメータではなくパスパラメータに入れる。
目的 | エンドポイント | メソッド |
---|---|---|
ユーザー一覧取得 | http://api.example.com/v1/users |
GET |
ユーザー新規登録 | http://api.example.com/v1/users |
POST |
特定のユーザー情報の取得 | http://api.example.com/v1/users/:id |
GET |
ユーザの情報の更新 | http://api.example.com/v1/users/:id |
PUT/PATCH |
ユーザの情報の削除 | http://api.example.com/v1/users/:id |
DELETE |
リレーションを表すリソースの変更(特にDELETE)を行う場合は、リレーションのIDを指定するか、そのリレーションの構成要素のIDを指定するかのオプションがあるが、基本的にはそれぞれの構成要素のIDを指定すれば良い。
- なぜなら、利用者にはAPIの裏側で何が起きているかを見せる必要はなく、見せない方がHackableから。
- 友達の削除APIなら
http://api.example.com/v1/users/:id/friends/:id
に DELETE
注意すべき点
- 複数形の名詞を利用する
- Naming
- スペースやエンコードを必要とする文字を使わない
- ハイフンで単語を繋げる(spinal case / chain case)
2.5 検索とクエリパラメータの設計
ページネーションは
-
per_page
とpage
-
limit
とoffset
offset と limit を使用すると RDBによっては先頭から読み込んでしまうのでとても遅くなる。
また、更新頻度が高いと相対位置がリクエスト毎に変更されるので一貫性に問題が生じる場合がある。
絶対位置を指定することが対策として有効
絞り込みはクエリパラメータのname
とかq
で行う。
-
name
は全文一致 -
q
は部分一致
2.5 ログインとOAuth2.0
現在は非推奨になっているResource Owner Password Credentials フローを紹介していたりと情報が少し古いので話半分でいい気がする。
トークンをクエリパラメータに入れる例が書いてあるけど普通に脆弱な気がするので本当か?になっている。
自分自身の情報を返すエンドポイントにself
とかme
がある例が紹介されているがJWTトークンの中に自分のID入っているんじゃなかったっけ、もうほとんど忘れてしまったけど
2.9 HATEOAS と REST LEVEL3 API
リチャードソンが提唱して、マーティンファウラーが解説した記事によればREST APIの設計レベルはいかに分かれるらしい
- REST LEVEL0: HTTPを使ってる
- REST LEVEL1: リソース概念の導入
- REST LEVEL2: HTTPの動詞(GET / POST / PUT / DELETEなど) の導入
- REST LEVEL3: HATEOAS
2章まとめ
- 覚えやすく、どんな機能を持つか一目でわかるエンドポイント
- 適切なHTTPメソッド
- 適切な英単語を利用し、単数系と複数形に注意
- 認証にはOAuth2.0を使う
- OAuthは認可プロトコルなので認証に使ってはいけない。今はOIDCでは?
3.2 JSONP
同一生成元ポリシーを超えて別のオリジンからデータを取得するために、callback(<JSON data>)
みたいな感じのレスポンスを返すと、scriptタグがそれを読み込んで関数を実行できる、みたいなやつ
同一生成元ポリシーが何で存在したかを考えれば普通に脆弱な気がするんだけど調べたらそのような情報が見つかる。話半分でよさそう。
と思ったらコラムに書いてあった。
3.3 レスポンスデータの構造
- APIへのアクセス回数がなるべく減るようにする
- ネットワーク越しの通信は重いから?
- マイクロサービスとかで閉じてる場合はどうなんだろう
- 返したデータがどのように使われるかを考える
レスポンスの内容をユーザが選べるようにするのは通信量の削減に効果的。
api.example.com/v1/users/12345?fields=name,age
とか
全てのAPIに共通的なメタデータを入れる構造をエンべロープというが、基本的には不要
- HTTPがエンベロープの役割を果たしているから。
Google の JSON Style Guide によれば、レスポンスはなるべくフラットにしたほうが良いが階層構造を持った方がわかりやすい場合もあると書いてある
- 送信ユーザ、受信ユーザ、のように同じスキーマを示す場合は階層構造があった方がわかりやすい
- 一方、ただ何らかのコンテキストでまとめるだけの階層は不要。
日付はRFC3339がおすすめ
JavaScriptは巨大な数字を表せない(IEEE754浮動小数点数で表すので誤差が生じる)ため、文字でも返すことを検討しても良い
{
"id": 123456789123456789
"id_str": "123456789123456789"
}
文字列で返すとJavaScriptならBigInt型でパースすれば良くなる
3.6 エラー処理
- ステータスコードでエラーを表現する
- エラー詳細情報はボディに入れている場合が多い
- Webサーバーやフレームワークのデフォルト設定によってエラー時にHTMLを返してしまう場合があるがクライアントが落ちかねないのでJSONで返した方がいい
- APIをメンテナンスするときは503を返して
Retry-After
ヘッダを入れる
4.3 キャッシュ
Expiration モデル
- Expiresヘッダ: キャッシュがstaleになる時間を指定
- Cache-Controlヘッダ: 色々
- max-age: Date ヘッダからの経過時間がmax-ageを超えたらキャッシュをstaleとする
Validationモデル
最終更新日付かエンティティタグのどちらかを送って最新かどうかをサーバー側で判断する
- Last-Modifiedヘッダ: 最終更新日付
- ETag: レスポンスデータのハッシュ値など(実装は任意)
クライアントからのリクエストヘッダ
- If-Modified-Since: 最終更新日付
- If-None-Match: Etag
変更がなければ304を返し、変更があれば200と変更内容を送る
リクエストヘッダの内容によってAPIのレスポンスが変わる場合がある
- Accept-Language など
そのためエンドポイントだけ見てキャッシュされているかどうかを判断するのは危険である。
どのリクエストヘッダがキャッシュ影響するかはVaryヘッダで指定する。
4.5 クロスオリジンリソース共有
- オリジン: スキーム、ホスト、ポート番号の組み合わせ
- 同一生成元ポリシー: 異なるオリジンへのXHTTPRequestを原則禁止するブラウザの動作
- CORS (Cross Origin Resource Sharing): 特定のオリジンからのアクセスを許可する仕組み
同一生成元ポリシーの目的
- CSRF: 脆弱なwebサイトへ、被害者の認証情報で書き込みなどの操作をさせる攻撃
- 掲示板などにCSRF脆弱性のあるサイトへのリクエストを含むリンクを貼ったとき、被害者がそのサイトへログインしていれば、攻撃リンクをクリックすると、リクエストが成功してしまう。=被害者の意図しない操作を行うことができる
- デフォルトで異なるオリジンへのリクエストをブラウザがブロックする(= 同一生成元ポリシー)ことで以上の攻撃を防げる
同一生成元ポリシーがブロックするのは非同期リクエストだけ?攻撃リンクのクリックした場合は防げない?
普通にCSRFは関係ないかも。
悪意のあるサイトでJavaScriptが実行されXHTTPRequestで正規サイトへリクエストした場合に、それをブラウザでブロックすることができる。同一生成元ポリシー(=ブラウザ側の対策)がなくてもCSRF対策(=サーバ側の対策)で防げる場合もあるかもしれないが、基本的には別のレイヤーの対策として理解した。
CORSのやりとり
- クライアントから
Origin
ヘッダを付与してAPIサーバへリクエストする - APIサーバではアクセスを許可するオリジンのリストを保持しておいて、
Origin
ヘッダのオリジンがそれに含まれているかを確認する。- 含まれていない場合は403
- 含まれている場合は、
Access-Control-Allow-Origin
ヘッダにリクエストヘッダと同じオリジンを入れて返す、またはAccess-Control-Allow-Origin: *
とする
Access-Control-Allow-Origin
ヘッダがついていないとブラウザはレスポンスを拒否する。
- オリジンをチェックしているのかいないのかの確証が持てないため。
プリフライトリクエスト
特別なリクエストを行う前に、APIサーバにそれを受け入れられるかどうかを問い合わせるリクエスト
- 存在意義がよくわからない…
ユーザ認証情報
Credentialを受け取って返信する際には追加のHTTPレスポンスヘッダを発行する必要がある。
-
Access-Control-Allow-Credentials: true
というヘッダをつけない場合、ブラウザはレスポンスを拒否する。
XHTTPRequestで送信する際にはwithCredentials
をtrue
にしないといけない
const xhr = new XMLHttpRequest();
xhr.open("GET", "http://example.com/", true);
xhr.withCredentials = true;
xhr.send(null);
- どういう意図があるのか…?
プリフライトリクエストの意義
PUTメソッドには(HTMLフォームから飛んでこないので、)CSRF対策をしていない場合、悪意あるサイトのXHRでPUTメソッドを使ってCSRFが成功してしまう
オリジンを見たら防げるのでは?やはりよくわからない- 勘違いしていて、プリフライトリクエストはブラウザ側の対策。XHRで単純でないリクエストを送信する場合に事前にブラウザが勝手に送信するリクエスト。
- オリジン見たら防げるのはそれはそうだけど対策のレイヤーが異なる。
- もしAPIサーバーがオリジンを見てなかったらCSRFが成功してしまう(急にブラウザにこのような仕様を追加した途端に脆弱になるのを避けたい、という歴史的経緯)
サーバーからはレスポンスに
Access-Control-Allow-Origin
Access-Control-Allow-Methods
Access-Control-Allow-Heders
Access-Control-Allow-Methods
などをつける必要がある。ついていないと、ろくにチェックしてないと判断してそのあとの単純でないリクエストを行わない。
- 繰り返しだがここでリクエストを送ってしまうとCSRFが成功する。
- CORS導入前はクロスオリジンでXHRをそもそも原則送れなかった。CORSをブラウザに追加した時に、CSRFができなかったサイトにできるようになってはいけないので、こうなっている
ユーザ認証情報を受け取ったときの動作も同様で、今までできなかったXHRができるようになったので、認証情報をつけてXHRしたときの動作は特に気をつけないといけない(認証情報がついているとはすなわち機密情報にアクセスしているということである)
なのでCORS対応していることをサーバからアピールしないとブラウザは安心できないので、こうなっている
5章 バージョニング
Web API を頻繁に変更するなと書かれている
- 変更する場合は、既存クライアントに影響が出ないようにする。パスにバージョンを明記するのが一般的。
軽微な変更で後方互換性を破壊するな!
- gender のフィールドで1を男性、2を女性にしていたのをmaleとfemaleに変えたくなったら、新しいgenderStrとかの項目を追加する方が安全
- メジャーバージョンアップでgenderは廃止されることを予告しておく
- セキュリティ上の更新が必要になったら後方互換性を破壊するのはやむなし
やむなく古いバージョンを削除する場合は、事前に終了日時をアナウンスしなるべく影響の出ないようにする
- ツイッターは1.0を消すときに一時的に消して影響が出るかどうかをテストするBlackout Testをおこなった
- あらかじめ終了時の処理を仕込んでおく。
- 終了したら410 Gone を返し、410を受け取ったときのクライアントアクション(強制アップデートとか)を事前に定義しておく、とか
- 利用規約にサポート期間を明記しておく。
6章 セキュリティ
-
X-Content-Type-Options: nosniff
をつける - JSON文字列をエスケープする
- GETメソッドで変更を起こす操作を実装しない
- imgタグで攻撃可能になってしまう
- CSRF トークンを使う
- JSONのトップレベルを配列(
[]
)にしない- 配列はJavaScriptとして正しいため、スクリプト実行される可能性がある
6.4 認証されたユーザによる攻撃
- マイナスの値をリクエストしてアイテムを増やす
- 値範囲をバリデーションしていないことが原因
- リクエスト再送信
- 購入IDの一意性をバリデーションしないと正規の購入リクエストを2回送れば1回しか購入していないのにも関わらず2回購入できる
6.5 セキュリティ関連のHTTPヘッダ
X-Content-Type-Options
X-XSS-Protection
X-Frame-Options
-
Content-Security-Policy
- 間違ってHTMLとして解釈された場合に、ほかのリソースを読まないようにする
Strict-Transport-Security
Public-Key-Pins
Set-Cookie
-
Secure
属性をつけるとクッキーはHTTPSでのみ送り返される -
HTTPOnly
JavaScriptからクッキーの値にアクセスできなくなる
6.6 レートリミット
- 429 Too Many Requests
- エラーの詳細をレスポンスに含めるべき
-
Retry-After
ヘッダで次のリクエストまでの時間を指定してもよい
標準ではないが、以下のヘッダを実装しているサービスも多い
- X-RateLimit-Limit: 単位時間当たりのアクセス上限
- X-RateLimit-Remaining: アクセスできる残り回数
- X-RateLimit-Reset: アクセス数がリセットされるタイミング
レートリミットの実装
詳しく書いていないが、RedisとかのKVSを使うといいよと書いてある