Closed36

Web API The Good Parts 読書メモ

alkshmiralkshmir

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
alkshmiralkshmir

リレーションを表すリソースの変更(特にDELETE)を行う場合は、リレーションのIDを指定するか、そのリレーションの構成要素のIDを指定するかのオプションがあるが、基本的にはそれぞれの構成要素のIDを指定すれば良い。

  • なぜなら、利用者にはAPIの裏側で何が起きているかを見せる必要はなく、見せない方がHackableから。
  • 友達の削除APIなら http://api.example.com/v1/users/:id/friends/:id に DELETE
alkshmiralkshmir

注意すべき点

  • 複数形の名詞を利用する
  • Naming
  • スペースやエンコードを必要とする文字を使わない
  • ハイフンで単語を繋げる(spinal case / chain case)
alkshmiralkshmir

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

ページネーションは

  • per_pagepage
  • limitoffset

offset と limit を使用すると RDBによっては先頭から読み込んでしまうのでとても遅くなる。
また、更新頻度が高いと相対位置がリクエスト毎に変更されるので一貫性に問題が生じる場合がある。

絶対位置を指定することが対策として有効

alkshmiralkshmir

絞り込みはクエリパラメータのnameとかqで行う。

  • name は全文一致
  • qは部分一致
alkshmiralkshmir

2.5 ログインとOAuth2.0

現在は非推奨になっているResource Owner Password Credentials フローを紹介していたりと情報が少し古いので話半分でいい気がする。

トークンをクエリパラメータに入れる例が書いてあるけど普通に脆弱な気がするので本当か?になっている。

alkshmiralkshmir

自分自身の情報を返すエンドポイントにselfとかmeがある例が紹介されているがJWTトークンの中に自分のID入っているんじゃなかったっけ、もうほとんど忘れてしまったけど

alkshmiralkshmir

2.9 HATEOAS と REST LEVEL3 API

リチャードソンが提唱して、マーティンファウラーが解説した記事によればREST APIの設計レベルはいかに分かれるらしい

  • REST LEVEL0: HTTPを使ってる
  • REST LEVEL1: リソース概念の導入
  • REST LEVEL2: HTTPの動詞(GET / POST / PUT / DELETEなど) の導入
  • REST LEVEL3: HATEOAS

https://martinfowler.com/articles/richardsonMaturityModel.html

alkshmiralkshmir

HATEOAS って何

hypermedia as the engine of application state の略

要はAPI レスポンスに次に見るべきリソースへのリンクを入れて返すべき、という主張。
ハイパーメディアとは、リンクによって複数のメディアが繋がったもの。

alkshmiralkshmir

結局 HATEOAS or REST LEVEL3 を採用すべきなのかどうか

  • SSKD向けAPIならニーズ次第で採用可能
  • LSUD向けAPIでは時期尚早では?

と書かれている

alkshmiralkshmir

2章まとめ

  • 覚えやすく、どんな機能を持つか一目でわかるエンドポイント
  • 適切なHTTPメソッド
  • 適切な英単語を利用し、単数系と複数形に注意
  • 認証にはOAuth2.0を使う
    • OAuthは認可プロトコルなので認証に使ってはいけない。今はOIDCでは?
alkshmiralkshmir

3.2 JSONP

同一生成元ポリシーを超えて別のオリジンからデータを取得するために、callback(<JSON data>)みたいな感じのレスポンスを返すと、scriptタグがそれを読み込んで関数を実行できる、みたいなやつ

同一生成元ポリシーが何で存在したかを考えれば普通に脆弱な気がするんだけど調べたらそのような情報が見つかる。話半分でよさそう。
https://blog.ohgaki.net/stop-using-jsonp

alkshmiralkshmir

3.3 レスポンスデータの構造

  • APIへのアクセス回数がなるべく減るようにする
    • ネットワーク越しの通信は重いから?
    • マイクロサービスとかで閉じてる場合はどうなんだろう
  • 返したデータがどのように使われるかを考える
alkshmiralkshmir

レスポンスの内容をユーザが選べるようにするのは通信量の削減に効果的。
api.example.com/v1/users/12345?fields=name,age

とか

alkshmiralkshmir

全てのAPIに共通的なメタデータを入れる構造をエンべロープというが、基本的には不要

  • HTTPがエンベロープの役割を果たしているから。
alkshmiralkshmir

Google の JSON Style Guide によれば、レスポンスはなるべくフラットにしたほうが良いが階層構造を持った方がわかりやすい場合もあると書いてある

  • 送信ユーザ、受信ユーザ、のように同じスキーマを示す場合は階層構造があった方がわかりやすい
  • 一方、ただ何らかのコンテキストでまとめるだけの階層は不要。
alkshmiralkshmir

JavaScriptは巨大な数字を表せない(IEEE754浮動小数点数で表すので誤差が生じる)ため、文字でも返すことを検討しても良い

{
    "id": 123456789123456789
    "id_str": "123456789123456789"
}

文字列で返すとJavaScriptならBigInt型でパースすれば良くなる

alkshmiralkshmir

3.6 エラー処理

  • ステータスコードでエラーを表現する
  • エラー詳細情報はボディに入れている場合が多い
  • Webサーバーやフレームワークのデフォルト設定によってエラー時にHTMLを返してしまう場合があるがクライアントが落ちかねないのでJSONで返した方がいい
  • APIをメンテナンスするときは503を返してRetry-Afterヘッダを入れる
alkshmiralkshmir

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と変更内容を送る

alkshmiralkshmir

リクエストヘッダの内容によってAPIのレスポンスが変わる場合がある

  • Accept-Language など

そのためエンドポイントだけ見てキャッシュされているかどうかを判断するのは危険である。
どのリクエストヘッダがキャッシュ影響するかはVaryヘッダで指定する。

alkshmiralkshmir

4.5 クロスオリジンリソース共有

  • オリジン: スキーム、ホスト、ポート番号の組み合わせ
  • 同一生成元ポリシー: 異なるオリジンへのXHTTPRequestを原則禁止するブラウザの動作
  • CORS (Cross Origin Resource Sharing): 特定のオリジンからのアクセスを許可する仕組み
alkshmiralkshmir

同一生成元ポリシーの目的

  • CSRF: 脆弱なwebサイトへ、被害者の認証情報で書き込みなどの操作をさせる攻撃
    • 掲示板などにCSRF脆弱性のあるサイトへのリクエストを含むリンクを貼ったとき、被害者がそのサイトへログインしていれば、攻撃リンクをクリックすると、リクエストが成功してしまう。=被害者の意図しない操作を行うことができる
  • デフォルトで異なるオリジンへのリクエストをブラウザがブロックする(= 同一生成元ポリシー)ことで以上の攻撃を防げる

同一生成元ポリシーがブロックするのは非同期リクエストだけ?攻撃リンクのクリックした場合は防げない?

alkshmiralkshmir

普通にCSRFは関係ないかも。
悪意のあるサイトでJavaScriptが実行されXHTTPRequestで正規サイトへリクエストした場合に、それをブラウザでブロックすることができる。同一生成元ポリシー(=ブラウザ側の対策)がなくてもCSRF対策(=サーバ側の対策)で防げる場合もあるかもしれないが、基本的には別のレイヤーの対策として理解した。

alkshmiralkshmir

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で送信する際にはwithCredentialstrueにしないといけない

const xhr = new XMLHttpRequest();
xhr.open("GET", "http://example.com/", true);
xhr.withCredentials = true;
xhr.send(null);

https://developer.mozilla.org/ja/docs/Web/API/XMLHttpRequest/withCredentials

  • どういう意図があるのか…?
alkshmiralkshmir

プリフライトリクエストの意義

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ができなかったサイトにできるようになってはいけないので、こうなっている
alkshmiralkshmir

ユーザ認証情報を受け取ったときの動作も同様で、今までできなかったXHRができるようになったので、認証情報をつけてXHRしたときの動作は特に気をつけないといけない(認証情報がついているとはすなわち機密情報にアクセスしているということである)
なのでCORS対応していることをサーバからアピールしないとブラウザは安心できないので、こうなっている

alkshmiralkshmir

5章 バージョニング

Web API を頻繁に変更するなと書かれている

  • 変更する場合は、既存クライアントに影響が出ないようにする。パスにバージョンを明記するのが一般的。
alkshmiralkshmir

軽微な変更で後方互換性を破壊するな!

  • gender のフィールドで1を男性、2を女性にしていたのをmaleとfemaleに変えたくなったら、新しいgenderStrとかの項目を追加する方が安全
    • メジャーバージョンアップでgenderは廃止されることを予告しておく
  • セキュリティ上の更新が必要になったら後方互換性を破壊するのはやむなし
alkshmiralkshmir

やむなく古いバージョンを削除する場合は、事前に終了日時をアナウンスしなるべく影響の出ないようにする

  • ツイッターは1.0を消すときに一時的に消して影響が出るかどうかをテストするBlackout Testをおこなった
  • あらかじめ終了時の処理を仕込んでおく。
    • 終了したら410 Gone を返し、410を受け取ったときのクライアントアクション(強制アップデートとか)を事前に定義しておく、とか
  • 利用規約にサポート期間を明記しておく。
alkshmiralkshmir

6章 セキュリティ

  • X-Content-Type-Options: nosniff をつける
  • JSON文字列をエスケープする
  • GETメソッドで変更を起こす操作を実装しない
    • imgタグで攻撃可能になってしまう
  • CSRF トークンを使う
  • JSONのトップレベルを配列([])にしない
    • 配列はJavaScriptとして正しいため、スクリプト実行される可能性がある
alkshmiralkshmir

6.4 認証されたユーザによる攻撃

  • マイナスの値をリクエストしてアイテムを増やす
    • 値範囲をバリデーションしていないことが原因
  • リクエスト再送信
    • 購入IDの一意性をバリデーションしないと正規の購入リクエストを2回送れば1回しか購入していないのにも関わらず2回購入できる
alkshmiralkshmir

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からクッキーの値にアクセスできなくなる
alkshmiralkshmir

6.6 レートリミット

  • 429 Too Many Requests
    • エラーの詳細をレスポンスに含めるべき
    • Retry-Afterヘッダで次のリクエストまでの時間を指定してもよい

標準ではないが、以下のヘッダを実装しているサービスも多い

  • X-RateLimit-Limit: 単位時間当たりのアクセス上限
  • X-RateLimit-Remaining: アクセスできる残り回数
  • X-RateLimit-Reset: アクセス数がリセットされるタイミング

レートリミットの実装

詳しく書いていないが、RedisとかのKVSを使うといいよと書いてある

このスクラップは2024/01/14にクローズされました