YAPC::Hokkaido 2016.12.10 Sat @札幌市産業振興センター
Yuji Shimada (@xaicron)
みなさん
APIサーバー書いてますか?
いろいろと API を作って来た自分がここ数年
iOS/Android アプリ向けの API を作るにあたって考えたことを発表します。
はい
すべてのAPIはSSLにすべし!
App Transport Security (ATS)
によって SSL にすることが推奨されているQ. HTTPS にしたらアクセス遅くないですか?
Q. HTTPS にしたらアクセス遅くないですか?
A. 一概には言えませんが、HTTP に比べたらハンドシェイクに時間がかかるケースが多いです。
しかし先述したように iOS からのリクエストはすべて ATS に準拠することが推奨されています。
Q. HTTPS にしたらフロントサーバーの負荷やばそう
Q. HTTPS にしたらフロントサーバーの負荷やばそう
A. SSL アクセラレータを買いましょう。
またはそれに準ずるなにかを利用するのがベターです。
(nginx + ssl cache on memcached 的なものとか)
Q. HTTPS にしたらフロントサーバーの負荷やばそう
A. SSL アクセラレータを買いましょう。
またはそれに準ずるなにかを利用するのがベターです。
(nginx + ssl cache on memcached 的なものとか)
もし、そういったものが利用できない場合でも、数年前に比べてハードウェアの性能が上がっていますので、SSL の処理が重くて死ぬ!みたいなことが発生するケースは減っているのではないかと思います。(要出典)
Q. え、API のアプリケーションサーバー自体も HTTPS に対応しないといけないの?
Q. え、API のアプリケーションサーバー自体も HTTPS に対応しないといけないの?
A. SSL アクセラレータまたはそれに準ずるなにかが HTTP にしてプロキシーすればいいので、イントラ内では不要と考えています。
Q. 現時点で HTTP/2 じゃないの?
A. もちろん HTTP/2 でもいいです。(将来的にはそうなるでしょう)
iOS9 からは NSURLSession
が、Android では OkHttp
が対応しています。
でも iOS8 をサポートする場合は別途 HTTP Client を選定する必要があるでしょう。
今後 SSL ではないことによるデメリットのほうが多くなっていくと思われますし、数年前に比べてもサーバーの性能は上がっており、ソフトウェアも改善されていっているので、早めに SSL 化をしてドヤりましょう
APIの認証について
というのが基本的な要件になります
JSON Web Token
(RFC7519) のこと。eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ6aWdvcm91IiwiZm9vIjoiYmFyIiwiZXhwIjoxNDgxMjExNzk4fQ.NQMMtnMK9L2JX6-7mheTGvUjMt9Vksq9RyX4GrKdbe0
みたいな文字列
Header
, Claims
, Signature
の 3つで構成されているHeader
をもとにどのような JWT を作成するかを決めるClaims
はいくつかの予約キーワードが存在するが、それ以外は自由に値を入れることができるSignature
を検証することで JSON が改ざんされていないことを保証できる# Header {"alg":"HS256"} # Claims {"iss":"nekokak","foo":"bar"}
を secret
で署名すると
eyJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJuZWtva2FrIn0.e1zHN8T8VkS8GYddVF3CJeJ5QyrXKQgG3x6AuuWOp18
のようになる
JSON::WebToken で書くと以下のような感じ
my $jwt = JSON::WebToken->encode( { iss => 'nekokak', foo => 'bar' }, # Claims 'secret', # secret 'HS256', # alg (デフォルトがHS256なので省略可) );
JWT を HTTP Header に入れてアプリから API リクエストを行う。このとき、JWT の Claims に認証情報をいれる。
例えば
などを送ることで、JWT を検証するのみで、ユーザーの認証が可能になります。
実際の利用ケースとしては以下のように Authorization ヘッダーにぶっこむなどする
と良いでしょう。
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJmb28iOiJiYXIiLCJpc3MiOiJuZWtva2FrIn0.e1zHN8T8VkS8GYddVF3CJeJ5QyrXKQgG3x6AuuWOp18
APIのデータ形式について
リクエスト形式は以下のようなもので
{ "id": "xxxxx", "jsonrpc": "2.0", "method": "someMethod", "params": { ... } }
レスポンスは以下のような感じ
{ "id": "xxxxx", "jsonrpc": "2.0", "result": { ... } }
たとえば、getTweet
のような API があったとしたら
{ "id": "xxxxx", "jsonrpc": "2.0", "method": "getTweet", "params": { "tweetId": "123456789" # ツイートのユニークな ID をしていする } }
{ "id": "xxxxx", "jsonrpc": "2.0", "result": { # ツイートオブジェクトが返ってくる "tweetId": "123456789", "screenName": "xaicron", "tweet": "お前はいままで逃した終電の数を覚えているか?", ... } }
のような感じになるでしょう。
とっても簡単ですね😊
リクエスト形式は以下のようなもので
{ "id": "xxxxx", "jsonrpc": "2.0", "method": "someMethod", "params": { ... } }
レスポンスは result
がなくて error
が返ってくる
{ "id": "xxxxx", "jsonrpc": "2.0", "error": { "code": -32600, "message": "Invalid Params" } }
さて、JSON-RPC は通信プロトコルまでは定義されていません。
HTTPS で通信することを決めましたので、HTTP プロトコル上で JSON-RPC を使う事になります。
たとえば、Invalid Params を例に上げると
400 Bad Request { "id": "foo", "jsonrpc": "2.0", "error": { "code": -32600, "message": "Invalid Params" } }
みたいな感じでしょうか?
たとえば、Invalid Params を例に上げると
400 Bad Request # <- HTTP Status { "id": "foo", "jsonrpc": "2.0", "error": { "code": -32600, # <- JSON-RPC Error Code "message": "Invalid Params" } }
なんかエラーコードが2つあるぞ...!!!
複数のエラーコードを処理するのは人類には早すぎる
アプリ側からは
アプリ側からは
-> この2つのルールを守るだけでアプリ側のエラー処理がものすごく簡単になる!
HTTP ステータスを最初に確認するときに簡単に原因が分かるので、大変はかどる
たとえば
たとえば
クライアントエンジニア (以下ク): なんか 401 返ってくるんだけど?
たとえば
クライアントエンジニア (以下ク): なんか 401 返ってくるんだけど?
サーバーエンジニア(以下サ): 社内ネットワークじゃないと Basic 認証かかってるよー
たとえば
たとえば
ク: 502 が返ってくるんだけど?
たとえば
ク: 502 が返ってくるんだけど?
サ: サーバー落ちてた、正直すまんかった
たとえば
たとえば
ク: 400 が返ってくるんだけど?
たとえば
ク: 400 が返ってくるんだけど?
サ: ドメインとかパスとかあってるンゴ?
大変わかりやすいですね😊
API種別毎にアクセスを分散したい
すべての API が同じ速度、同じリソース使用率、同じリクエスト数であることはまず無いので、APIごとに負荷を分載したいというニーズが自ずと出てくるかと思います。
JSON-RPC でいうと method 単位で切り分けたいですね。
また、API のバージョンなどで受け付けるサーバーを切り替えたりしたいことも考えられます。
しかし、JSON-RPC のみで API ごとにリクエストを分散するのは若干スマートではありません。
JSON-RPC over HTTP ではリクエストボディをパースして method
を得る必要があります。
{ "id": "xxxxx", "jsonrpc": "2.0", "method": "vearyHevyMethod", # この method は専用のサーバーに振り分けたい! "params": { ... } }
通常、API のアプリケーションサーバーが直接クライアントアプリからリクエストを受
け付けるのではなく、nginx や Apache などのフロントサーバーが存在していると思います。
愚直やるとリクエストボディを、フロントサーバーで JSON-RPC の解析をしなくてはいけなくて若干イケてません。
というわけで、普通に URL で分けることにしましょう。
具体的にいうと以下のような形式にします。
https://api.example.com/{version}/{method}
例えば以下のような形式にします。
https://api.example.com/{version}/{method}
例えば以下のような形式にします。
https://api.example.com/{version}/{method}
フロントで以下のような URL を受けられるようにすることで、それぞれの API のエンドポイント毎にリクエストを振り分けることが可能になります。
https://api.example.com/v1.0/getBestSpeakerAward
ただし、愚直にエンドポイントを追加していく方法だと、フロントと API 両方を変更する必要があるので、インフラエンジニアの人とあれこれしないと行けないですね。
ベースとして以下のようにワイルドカードで指定しておいて、必要なやつから徐々に分散していくのが良いでしょう
例えば nginx だったら
location ~ ^/v\d+.\d+/getBeer$ { proxy_pass http://beer.example.com/; } location ~ ^/v\d+.\d+/.+$ { proxy_pass http://all.example.com/; }
のような感じにしておけばカジュアルに method が追加できて便利ですね。
version
+ method
で URL を自動生成するようにしておけば、新しい method を追加したときでも何も気にせずにこの仕組に乗れるのでとっても簡単です!
はい
今日は珍しく API の中身の話ではなく、その周辺のアーキテクチャ的な話をしました。
ただ、今後はオンプレでこういうのを考えることは減っていくと思いますし、「それクラウドでw」という「それクラ」も増えていくでしょう。(というかそうなっている。)
だいたい2014年ぐらいに考えて実装した仕組みですが、だいたい現在もクラウドでマイクロサービスあんじゃ〜!って感じでそんなに当時考えていたことを未来がずれてなかったな〜という感じです。
とはいえ今後は API サーバーを自分で作るとかなくなっていくと思いますし、よりサービスを良いサービスを作る方向にフォーカスしやすくなっていって良い世の中だなって思いました!(小並感)
ご清聴ありがとうございました
ご質問タイム
完
j or →: next
k or ←: prev
h or ↑: list
l or ↓: return
o or ↵: open
? or /: toggle this help