日曜日, 1月 19, 2014

次世代Webアーキテクチャーの話 (CROSS2014を聞いて) このエントリーを含むはてなブックマーク


 CROSS2014の次世代Webセッションを見て来た。
セッションの前半で議論されていたプロトコル編はしっかりとした方向性が示されていたが、後半のアーキテクチャー編は現状の混沌とした話が多くて、方向性というか新しいビジョンを示すまではいけなかった印象だった。

それは、サーバのアーキテクチャーが成熟していることも理由の一つなのかもしれない。
しかし、アーキテクチャーこそ方向性を示すのが重要だろうと思うので、個人的に考えていることをまとめることにした。 

Webスケールを実現する技術とリアルタイムWebの方向性


  リアルタイムWebというわけではないが、密結合なプロトコルはことごとくインターネットで玉砕されてきた歴史がある。 

古くはCORBA IIOP、DCOMの失敗。それからSOAPの失敗。 
(ちなみに、IIOPのIはInternetで、当初は大規模なインターネットスケールで分散させようとしたことがうかがえる) 

これらは何でうまくいかなかったのか。 

それは、簡単に言ってしまえば、単一のマシンから複数のマシン、そして、インターネットへと拡張していく際に失うもの、つまり整合性や信頼性の問題を考慮してなかったからだと思う。(詳しくは補足を参照)

 IIOPにしてもSOAPにしてもライブラリのAPIのような形で提供して、それをインターネットを跨いで同期的に実行することを想定していた。
 
これらは整合性や信頼性の問題を解決することなしに、単一マシンのアーキテクチャーをそのままインターネットの信頼できないマルチ・マシン上に拡張してしまったのだ。 
(かつてIBMでCORBAのプロジェクトに多少からんでいた私は、インターネット上では通用しないことを嫌というほど味わった。早く気づくことができていたら(特にIBMは)莫大な投資をせずに済んだのに。) 

ところで、リアルタイムWebの方向性はどうなんだろうか。

将来においても、既存のWebアーキテクチャの延長線上にあるものなのか。それとも、別の違う方向を進むのか。 また、何かリアルタイムWeb技術が急速に進んで、既存のWebアーキテクチャーがそれに置き換わるというような話になるのか。 

クライアントとサーバが密に結合した形になってしまうと前述したようなトラウマが蘇ってくる。 

プロトコルの高速化によりある程度の適用範囲は広がるとは思うが、それでも局所的にしか解決できないだろう。 逆の言い方をするとリアルタイムWebを追求していくとNWやプロトコルの高速化でしか対応する術はないように思える。 

一方、既存のWebアーキテクチャというのは、http(s)とかプロトコルの話だけでなく、RESTなどを含むアーキテクチャスタイルの話が中心になる。

これは、permalink(URI)で示すリソースを操作する概念であり、基本的にステートレスである。 あらためて説明するまでもないが、RESTの「リソース」概念は次のようなものである。

・インターネットのすべてのものがリソースである。
・すべてのリソースは、URIによってアドレス可能である。
・リソースに対する操作は、HTTPの基本的なオペレーション、Get, Put, Post, Delete によって定義される

普通のWebサーバは基本的にはスタティックなホームページの配信を行い、動的なWebにしてもサーバで都度生成したものを返しているだけだ。

Webスケールといった言葉があるように、これはスケーラビリティに優れており、また、ネットワーク耐性にも優れている。

RESTは要はそのWebのアーキテクチャースタイルを壊さないようにWebサービスに応用したものである。 

また、キャッシュも重要な技術要素の一つである。これは、「誰か他の人のリソースを利用し、あるいは、他の誰かが、我々のリソースを利用することを認める」ような世界である。

Webスケールを実現できているのは、ブラウザキャッシュ、PROXY、あるいはCDNといったキャッシュ技術があるからといっても過言ではない。

他方で、ワンセグTVがフルセグTVより数秒のディレイがあっても気にする人はいないように、そもそも本当にリアルタイムである必要があるかという話がある。 

別のいい方をすれば、非同期的な更新で、eventuallyであっても十分ではないかということ。 例えば、一般的なWebアプリのユースケースを考えてみると、そこまでオリジナルにこだわる必要はなく、コピーの若干古い情報だとしても実用上問題はないケースも多い。むしろ、完璧に同じタイミングじゃないとまずいケースの方が少ないのではないか? 

これは妥協しろと言っているのに等しく、低レイテンシーを求めるリアルタイムWebとは相容れない考え方かもしれない。 

なので、個人的には、既存WebとリアルタイムWebは別の方向性をとり、リアルタイムWebが既存Webに置き換えられていくこともないだろうと考えている。

全体的な方向性としては、リアルタイムWebはそれはそれで盛り上がりつつも、既存Webのアーキテクチャー上をベースとして今後も進んでいくのではないだろうか。 

データのキャッシュ化を実現するためには


 一つの解として、HTMLなどがWebサーバやブラウザでキャッシュされるのと同様に、データにおいてもキャッシュさせるという考え方がある。 

そもそもデータは動的であり揮発性の高いものであるが、生成する際にバージョニングして静的に扱えるようにすることで、HTMLなどの静的コンテンツと同様、キャッシュを効かせることができればスケーラビリティは高くなる。 

バージョニングとは、例えば、データはレコード単位にURI+リビジョン番号を振ることでユニークであることを保証し、それが一致している限り同じデータとみなすような仕組みである。更新されるとURIは変化しないがリビジョン番号はカウントアップされる。 

正のデータはサーバ上にあって常に誰かによって更新されている。他のシステムはそのコピーを持ち、正のデータが更新されるとEventuallyに更新されることを前提に考える。ここでは遅延を許容することが肝要である。 

また、同じデータが複数のクライアントやサーバに散在している可能性も考慮し、更新する際は何回更新されても同じリビジョンであれば同じ結果になるようにする(べき等性を考慮する) 

ただし、データの整合性についてはシビアに考えなければいけない。これには、更新の際に元データのリビジョン番号が異なれば他者による更新があったということでエラーとするような楽観的排他の仕組み(Snapshot Isolation)で対応できる。

これにより、整合性の問題をWebのアーキテクチャースタイルを壊さないで解決できるようになる。 

去年だったか、permalinkの重要性は薄れて短縮リンクで十分という議論もあったが、整合性まで考えるとやはりpermalinkは重要であるように思う。短縮リンクはpermalinkのaliasという位置づけであろう。

サーバ負荷軽減のためには通信しないこと

 
 注意しなければならないのは、既に同じリビジョンのデータを持っているにもかかわらず、何度もサーバにリクエストを投げてしまうようなことである。これではキャッシュしている意味がない。 

CROSSセッションの議論でも、シングルページアプリケーションのデメリットとして、サーバへの通信回数が増えて負荷が大きくなるという指摘がなされていた。

「long pollなどNativeと同じで不必要な通信が多く発生するリスクがありしばしば作り方が問題になる。 初期化のタイミングがなくなる反面、サーバへの通信回数が増える。 最悪、ステータスコードを見ないでリトライするようなクライアントコードが書かれるリスクも考慮しないといけない」

シングルページだとパフォーマンス管理が難しくなったという意見であるが、そもそもサーバサイドのレンダリングを引きずられていると高速化は難しく、たとえデータバインディングに特化できたとしても、効率よくしないと今度はそれがパフォーマンスに悪影響を与えてしまう。

ステートレス性というか、キャッシュを利用してなるべく通信を発生させないようにする。また、発生しても必要最低限のデータに絞ることが大事である。

究極の形態がオフライン耐性をもったアプリであり、Offline Firstのような概念や方向性をもって作っていけば、おのずとNW効率のよいものが出来上がるのではないかと思う。 

クライアント・サーバ間のデータバインディング

 
 クライアント・サーバ間のデータバインディングは、具体的にはAngularJSに代表されるようなMVVMに加えてサーバとのデータバインド機能を追加するようなイメージとなる(図)


これまでの、クライアントからのリクエストによってサーバ上で動的にコンテンツが作られるスタイルから、基本的にクライアントとサーバが同じデータを参照しており、どちらかが更新したタイミングでお互いに通知されて同期されるような、クライアントとサーバとの双方向バインディングのスタイルに変化していくと思う。(リアクティブプログラミングという)

これからの時代、このようなクライアント・サーバ間のリアクティブプログラミングなどが求められると思うのだが、一歩間違えれば密結合の世界になって、スケールしなくなるのも懸念される。 

同期的な密結合ではスケールしないことは述べてきた通りであり、それは、XML/SOAPでもREST/JSONでも同じことだといえる。
 
実際、WebサービスのAPIは今ではREST/JSONが主流であるが密結合になってしまって残念なものも多く見られる。 

クライアント・サーバ間のデータバインディングでは、図にあるように、XHRでリクエストを受けつけるが、レスポンスについてはWebSocketで非同期更新する。

バージョニングされたデータが非同期更新されるのがポイントであり、WebSocketであっても同期的な密結合に陥ることはないのが特長である。 

あと、バリデーションの問題(サーバとクライアントでどうバリデーションするか)といった新たな課題もあるが、長くなるのでここでは触れない。

通信のデータ形式、それから、BaaS

 
 クライアント・サーバ間のデータバインディングでは、通信のデータ形式はXMLでもJSONでもMessagePackでも何でもいい。 むしろ同じように扱えるべきだと考える。 
データとは本質的には普遍的なものだから。

よく見かける、XML vs JSONとか、SOAP vs RESTなどの議論はたいへん不毛である。
データ表現を変えたところでアーキテクチャーが密結合であれば意味がない。

ちなみに、弊社のReflexWorksは様々なデータ表現に対応している。
 (先日、既存のReflexWorksシステムにおいて、XMLからMessagepack+Deflateに変更したところ処理速度が4倍(1034msから250ms)、データ量が2%(892,799bytesから21,255bytes)になった。なんと98%削減できたことになる。業務ロジックに変更はない。) 

現在、AngularJSのクライアントとReflexWorksサーバ間との非同期データバインディングが可能なBaaS、vte.cx engine(ぶいてっくす えんじん)を開発中である。

ソフトスキーマと項目単位に付与できるACLやグループ機能などのSNS機能もある。 

乞うご期待。 

(補足:http://stream.itmedia.co.jp/enterprise/pdf/pdf_16_1235461490.pdf より)
 

・整合性の問題
マルチ・プロセスからマルチ・マシンへ拡張するとき、我々は、状態を失う。「システム」のグローバルな状態というのは、虚構である。興味深い分散システムには、整合的な状態というものは存在しない。(Lamport:http://research. microsoft.com/users/lamport/pubs/pubs.html)分散OSのプロジェクトは、グローバルな状態を導入しようとしたが、大々的に失敗した。
しかし、障害の局所化(何かまずいことが起きても、システムの部分は生きのこる)を手に入れた。

・信頼性の問題
(インターネットの)信頼できないマルチ・マシンたちに拡張するとき、誰を信ずることが出来るか分からない難しい状況の中で、我々は信頼を失う。しかし、我々はスケール(webスケール、インターネットスケール)」を手に入れた。
 


火曜日, 1月 14, 2014

Web API認証について このエントリーを含むはてなブックマーク

 最近、Web APIの認証をどうすべきか考えている。

 例えば次のようなケースをどうするか。
 「既存のWebサイトがあり、既にユーザIDとパスワードによる認証によって、ブラウザでデータを提供している。 今回、この提供データをブラウザの画面ではなく、REST APIにて取得可能にしたい。 このデータはユーザ毎に取得可能な値が違うので、認証、または認可によって制限をかけたい。」

 ユーザーがブラウザからIDとパスワード(以下ID/PW)を使ってログインする方式を、そのままWeb APIにも適用しても安全なのだろうか。 Web APIの先にはスマホアプリやシェルスクリプトなどから直接ログインするものなどが考えられるが、安全かつシンプルに実装するにはどうしたらいいのだろうか。 

私はセキュリティの専門家ではないので間違った考え方をしている可能性もあるが、誰かの目に留まって助言いただけるかもしれないので、それを期待して今考えていることを率直に述べようと思う。

APIで安全な認証はそもそも作れるか

 
 そもそも認証については原則ブラウザを使わないとダメなんじゃないかと私は考えている。
 その理由は、ブラウザじゃないとサイトの安全性について確認することが困難だから。
ブラウザであれば、利用者はアドレスバーでドメイン名を確認できるし、https通信されていることやベリサインシールも確認できる。その逆にアドレスバーを隠してあるサイトは信用できないしパスワードを盗む偽サイトかもしれない。
 世の中を見渡すと、ほぼ全ての認証の基本になっているのがブラウザからのログインケースであるように思う。

 一方、API認証で、たとえID/PWで認証できるようなAPIを作ったとしても、ブラウザのようにサイトの信頼性を確認する手段がないため、利用者が安心して使えるものを作るのは難しいのではないかと思う。
 例えば、コマンドラインのプロンプトやスマホアプリの画面でID/PWを入力するアプリを作ったとすると、どこで認証するかをユーザは正しく認識する必要があるが、送信先URLに不正がないことなどを確認するのは難しいと思う。(※1)  
 ただ、利用者に毎回ID/PWを入れさせなくてもアクセス制御さえきちんとできれば十分というケースもあるだろう。このようなケースであれば後述するOAuth2.0や認証キーを使う方法などがある。

API認証のパターン


 認証(認可)の方法は、大きく分けて以下の3つがあると思う。 


1.ID/PWを送信するBasic認証やDigest認証(含むWSSE)  
2.(ブラウザの管理画面等で発行する)認証キーを使う方法  
3.OAuth2.0 アクセストークンを使う方法  

1.のBasic認証APIでID/PWをハードコードして使うケースはよくありがちで、このような安易なやり方はセキュリティ上まずいのはわかる。 APIはプログラムから利用されることを前提として作られているため,PWリスト攻撃/辞書攻撃を試みるクラッカーにとって攻撃に役立つ便利な機能になり格好なものになることは容易に想像できる。

 2.の認証キーを使う方法では、まず管理者がログインして画面を開き、そこに表示されている認証キーを使ってAPIからアクセスする。MBaaSでもよく使われており、ID/PWを入力しないため安全な方法といえる。

 3.のOAuth2.0では、アクセストークンをAPIの認証キーとして使う。アクセストークンを入手する際に最低一回は必ず、正規のWebのログイン画面で認証する必要がある。 

DropboxUploader(https://github.com/andreafabrizi/Dropbox-Uploader)はbashでOAuth2.0を使っている。(しかし実際に導入してみるとわかるのだが設定がちょっと面倒くさい) 

OAuth2.0は「認可」である。これを使うということは、つまり、認証はAPIでは行わないことと同意となる。認証キーを使う方法も、Authorizationヘッダに付与するOAuth2.0 Bearerも同様に「認可」の一部であると考えられる。認可はユーザによって正しく認証されていることが前提であり、この方式を使えばパスワードが罠サイトに送信されるような心配もない。 

2.と3.は認証と認可を別々に分けることでリスクを減らすという考え方で、これらを採用できるのであれば特に問題はないし、また冒頭に挙げたケースもこれで解決できるかもしれない。 OAuthのアクセストークンを使えばAPIで認証やる意味はないという意見もアリだと思う。ということで、OAuth2.0などの認可の話はここで終わり

 問題は、1.のような「認証」をAPIでやってよいかどうかということである。  

次にこの認証の論点に絞ってもう少し掘り下げてみる。

APIの認証をやらなくなった現状


 Google Data APIには以前、Client Login方式 (https://developers.google.com/accounts/docs/AuthForInstalledApps)というのがあったが今は廃止されてOAuth2.0に移行している。 

かつてのClientLogin方式はID/PW認証ができてとても便利だったのを覚えている。しかし、しばしばCAPTCHAロックが発生するため、アプリでID/PW固定で使うのは困難であった。結局、Googleは不正ログイン対策のためID/PW方式を諦めざるを得なかったのだと思う。(廃止理由:http://adwords-ja.blogspot.jp/2013/09/adwords-api-clientlogin.html) 

GoogleのClientLoginの件もしかり、昔流行ったWSSEにしても、 https://www.google.co.jp/search?q=WSSE で検索してもわかるように、多くのサービスがOAuth2.0に移行している現状がある。これは、リピート攻撃に弱くて筋が悪いと言われているWSSEを廃止したいことや、Webの認証機能に一元化したいという背景があるように思う。また、前述したような、ユーザがどこで認証するかを正しく認識する必要があることや、認証と認可を分けて考えればそもそもAPIに認証が不要だったというケースも多いのかもしれない。

でも、よく考えてみると、WebのログインフォームからはID/PWが送信されているわけで、API認証を廃止したところでログインフォームの動作をAPIでエミュレートすることが可能である。むしろそれを禁止する方が難しく、例えば、ログインページをスクレイピングしてtokenを切り取るなどすれば大概はエミュレートできる。(ただ、ワンタイムトークンなど発行することで難しくすることはできる) 

ブラウザでできることをプログラムから実行するのがなぜまずいのか。 今のところ、ログインフォームのようにID/PWをうまく保存する方法がないとか、不正ログイン対策のためのCAPTCHAをプログラムでハンドルするのが難しい、ぐらいしか説明が見つからない。

ログインフォームなどのセキュリティ強化策


 ID/PWを送信するAPI認証がセキュリティ上まずいとはいったものの、そもそもログインフォームのエミュレートを防げないのであればAPI認証の非公開にかかわらず意味がないし、結局はログインフォーム自体のセキュリティを強化する必要があるように思う。 

例えば、生PWを送信するリスクをどう考えるか。 ブラウザのログインフォームにしてもAPIにしても認証を行う際には必ずID/PWを必要とするが、生のPWを送信するときには細心の注意が必要である。(かつて、URLパラメータにPWを含めてしまうとRefererに機能によってリンク先に送出されることがあったように) 
またSSLで暗号化していてもログに書かれてしまうことで漏洩の危険性が高まるため可能であれば生で送信はやめた方がよいと思う。 

そこで、Basic認証は使わずにDigest認証やWSSEのようにパスワードをハッシュ化(※2)することでパスワードそのものの推定を難しくすることを考えてみる。これで万一漏洩した際にも同じパスワード使ってるサービスへの影響が少なくなるというメリットが生じる。

 個人的には、(今さらかよ!と思うかもしれんが)WSSEをハッシュ化して送信する方法で十分だと考えている。ただし、サーバではリピート攻撃に耐えうるようにWSSEをワンタイム化する。 そのまま保存してしまうとハッシュ化パスが事実上のパスワードになってしまうため、PWをさらにハッシュ化(または暗号化)したものを保存する。 生パスワードを知らないので漏れることも悪用(生パスワードを保存しておいて、他のシステムの認証にも使ってしまうようなこと)される心配もないためユーザに対して安心感は増すと考えられる。 また、初回だけWSSEを送って認証に成功したらサービス側で有効なトークン(セッションID)を発行してクライアントは以後毎回それを送るというブラウザでのセッションクッキーと同等の仕組みにすると毎回パスワードを入力しなくてもよくなるのでユーザビリティが増す。

これを実装するのに、Javascriptでパスワードをハッシュ化してXHRで送信することになると思うが、CSRF対策も考える必要がある。 
http://matome.naver.jp/odai/2133794394551702001 
http://utf-8.jp/public/20131114/owasp.pptx (P24-25に対策が載っている) 

不正ログイン対策まで考えるのであれば、CAPTCHAの他にリスクベース認証や二要素認証などの防御対策の他、不正アクセスがないかチェックするためのログイン履歴機能なども必要になってくるだろう。つまり、API認証だけのセキュリティを考えるのではなく、ログインフォームを含む認証全体の機能についてよく考えないといけないという話になる。

APIKey+WSSE


 最後に、どうしてもAPIで認証したいという場合の簡易的な方法について思いついたことを紹介する。 

これは正規のIdpからお墨付き(APIKey)をもらうことでログイン認証できる権限を得る方法である。 
具体的には、WSSEのこれまでのPasswordDigest生成ロジックを改良したもので、管理画面から入手したapikeyを先頭にくっつけてsha256にしてbase64エンコードするだけ。(注:元のWSSEはsha1だった。WSSEはワンタイムである必要がある) 
 PasswordDigest = base64(sha256(APIKey.base64_decode(Nonce).Created.Password)) 
 ID/PW以外にsecretのAPIkeyを付けることで2要素認証的な意味でセキュリティを強化できる。 また、アプリごとにトークンを使い分けることや、(不正ログインなどの)問題が発生した場合にキーを無効にできたり、より柔軟な運用が可能になる。複数回ログイン失敗で検知できる仕組みは必要だが、CAPTCHAなどのロックの仕組みは必要ないだろう。

 ただ、このようなオレオレ認証にはリスクが伴う。 
snapchatの事件(http://www.thread-safe.com/2014/01/46m-usernames-phone-numbers-leaked-by.html) も起きたばかりなので慎重に考えていきたいところである。


(2016/1/6 追記) BaaS vte.cxでは結局、こんな感じで整理した。=> セキュリティについて

(※1) もしクローズドなHTTPクライアントだとしたらどうやってSSLチェックしてホスト証明したと示せるのかが問題になる。まずはクライアント自体が信頼できるものであることを証明できないといけない。詳しくはこちらの資料(http://www.slideshare.net/shunsuketaniguchi520/ssl-28267938)のP7-を参照。適切なサーバ証明書の設定により接続先のサーバの正当性は検証できるが、スマホアプリにはアドレスバーがないものが多く、利用者が能動的に接続先や接続プロトコル(https)を確かめるのが困難である。

(※2) Saltが無いハッシュでは生パスワードが推測可能になるという危険があります。sha256(APIkey+nonce+日時+パスワード)のnonceがソルトに相当します。さらにDBに保存するときにストレッティングして保存、または暗号化します。

(追記)
@ritouさんからご意見いただいた。感謝です。



ごもっとも。
ちょっと、Resource Owner Password Credentialsについて調べてみます。

https://github.com/applicake/doorkeeper/wiki/Using-Resource-Owner-Password-Credentials-flow によれば、

以下のリクエストをOAuth Providerに投げると、

{
  "grant_type"    : "password",
  "username"      : "user@example.com",
  "password"      : "sekret",
  "client_id"     : "the_client_id",
  "client_secret" : "the_client_secret"
}

以下のように、access_tokenが返ってくるので、

{
  "access_token": "1f0af717251950dbd4d73154fdf0a474a5c5119adad999683f5b450c460726aa",
  "token_type": "bearer",
  "expires_in": 7200
}

あとは、Authorizationヘッダに"Bearer " + access_token
をAPI実行時のヘッダに付ければいけます。

PS. しかし、これはPWが生で送られている。パスワードを送信してたらログに書くとかろくでもないことをする人が出てくるという経験則がある。
オレオレ認証作ったやつには税金かけろみたいな話( http://togetter.com/li/617280 )が聞こえてきて(;゜Д゜)ガクブル だしなあ。困った。

 
© 2006-2015 Virtual Technology
当サイトではGoogle Analyticsを使ってウェブサイトのトラフィック情報を収集しています。詳しくは、プライバシーポリシーを参照してください。