例えばの話
例えば, 電子書籍の販売サイトのWebAPIを作ることなったとしましょう.
そこで, せっかくだからRESTfulな設計を目指そうとしたアナタは, まずいちばんわかりやすい,
本一覧を取得するAPIから着手することにしました.
いろいろ本やサイトをあたった末にたどり着いたURLは
GET /books
対象リソースは本一覧だから/books
, 目的はデータ取得だからHTTPメソッドはGET.
まさに教科書通り, RESTfulAPIも簡単じゃないか.
ほんとにそうでしょうか?
もうすこし詳しい話
少し話を戻すとします.
この電子書籍販売サイトには, 実はユーザとして
- 利用者(本を買ったりレビューを書いたりするユーザ)
- 販売者(本を売る人)
- システム管理者
という3種類のロールがあり, それぞれ異なった目的でアプリを使っています.
ですので, 本一覧情報を取得したい, となった場合でも
- 自分が持っている本一覧の情報が見たい(利用者視点)
- 自分が販売している本一覧の情報が見たい(販売者視点)
- とにかく登録されている本一覧の情報が欲しい(システム管理者視点)
と異なったリストが欲しくなって来ることが考えられます.
そういったことを念頭において,
GET /books
という当初の想定で, 上記の要望を十全に満たすことはできるかというと, なかなかに怪しい感じに思えてきます.
そこで, これらの要求を全て満たしつつ, 最大限明確かつコンパクトなAPIとはどういったものになるのか,
少し考えてみたいと思います.
ユーザ認証について
本題に入る前に, API利用ユーザの認証方法について.
WebAPIを公開する上で, ユーザ認証のやり方はいろいろあると思いますが, 今回は自前に発行したAPIアクセスTokenを
HTTPのリクエストヘッダに付与する形で認証を行っているとします.
この方法の良し悪しについても, いろいろ議論はあるとおもいますが, 今回のところは気にしないでおいて下さい.
思いついた本一覧取得API案たち
案1 一つのURL / 暗黙的なパラメータ
この方法では, 本一覧が欲しい時は常に
GET /books
でサーバにアクセスし, サーバはアクセスユーザのIDとロールを確認して適切なデータを返します.
つまり, ID10の利用者ユーザがアクセスしてきたならば, そのユーザが持っている本一覧を返し
ID20の販売店ユーザがアクセスしてきたならば, その販売店が販売している本一覧を返します.
管理者ユーザだったら当然全件返します.
この方法の良い点は, ユーザは誰であれ, 本一覧が欲しい時は何も考えずに
GET /books
すれば良いので, APIのクライアント側としては細かいこと考えなくて良い点.
それと, APIの総数が少なくなるとこが期待できる点だと考えられます.
悪い点は, 全く同じAPI呼び出しなのにもかかわらず, アクセスユーザによって取得されるデータが異なる点です.
このことは
リソースを一意に識別する「汎用的な構文」
RESTful なシステムでは、すべてのリソースは URI (Uniform Resource Identifier) で表される一意的な (ユニークな) アドレスを持つ。
というRESTの原則に反しているというだけでなく,
例えばなんかのエラーが発生した時に, Webサーバのアクセスログが原因究明のための
追加情報を与えてくれないことも意味します.
(まあ、やり方次第かもしれませんが)
-
Pros
API全体がコンパクトになるかもしれない
API利用者が知るべき情報が減る -
Cons
URLに十分な情報を付与できない
RESTの原則に反する?
案2 一つのURL / 明示的なパラメータ
この方法は,
利用者が所持している本一覧を欲しい時は
GET /books?user=10
販売者が販売している本一覧を欲しい時は
GET /books?publisher=20
このように, リソース特定に必要な情報を全てクエリストリングとしてURLに付与する方法です.
良い点は, 案1と同様に, クエリストリングを除けば使っているAPIは一つだけなので,
APIの総数は少なくなる点.
(もっとも, 見かけ上のAPIが減るだけで実質上は, ドキュメントが必要ないほど
簡潔なAPIを意味はしないので, 良い点には挙げれないかもしれません.)
それと, 案1と異なり, URLに必要な全ての情報が付与されているので,
アクセスログがエラー時が原因究明の助けになり, URLとリソースの関係も1:1になります.
他に, 管理ユーザが, 特定販売店の代わりに処理を行いたいなどとなった要件にも,
問題なく対応できるなどの利点もあります.
悪い点は, 案1と違って, 欲しい情報に合わせてパラメータを適切に組み合わせていく必要が有るため
APIのクライアントから見るといちいちAPIドキュメントを調べる手間が発生する点.
また, 指定されたリソースが本当にユーザがアクセスして良いリソースかどうかの処理が, 一般化しづらいという問題もあります.
つまり,
GET /books?publisher=20
というアクセスが, 許可されたアクセスであるかどうかを判定するには
if ($this->request->has('publisher')) {
if ($this->user->role() !== 'publisher' || $this->user->id !== $this->request->input('publisher')) {
return false;
}
} else {$this->request->has('user') {
if ($this->user->role() !== 'user' || $this->user->id !== $this->request->input('user')) {
return false;
}
} else {
...
}
...
といったようなバリデーションをAPI毎に, いちいちクドクドと書いていく必要がある, ということです.
この例はわざとわかりにくいパラメータを採用しているだけであって, API全体を通して一貫したルールで
パラメータを設計していけば, こういった処理も十分に一般化することができる,
と考える人もいるかもしれませんがそんな緻密で遠大な設計は,
とても私にはできそうにありません.
-
Pros
APIの見た目上の総数は少なくなるかもしれない
URLで必要な情報全てを表現することができる
権限をまたいだデータアクセスなどにも対応できる -
Cons
コンパクトさが, 結局見た目上のものでしか無い可能性がある
案1よりも, API利用者に負担をかける
リソースへのアクセス許可を一般化することが難しい
案3 ユーザのロールごとのURL
この方法は,
利用者が所持している本一覧を欲しい時は
GET /users/10/books
販売者が販売している本一覧を欲しい時は
GET /publishers/20/books
このように, データ取得の目的毎に別APIを定義していくという方法です.
この方法の良い点/悪い点は大体案1のものの反対で, URLに十分な情報を付与できるようになる代わりに
目的ごとにAPIが作られるため, APIは膨張してゆき,
使う側からしたらどんなAPIがあるかを把握するのに一苦労という自体になってしまう可能性があります.
(まあ、案2のものよりは憶えやすいでしょうが)
最近のフレームワークを使っていれば, ロールごとにアクセスできるURLを指定する機能や
特定ルーティンググループにのみ共通した処理の差し込みを行う機能などは用意されていると思うので,
案2で問題になったアクセス許可のチェックを一般化も, 多少はやりやすくなると思います.
-
Pros
URLに十分な情報を付与できる
RESTの原則に準拠している
案2よりはアクセス許可の処理が簡単 -
Cons
APIの数が膨大になりがち
利用するためには十分なドキュメントが必要になる(メンテも大変)
おわりに
思いついたものをつらつらと書いてみましたが, いかがでしたでしょうか?
個人的には, おおむね案3の方が案2良いと思いますが
(無論, 任意の条件で本を検索するためのAPIを作りたいなら案2の方法にしますが)
かと言って, 細かすぎるAPIを設計して, 作る側も使う側もドキュメントに振り回されるようになるのも
いやだなあと, はっきりした答えはすぐには出せない感じです.
それぞれの案について見落としているメリット/デメリットが有る, もっと他の手法がある,
あるいは, そもそも言っていることが全然見当違いだなどの意見がありましたら, ぜひコメントください.
参考
REST : https://ja.wikipedia.org/wiki/REST
よくわかる認証と認可 : http://dev.classmethod.jp/security/authentication-and-authorization/