Clean Architecture を読んだ感想

Clean Architecture を読んだ感想というかまとめというか、備忘も兼ねて書きたいことをテキトーにメモします。

Clean Architecture 達人に学ぶソフトウェアの構造と設計

Clean Architecture 達人に学ぶソフトウェアの構造と設計

全体的に、Clean Architecture という設計手法の具体的な How to が書かれているという感じではなく、ソフトウェアを設計する際に大事にするべき考え方の部分を軸に書かれているなあという印象でした。ソフトウェア設計の原則や考え方がざーっと序盤に書かれていて、それらを大事にするとこういうアーキテクチャがいいよね、という流れで Clean Architecture という設計が登場する流れです。ソフトウェア開発って、手法の優劣というか、主張の妥当性を示す決定的な根拠を出すのは難しいと思うのですが、その中でも、アーキテクチャが大事なんだという筆者の主張はそれなりに納得感のある形で伝わってきた気がします。

そんな感じで、優れた設計方法を知るというよりは、優れた設計の背景にある考え方を知るという感覚で読めました。あまりプログラミングや設計に慣れていない自分のような人間にもわかりやすく書かれていて、汎用的な知識として身についたものが結構あったと思います。オブジェクト指向や設計に深い理解がある人にとっては情報量が少ないかもしれないなーという気がしなくもないですが。

設計を題材にした本なので、優れた設計とはこういうものだという話を色々しているのですが、まとめると「変更に強い設計」を優れた設計と考えていいのではないかと思いました。逆に変更に対しての弱さはどこから生まれてくるのかと考えると、おそらく多くの場合で不適切な依存関係からではないかと思います。すなわち、システムやプログラムが疎結合な状態や依存関係がキレイな状態は、変更に強い状態と言えるのではないでしょうか。

で、プログラムをそのような状態にするために、オブジェクト指向プログラミングではポリモーフィズムが活躍するという話が沢山書いてあります。ポリモーフィズムを活用することで詳細な実装を隠し、アダプティブというかプラガブルな設計を作り出すことができるからです。依存関係逆転の原則によれば、ポリモーフィズムを使えば依存関係を自由自在にコントロールできるとも言えます。筆者はこれをオブジェクト指向の最たる強みであるとしています。変更への弱さが不適切な依存関係から生まれるのだとすれば、依存関係をコントロールできれば変更への強さを意図的に作れるからです。

オブジェクト指向についてまだまだ理解が浅い自分にとっては、その辺の考え方も整理された感があります。有名なソフトウェア設計の原則に、この依存関係逆転の原則や、オープンクローズドの原則といったものがありますが、改めてこれらにオブジェクト指向のエッセンスが垣間見えた気がします。「オブジェクト指向プログラミングとは何か」と聞かれたら「抽象でプログラミングする手法」とテキトーに答えていましたが、その点では筆者と考え方自体は近い気がしたので少し安心した感もありました。ちなみにオブジェクト指向以前のパラダイムの言語でも、一応ポリモーフィズム的なことは実現できたらしいのですが、関数ポインタを使うため危険性が高く、実用的ではなかったとのことです。

ただ、抽象化にはコストが伴うということは多くの人が経験的に知っていると思います。過激に抽象化して変更に強い状態を作るのは手間のかかる作業だし、逆に構造を把握しづらくなったりもします(YAGNI なんて言葉が生まれるくらいですから)。設計なんて考えずに、動けばいいやで書いたほうがサクッと作れたりするものです。しかしこの方法では機能変更、追加時に高く付きます。すなわち、将来のために投資するか否かという選択がここにあるわけです。投資すべきかどうかを絶対的に評価/判断する方法があるわけでもなさそうなので、この辺りはアーキテクトの腕の見せどころといったところなのでしょう。適切な意思決定には経験値はもちろん、業務知識なんかも必要になりそうです。とまあそんな感じで、YAGNI や DRY なんかと相反するように見える「優れた設計」についても筆者の意見が書かれていて、なるほどなーという感じでした。

あと個人的に面白いなと思ったのは、デキるアーキテクトは詳細(使うDBとかの具体的な話)の決定をできるだけ遅らせるようにデザインする的な話で、あまりできていなかったことだなーと。決定を遅らせるのが大事というよりは、決定を遅らせても大丈夫なような状態にしておくのが大事ということでしょう。Web サービスを作るときなんかは、開発初期段階でサーバや DB のことをかなり意識してしまっていたので、詳細よりもサービスの本当の価値となるビジネスロジック部分の方にもっとフォーカスしていくべきだと思いました。中核にあるビジネスロジックを捉えることは DDD に限らず大事な気がしますし。Web サービスを作る際に大前提として考えてしまいがちな 「Web で配信する」ということすら意識する必要がないという主張にはハッとした感がありました。

Clean Architecture を導入したプロジェクトはどのようなディレクトリ構成をとるべきなのかとか、ユースケースが実際にはどのように表現されるのかとか、Clean Architecture の具体的な部分はまだ理解していないことだらけなので、テスト的に実践してみようかなーと思います。ただ、こういうシッカリしたアーキテクチャってそれなりの規模のプロジェクトじゃないと有り難さがわからなそうな感はありますが...。

総括として、個人的な本書の位置づけは、ある程度オブジェクト指向を理解できてきたら次のステップとして読みたい本、という感じでした。同列のステップとしてデザインパターンの本などもありだと思いますが、あっちはなんというか具体例の裏にあるエッセンスを自分で掴んでいかないと、汎用的な知識としては得られるものが少ない気がしたので。本書は設計の「考え方」の部分が大きなウェイトを占めていたので、Clean Architecture というアーキテクチャ自体に興味がなくてもなにかしら気づきがありそうな印象を受けました。おわり。

DNS初心者が独自ドメインの設定する時に知っておきたいこと、ざっくりとした設定方法

ホスティングサービスで借りたサーバを独自ドメインで運用したいケースが結構あるのですが、設定するたびに DNS の仕組みを忘れてしまいます。仕組みを忘れるというよりは、自分が今何を設定しているのか、何を設定しなければいけないのかが分からなくなる感じですね。最近またホスティングで借りたサーバに独自ドメイン設定をすることがあったので、設定の際に頭に入れておいたほうが良さそうなことをメモっておきます。

なお、DNS プロトコルの詳細や、実際の具体的な設定方法については書きません。どういう流れで何を設定すればいいのか、それぞれの設定やアクターがどういう関係になっているのかをなんとなく書きます。便宜上、厳密性を欠いた記述があります、間違った記述があったらさーせん。

登場人物

クライアントのアクセスの流れ

ドメインの設定方法の流れを見る前に、クライアントが Web サイトにアクセスする流れを簡単に抑えておきます。アクセスの流れが把握できれば、どのような設定をすべきかがわかります。例として、クライアントが example.comドメイン名を持つ Web サイトにアクセスする場合について考えます。以下、ざっくりとした図(実際には DNS キャッシュサーバなども存在しますがここでは割愛)。

f:id:norikone:20180821152840p:plain

実際の example.com の IP アドレス(3.3.3.3)を知っているのは、ネームサーバです。このドメイン名と IP アドレスの対応情報は DNS レコードとして管理されます(図の黄緑色の長方形)。で、そのネームサーバが"誰"なのかを知っているのが .com ドメインレジストリが管理するTLD権威サーバです。その情報もレコードとして管理されます。

Web サイトへのアクセス時には、クライアントはまず TLD 権威サーバに example.com の IP アドレスを知っているネームサーバが"誰"なのかを聞き①、dns01.piyo.hoge にアクセスすればいいことを知ります②。次に dns01.piyo.hogeexample.com の在処を聞くと③、3.3.3.3 という IP アドレスを取得できます④。最終的に、この IP アドレスをもとに Web サイトにアクセスします⑤。

すなわち、取得した独自ドメインを Web サイトに反映させるためには、「ネームサーバ」「ネームサーバのレコード」「TLD権威サーバのレコード」の用意・設定が必要になるわけです。これらを設定できれば、取得したドメインを自由に好きな場所(サーバ等)に向けることができます。「TLD 権威サーバのレコード」と「ネームサーバのレコード」の組み合わせで最終的なアクセス先が決まるとも言えるかもしれません(実際には階層構造になっていますが)。

Webサイトのドメイン設定の流れ

上に書いたように、基本的には「ネームサーバ」「ネームサーバのレコード」「TLD権威サーバのレコード」を設定すればOKということになります。

TLD権威サーバのレコード」はレジストラサービスを使って設定すればいいのですが、ネームサーバの用意・設定にはいくつかの方法があります。ネームサーバの準備は基本的には以下のパターンから選ぶことになると思います。

わざわざ自分で立てることはあまり無いと思うので、下の3つについて書きます。

レジストラが用意しているものを使う

f:id:norikone:20180821001239p:plain

レジストラの多くは、DNS ホスティング機能を提供しています。つまり、ネームサーバを貸してくれます。この場合、レジストラサービス(お名前.comなど)内で、借りたネームサーバの DNS レコードを設定します。大体どのレジストラサービスにも "DNSレコードの設定" 的なメニューがあって、そこで例えば以下のようなレコードを設定することになります。

example.com A 3600 3.3.3.3

DNS レコードについての詳細は割愛しますが、example.comIPアドレスの 3.3.3.3 と紐付けるよ、という意味です。なお、設定の際には当然ですが Web サーバの IP アドレス(ここでは 3.3.3.3)を知っている必要があります。なので、予めホスティングサービスの方で、Web サイトをホストしているサーバの IP を確認しておきます。

ホスティングサービスによっては、外部に公開する IP を隠蔽している場合があります。その場合にはホスティングサービスが指定するエンドポイント(ロードバランサ等)の IP を設定することになります。もしくは、example.hogehoge.com のようにホスティングサービス側で Web サイトのドメイン名を用意してくれるケースでは、CNAME や Arias レコードで example.com と example.hogehoge.com を紐付けることもあります。いずれにせよ、レジストラが提供しているネームサーバを使う場合には、レジストラサービス経由でそのネームサーバの DNS レコードを設定します。

で、先に見たように、クライアントが Web サイトにアクセスする時にはネームサーバよりも先に TLD 権威サーバに DNS 問い合わせをします。まず TLD 権威サーバにネームサーバが誰かを聞きに行くわけです。ので、「TLD権威サーバのレコード」も設定しなければなりませんが、こちらもレジストラサービスで設定できます。レジストラサービスの "ネームサーバを変更する" 的なメニューから、ネームサーバを指定します。すなわち、TLD権威サーバに、example.com を管理しているネームサーバを教えてあげるということです。"ネームサーバを変更する" という表現は、"TLD権威サーバのレコードを書き換える" ことを意味していると言えます(この例では)。

したがって、このパターンでは「ネームサーバのレコード」と「TLD権威サーバのレコード」をレジストラサービス経由で設定することになります。

ホスティングサービスが用意しているものを使う

f:id:norikone:20180821001313p:plain

ホスティングサービスが用意してくれるネームサーバを使う場合には、「ネームサーバのレコード」は基本的にはホスティングサービス側で設定してくれるので、Web サイト運営者側で設定しなくてOKです。

ただ、TLD 権威サーバにそのネームサーバを教えてあげる必要はあります。なので、ホスティングサービスが用意したネームサーバのドメイン名(dns01.piyo.hoge)を控えて、前述の方法と同様にレジストラサービスから設定します。例えば、お名前.com でとったドメインを Netlify でホストする Web サイトに反映させる場合には、お名前.com のネームサーバ設定で、Netlify が用意するネームサーバを指定することになります。

外部のDNSホスティングを使う

f:id:norikone:20180821001345p:plain

AWS の Route53 などの外部の DNS ホスティングサービスのネームサーバを使うパターンです。このパターンでは、DNS ホスティングサービス経由でネームサーバを借りて、その DNS レコードを設定します。

TLD権威サーバのレコード」は、前述のパターンと同じようにレジストラサービスを使ってを設定してあげます。つまり、TLD 権威サーバに DNS ホスティングサービスで借りたネームサーバを教えてあげます。

このパターンでは、レジストラサービスと DNS ホスティングサービスの2箇所で DNS の設定が必要なため、やや複雑です。

おわり

独自ドメインの設定をする際に個人的に頭に入れておいたほうが良さそうと思ったことを書きました。Netlify などのホスティングサービスは非常に簡単にサーバを用意できるため重宝していますが、独自ドメインの設定に若干の面倒臭さを感じてしまいます。ホスティングサービス側に設定したい独自ドメイン名を入力したら、OAuth とかを使って自動的にレジストリやネームサーバに必要な設定をしてくれれば楽なのになあと思いますがそれはそれで仕様的に面倒臭いんでしょうかね。おわり。

指定した期間内の特定の曜日をカウントする方法【JavaScript例】

標題の通り.考え方と実装例をメモしておきます. 「5月10日〜7月20日の間の水曜日の数」というような感じで値を得る方法です.

実装

擬似コード

切り捨て((期間の日数+(6+開始曜日-指定曜日)%7)/7);

例えば JavaScript では以下のような感じ.

const numOfDay = 10;     // 期間の日数=>10日
const beginDay = 3;     // 期間の開始日=>水曜日
const targetDay = 5;     // カウントする曜日=>金曜日

result = Math.floor((numOfDay + (6 + beginDay - targetDay) % 7) / 7);     // => 2

考え方

パッと最初に思いつきそうな方法として,期間(開始日から終了日まで)の日数を求めて,それを7で割るというものがあります.しかし,考えてみるとこの方法ではうまくいかないことがわかります.例えば,期間が8日だったとして,7で割ると1余ることになりますが,この余った1日分がカウントしたい特定の曜日なのかどうかを調べなければいけません.一方で,ある方法を採ると,単純に期間を7で割るだけで曜日をカウントできるようになります.その方法の考え方は,以下に書かれている「その1」と「その2」が参考になりました.

Excel一般機能:期間内の指定曜日の数

要は,期間の開始日を意図的にずらすことで,日数を7で割ったときの余りの扱いを簡単にするということです.上の記事のように開始日をずらせば,余った日が指定曜日になることがないため,余りは無視(切り捨て)すれば良くなります(6日前までなら開始日をずらしてもカウントに影響はない).

この方法で重要なのは「開始日の前の週の指定曜日の翌日」に開始日をずらす(戻す)ことです.こうすることで,7で割った商がカウント結果になります.となると,どうやってその開始日までもどすのか,という話になりますが,ここでは開始曜日(期間が始まる曜日)と指定曜日(カウントしたい曜日)が重要になります.

例えば,開始曜日が(火)で指定曜日も(火)だった場合,6日後ろにずらせば,「開始日の前の週の指定曜日の翌日」である前週の(水)に開始日をセットできます.次に指定曜日が開始曜日の1つ先である(水)だったとすると,1日分ずらす曜日が減って,5日後ろにずらすことになります.同じように指定曜日が(木)だと,2日分ずらす曜日が減って,4日ずらします.逆に開始曜日の1つ前である(月)が指定曜日だと,ずらす必要がなくなります.また,(日)が開始曜日だと,1日だけずらすことになります.

これを観察してみると,6日分戻すことをベースとして考えて,開始曜日と指定曜日の差でそこに調整を加えればよさそうです.差が開始曜日から後ろ方向に広がっていればずらす日数をその差分だけ減らし,前方向に広がっていればその差分だけプラスして,余剰をとります.これで,開始日から「何日前にずらせばいいか」を求められます.

で,日数を前にずらすというのは,期間をその分長くしているとも捉えられます.なのであとは,もともとの期間日数に上で求めた値を足してあげて,それを7で割った結果を切り捨てすれば完了です.

ずらす日数 = (6 + 開始曜日 - 指定曜日) % 7;
期間日数 = 期間日数 + ずらす日数;
指定曜日の数 = 切り捨て(期間日数/7);

つまり冒頭で記したように,

切り捨て((期間の日数+(6+開始曜日-指定曜日)%7)/7);

のようになります.

JavaScript で Date をディープコピーする

Date オブジェクトをコピーしたかったときのメモ.

どうやら標準ではオブジェクトのディープコピー用のメソッドは用意されていない模様.JavaScript でディープコピーする方法として JSON.stringify を使うものや jQuery.extend を使うものが紹介されているのを見かけましたが,これらは配列や文字列等を対象としていて,Date オブジェクトでは上手くできませんでした.

Date をコピーするために,以下のように元のオブジェクトのデータをコンストラクタに与えて新たにオブジェクトを生成するのが一番安心で簡単そうです.

let original = new Date();
let copy = new Date(original.getTime());

Date は割と簡単にコピーできましたが,生成が複雑なオブジェクトの場合には自前でコピーメソッドを用意する必要がありそうです.

Web認証におけるToken(JWT)とCookie(セッション)の違い

HTTP の世界はステートレスです.すなわち基本的なプロトコル上では状態が管理されません.なので,ユーザの認証状態などを管理するために,これまでは主に Cookie & セッション ID が使われてきました.一方で,最近ではトークンを使った認証状態の管理もよく使われるようになってきました.この記事では,この辺の参考文献を見ながら,それぞれの特徴(主にトークンの利点)についてメモします.

それぞれの認証方式の概要

Cookie & セッションID による認証

この方式では,ユーザが ID/PW をサーバに送り,その認証結果としてセッション ID が Cookie で返されます.サーバ側では,払い出したセッション ID とそれに紐づくセッション情報を1対1で管理しておきます.ユーザはそれ以降の HTTP リクエストに Cookie(セッションID) を含めることで,サーバに自分が誰であるのか(どのセッション情報に紐付いているユーザか)を教えます.サーバは,送られてきたセッション ID と,裏で管理しているセッション情報を照合して,ユーザの認証状態を判断します.逆に言えば,セッション ID さえ知ることができればそのユーザになりすましてアクセスすることが可能です(セッションハイジャック).この方式はサーバ側で実際にユーザの状態を管理しており,ステートフルと言えます.

トークンによる認証

トークンでの認証は,ユーザは認証結果として Cookie の代わりにトークンを受け取ります.サーバはトークンを払い出しますが,セッション ID を使う方式と異なり,ユーザの状態に関する情報は何も管理しません.ユーザは HTTP リスエストにトークンを含めることで,サーバに自分が誰であるのかを示します.サーバは送られてきたトークンを検証し,そのユーザを認証します.こちらも,攻撃者にトークンを盗聴されてしまうと成りすまされる可能性があります.この仕組みではサーバは状態管理する必要がないので,ステートレスと言えます(クライアントに状態を保存しているとの見方もできなくはないですが).ちなみに,ここでいうトークンは事実上のスタンダードとも言える JWT(JSON Web Token) を想定しています.

トークン(JWT)の特徴

ステートレス

Cookie & セッション ID を使った認証管理では,サーバ側でユーザの認証状態を管理する必要があります.こうなると,ロードバランサを使うような分散環境だと,複数のサーバ間でセッション情報を同期したり,ユーザと接続先サーバの組み合わせを固定するようにリクエストを振り分けたりしなければならないわけです.この辺の話は過去に書いた以下の記事で触れています.

norikone.hatenablog.com

一方,トークンを使った認証では基本的にサーバ側でユーザの認証状態を管理しません.サーバ側でやることは,送られてきたトークンを検証するだけです.なので,上記のような,サーバ側で認証状態をどうやって扱うかという問題を考えなくてよくなります.つまり,ユーザ状態とサーバが密に結合することがあるセッション方式と違って並列分散が容易で,スケーラブルであると言えます.まあ最近では,KVS やフレームワークを使えば水平分散環境でも割と簡単に状態を管理できるようになってきてはいると思いますので,そこまでの優位性があるかはわかりません.ただ,そういった状態管理のための機能がそもそも不要という点で,コストを低減できるかもしれません.

クロスドメイン

Cookie は有効範囲がドメイン(サイト)単位で設定されます.恐らく無関係のサイトに滅多矢鱈に Cookie の情報を渡してしまわないためでしょう.なので,あるドメインでもらった Cookie の情報は,他のドメインでは使えません.

しかし,第三者への Web API によるサービス提供が盛んになっている現在,このような制限は円滑なサービス提供の足枷になります.セキュリティが十分に確保されていれば,1つの認証結果を複数の第三者サービスで使い回すことが効率的ですね.

CORS を有効にしたトークン方式では,Cookie 方式のような有効範囲の制限がありません.トークン認証を許可しているサービスであればどこでもトークン1つで利用できる世界です(SSO 的).また,アプリケーション側で状態を持つ必要がなく,サービス提供のための状態情報をトークン内で管理できることも,そのようなサービス間連携における利点となります.

データストア

Cookie & セッション ID による方式では,Cookie にセッション ID という単一の文字列を管理するだけに留まります.トークン(JWT)は,それ自体に様々なメタデータJSON で格納することが可能であり,表現力に優れています.

例えば,ユーザの権限情報なんかをトークン内に含めたりできるみたいです.使い方はそれぞれのアプリケーション次第といったところでしょう.

パフォーマンス

リクエストのたびにユーザから送られてきたセッション ID で認証するというのは,毎回それをキーとしてデータベースの検索処理が走ることを意味します(リクエストと裏で管理している情報のマッピングをとらなければいけないので).

トークン方式ではサーバがトークンを検証するだけなので,データベースを検索する処理は不要です.このため,一般的にはトークン方式のほうがパフォーマンスが高いみたいです.実際に評価したわけではないので何とも言えませんが,確かに検証処理の方がすぐに終わりそう.

また,上でトークンには任意のデータを格納できると書きましたが,例えばユーザのロール情報(管理者 or 一般)などを入れておけば,サーバ側でデータベースを検索してユーザのロールを判定するような処理をカットできたりもします.このような使い方もできるという点で,トークンがパフォーマンスで優位であると言えるかもしれません.

ただ,トークンに格納するデータが過剰に大きくなると,データ転送がボトルネックになって逆にパフォーマンスが落ちるということも考えられなくはなさそうです.

モバイル対応

Web API を使うのはブラウザだけではありません.ネイティブアプリや IoT デバイスなんかもガンガン API を使います.で,こういったネイティブ環境で Cookie を扱うのは割と面倒臭く,考えなければならないことや制限が多いみたいです.IoT デバイスなんかは Cookie を管理する機能がそもそも備わっていなかったりするかもしれません.

そこでトークンを使えば,このような Cookie を使うのが難しいような場合でも,比較的容易に実装できます.このような理由から,ブラウザ以外のクライアントでは多くの場合でトークンを使うのが簡単かもしれません.

ビッグサイズ

JWT はサイズが大きいという欠点があるみたいです(そこまで気にすることでもなさそうという印象ですが).クライアント側でトークンを管理する場所としては LocalStorage か Cookie を選択できますが,Cookie では 4kb がデータサイズの上限なので,トークンに沢山の情報を埋め込もうとすると問題になるかもしれません.

XSRFXSS 耐性

XSRFXSS の詳細は割愛しますが,XSRF が発生する原因に,Web アクセス時に自動的にアクセス先サイトに紐付いた Cookie を送ってしまうというものがあります.攻撃者はこれを利用し,間接的に Web アクセスを発生させてアクセス先で認証を得るわけです.

トークンは基本的に LocalStorage か Cookie で保管されますが,LocalStorage で保存することで勝手にアクセス先に認証情報を送ることはなくなるので(Cookie ではないので),XSRF を受けなくなります.トークンを Cookie に保存した場合には,同様の問題が発生します.

一方,LocalStorage にトークンを保存すると,今度は XSS を受けやすくなります.LocalStorage は JavaScript から容易にアクセスできるためです.逆に,トークンを Cookie で扱えば httpOnly 属性により XSS 対策ができます. つまり,トークンを管理する方法に応じて意識すべきセキュリティ対策が変わってきそうです.

おわり

ということで,主にトークンを使った認証方式についてざっくりとまとめてみました.トークンと言うと OAuth や OpenID Connect を連想しますが,それらについては以下の記事で概要をまとめています.おわり.

OAuth2やOpenID Connectとは何か、なんとなくわかった気になるための概要 - 備忘録の裏のチラシ

OAuth2やOpenID Connectとは何か、なんとなくわかった気になるための概要メモ

今更ですが備忘のため,OAuth とか OpenID とかその辺の概要についてまとめたメモ.具体的な仕様については書かないけど RFC や公式サイトに載っています.便宜上,厳密性を疎かにしている点あり.

OAuthについて

背景

現在,Twitter や Yahoo など様々なサービスが,Web API として機能を提供しています.API エコノミーなんていう言葉がありますが,Web API としてサービスを提供していくこの動きは今後も続きそうです.

これに伴って,API をラップしてサービスを提供するサードパーティ製のアプリケーションが続々と出てきています.それをここでは「クライアント」と呼びますが,サービスを使いたいユーザはこのクライアントを通して API を利用し,何かしらの価値を享受するわけです.この方式が現在の Web では普及しています.

例えばユーザが Twitter クライアントを使うと,クライアントは TwitterAPI を叩き,タイムラインを取得して結果をユーザに表示したりします.基本的にはこのように「ユーザ ↔ クライアント ↔ API」という関係になります.クライアントは API サービスのフロントエンドと言えるかもしれません.

この際,クライアントからリクエストを受けた API 提供側は,どのユーザからのリクエストなのかを判別する必要があります.API 提供側が,自分を使おうとしているクライアントの向こうにいるユーザが誰なのかを検証できなければ,ユーザが他人になりすまして他人の情報を知ったり,他人のアカウントで活動することが可能になってしまいますから.Twitter で言えば,他人になりすましてダイレクトメッセージを取得する API を叩ければ,盗聴できてしまいます.これは,他人のアカウントでWeb サービスにログイン出来てしまうようなものです.

つまり,普通(?)の Web サービスを利用する時と同じように,API の利用/提供においても,ログインのような認証/認可の仕組みが必要になるわけです.が,この認証/認可について,一般的な Web サービスと同様に考えることはできません.これは,「クライアント(第三者)の存在の有無」という違いがあるためです.

このため,API の世界では,クライアントから API 提供側に対して「ユーザからは許可をもらっているから,ユーザ情報を扱う API を使わせてくれお」というような要求が発生し,そのセキュリティチェックが発生することになります.要は,第三者が存在するため,それを含めた間接的なセキュリティチェックが必要になるのです.では,API 提供側は,あるクライアントがあるユーザの許可をもらったことをどうやって検証すれば良いのでしょうか.

ここで,ユーザがクライアントに ID/PW(API提供側サービスのモノ) を渡しておく方法が容易に思いつきます.クライアントはその情報を使って,ユーザに成り代わるような形で API を叩くということです.API 提供側は送られてきた ID/PW を見て,クライアントの向こう側にいるユーザを特定し,本人にしか渡さない情報を返したりします.

Twitter の例で言うと,ユーザが Twitter の ID/PW を Twitter クライアントにあらかじめ渡しておけば,クライアントはその情報を使って Twitter にアクセス(ログイン)し,当該ユーザ情報の取得や操作ができるよね,ということです.

しかし,この方式にはいくつか問題があります.例えば,ID/PW をクライアントに渡してしまうと,基本的にはクライアントがそのアカウントの全ての権限を持つことになります.クライアントに「タイムラインの閲覧だけを許可する」といった制限をかけるのが難しくなるということです.悪意のあるクライアントを想定した場合にはより深刻な問題になりますし,クライアントから情報が漏れたときのインパクトが大きなものになります.なので,できればクライアントという第三者にユーザの認証機密情報を渡すことは避けたいところです.

また,ユーザがあるクライアントに 自分の ID/PW を使って API を叩くことを取りやめさせる場合には,ID/PW を変更せざるを得なくなります.この時,同じ ID/PW 情報を使っている他のクライアントにもこの変更の影響が出るため,アクセス許可の取り消しのような変更を個々のクライアントに適用することが難しくなります.

で,このように第三者が存在する API の世界で,イイ感じにセキュリティチェックをやろうというのが OAuth です.

概要

上に書いたように,ID/PW を単純にクライアントに譲渡する方法では,様々な問題が起こり得ます.そこで OAuth では,ID/PW をクライアントに教えずに済む方法が採用されています.ID/PW 以外の何かで API アクセスの認可をするということです.

ちなみに「認可」というのはアクセス等の許可をすることで,「認証」とは別です.「認証」は簡単に言えば本人を確認することで,例えば,ID/PW での認証や指紋認証があります.この辺は割愛.考え方は次の記事になんとなく書いてあります.認証と認可の違い - ITmedia エンタープライズ

で,OAuth ではトークンというものを使って API アクセスを認可します.トークンは,API を使うための鍵のようなものです(実体はタダの文字列).ユーザがクライアントに自分の情報へのアクセスを許可した証として発行されるイメージです.

具体的には,クライアントはこのトークンを API リクエストと一緒に送ることで,API の使用を認可されます.API 提供側は送られてきたトークンを検証することで API へのアクセスを許可します.すなわち,クライアントは「ユーザに許可を得た証としてトークンを持っているから,ユーザ情報を扱う API を使わせてくれお」という感じで API 提供側にリクエストを送ります.

OAuth ではそんな感じでトークンを使って認可をします.となると,じゃあクライアントは一体どうやってトークンを手に入れるんだ,という話になります.トークンは,例えばある特定のアカウントへのアクセス許可証なわけですから,誰にでも渡していいわけではありません.ユーザに紐付いたトークンを渡していいのは,そのアカウントを持ったユーザ本人だけです.ここで「認証」が必要になります.

まず,ユーザがクライアントを使おうとすると,API 提供側からユーザに対して,認証とトークンの発行許可が求められます.クライアント視点だと「ユーザさんよ,私はあなたのトークンが必要です.API提供側に本人証明をして,私にトークンを発行するよう連絡してくだし」という感じです.

API 提供側がユーザからその連絡を受けてユーザが認証を完了すると,クライアントにトークンが発行されます.この「ユーザ ↔ API 提供側」間の認証は基本的にユーザの ID/PW で行われます.言わずもがなですが,ここで認証をするのは他人になりすましてトークンを発行されることを防ぐためです.この点から,トークンを持っている=ユーザ本人から認証およびトークンの使用許可を得た,と判断するような形で,API 提供側はその使用を認可するのです.

ここで,OAuth ではトークンで認可が行われるため,クライアントに ID/PW を教える必要がないということが重要です.クライアントはアクセス許可証としてのトークンを持つだけです.これで上に挙げたいくつかの問題を解決できます.

例えば,このアクセス許可証にはアクセス有効範囲が設定されます.つまり,ID/PW を渡してしまう場合の課題の1つだった,過剰に権限を与えてしまう問題を解決できます.Twitter の例で言えば,ある Twitter クライアントがもつトークンでは,タイムラインの取得はできるけど,ツイートはできない,という状態を作れたりします.

ここではざっくりと概要を書きましたが,トークンの具体的な発行方式にも色々あって,ケースに応じて使い分けられています.以下,それについて説明されている記事です.

OAuth 2.0 全フローの図解と動画 - Qiita

まとめると

クライアント(第三者)が存在する Web API の世界では,それを含めた間接的なセキュリティチェックの仕組みが必要になります.ユーザが ID/PW をクライアントにわたす形でこれを実現しようとすると,色々な問題が発生してしまいます.そこで,「認証」と「認可」を切り離し,トークンを使ってクライアントを「認可」する仕組みを導入します.ユーザと API 提供側との間には「認証」が発生し,その結果としてトークンがクライアントに渡されます.クライアントと API 提供側との間にはトークンによる「認可」が発生します.この際,クライアントはユーザの認証情報に触れずに,認可を得ていることがポイントです.

OpenID Connect について

概要

Web サービスなどを使う際,ユーザは基本的には各サービスごとに ID/PW の組み合わせ=認証情報を用意することになります.が,これは結構問題です.

沢山のサービスを使おうとすると,ユーザ側での認証情報の管理が煩雑になり,不便です.また,そもそも ID/PW での認証は,PW がバレてしまえばいくらでもなりすましできてしまう点で,脆弱でもあります.

このようなことから,ID/PW によらない方式で,且つ1つの認証情報で複数のサービスを使えるようにしたいというモチベーションが生まれます.OpenID Connect は,このような課題にアプローチするために発展してきた技術と言えます.SSO(シングルサインオン)的なイメージでしょうか.実現できれば,ユーザはサービスごとに個別の認証情報を用意して管理する手間から開放されますし,サービス提供側としてはユーザの認証情報を管理するリスクをヘッジできます.

OpenID Connect では,ユーザは「あるサービスで認証された結果」を複数のサービス間で使い回します.例えば,Yahoo! で認証されたことを示す証明書のようなものが存在して,Yahoo! 以外の他のサービスでもその証明書を使ってログイン(認証)することを許します.「Yahoo!で認証されていることを確認できたからそのIDでウチにもログインしていいよ」というような世界です.

実は OpenID Connect でも,OAuth と同様にトークンを使ってこれを実現します.上に書いた証明書の役割を果たすのがトークンです.OpenID に対応しているサービスは,トークンを得ることで他のサービスの ID と連携できます.ユーザ視点では,あるサービスの ID を使って別のサービスにログインできるということです.トークンを発行するのは,Yahoo!はてななどのプロバイダと呼ばれる機関です.

ユーザがとあるプロバイダの OpenID(OpenID Connect でログインするためのID) でそれに対応しているサービスにログインしようとすると,そのプロバイダからユーザに対して,認証およびトークンの発行許可が求められます.サービスの視点だと,「ユーザさん,あたなを認証するためにトークンが必要です.プロバイダに本人証明をして,私にトークンを発行するよう連絡してくだし」という感じです.

トークンを得られたサービス側は,ログインしようとしているユーザが本当にプロバイダから認証されたのかを確認します.OK なら,プロバイダが提供している ID と自社サービス内の ID を紐づけ,ID 連携が完了します.

ちなみに,ユーザがサービスに新規登録する際はだいたいプロフィール情報などを入力するステップがありますが,OpenID Connect ではこのような入力を支援するための仕様も定義されています.サービスはプロバイダ側の ID を参照して基本的なユーザ情報を取得し,予めプロフィール情報などをセットできるようになります.折角 ID を連携するのだから,ユーザ情報の初期化等に連携元の情報を少し使わせてくれ,という感じです.

さて,このトークンの発行から利用の大まか流れについて見ると,実は OAuth と全く同じです.というのも,OpenID Connect は OAuth の仕様をベースにしているのです.では,これらの違いは何なのかについて,なんとなくの位置づけを次に書きます.

OAuth との比較

OAuth と OpenID Connect の大きな違いとして,「OAuthは認可のために使うが,OpenID は認証のために使う」ということがよく言われています.OAuth におけるトークンは,API へのアクセス権です.すなわち,トークンは API 利用の認可に使われます.一方で,OpenID はサービスにアクセスしたきた人を認証するためにトークンを使います.

ここでまず,OAuth のトークンを OpenID のような認証に使うことを考えてみます.両者が似たような仕様なのであれば,OAuth でも認証ができそうです.OAuth でも,トークンを持っている=ユーザ本人から認証およびトークンの発行許可を得た,と判断することができるため,トークンを持っていたら認証,とすればいいのではないかという話です.ちなみにここでいう OAuth での認証というのは,OAuth のトークンをログインのための認証情報として使うという意味です.

さて,OAuth の文脈でのトークンというのは,API の利用権です.これはしばしば鍵に例えられます.鍵を持っているからアクセスロックを解錠して API を使えるという感じです.OAuth で認証をするというのは,鍵を持っていればその鍵の所有者として認識する,というやり方です.これはセキュリティ的にまずいです.

なぜなら,OAuth では,鍵とその所有者のマッピング,すなわちトークンとユーザの紐づけについては考慮されていないのです.言い換えれば,OAuth でのクライアントは,その鍵を使おうとしている人が誰なのかは考えないわけです.なので,安易に OAuth で認証しようとすれば,自分の鍵だと言い張って他人の鍵を渡したのにも関わらず,その他人として認証してしまうことが考えられます.

しかし,これは OAuth の欠陥というわけではありません.OAuth はあくまでも認可のための仕組みなので,このような認証については対象外なわけです.そこで,OAuth とは別に,その仕組みをベースに強度の高い認証を可能にした仕様が OpenID Connect なのです.

OpenID Connect ではトークンが大きく拡張されており,トークン自体に認証のための情報が付加されています.これにより,上に書いた OAuth での認証で発生していたような問題を解決しています.すなわち,OpenID Connect の文脈でのトークンというのは,非 ID/PW 形式の認証情報なのです.これは,OAuth でのトークンと役割が異なります.

OAuth でのトークンはタダの単一のキー文字列ですが,OpenID Connect のトークンは JWT と呼ばれる形式の構造化データです.トークンのデータ構造を拡張することで,認証の仕組みを実現しているわけです.OAuth と OpenID Connect ではトークンの発行や利用の大きな流れは一緒でしたが,主にはこのようにトークンの利用目的や実態が異なっているのです.

まとめると

利便性向上を主な目的として,1つの ID を複数サービスで使い回せる仕組みとして発展してきたのが OpenID Connect です(SSO を実現する他の手段として SAML 等もありますがここでは割愛).OpenID Connect は OAuth をベースにした仕組みであるため,トークンの発行や利用の大きな流れは非常に似ています.しかし,「認証」か「認可」という枠組みで,実現したい目的や思想が異なります.OpenID Connect では,主にトークンのデータ構造を拡張することでトークンによる「認証」を実現しています.


仕様書とか参考になりそうなもの

一番分かりやすい OAuth の説明 - Qiita

一番分かりやすい OpenID Connect の説明 - Qiita

RFC 6749 - The OAuth 2.0 Authorization Framework

単なる OAuth 2.0 を認証に使うと、車が通れるほどのどでかいセキュリティー・ホールができる | @_Nat Zone

Final: OpenID Connect Core 1.0 incorporating errata set 1

【クラスローダ】JVMが読み込むクラスを見つける仕組み【パッケージ】

NoClassDefFoundError や ClassNotFoundException に遭遇した時など,Java で書くならある程度クラスローディングについて知っておいたほうがいいかなあと思い調べたときのメモ.なんとなくの概要.

クラスローダ

基本的な仕組み

Java アプリケーションは基本的に,複数のファイルに分かれた沢山のクラスによって成り立っています.アプリケーションを動かすためには,これらのクラスを JVM が読み込まなければなりません.Java では JVM のクラスローダと呼ばれる機構がこの処理を担います.クラスローダは,読み込むべきクラスを判断し,ストレージから当該クラスファイルを探し出し,ファイルを開いてクラスをロードします.

クラスロードの戦略は,オンデマンドな感じになっています.アプリケーションの開始時に必要な全てのクラスを読み込むのではなく,実行中に必要になったタイミングで必要になったクラスのロード(検索)をします.

クラスローダは担当検索範囲ごとに複数存在していて,それらが階層構造(親子関係)をとっています.それぞれのクラスローダがやることは同じなのですが,クラスを検索する場所がそれぞれ異なります.あるクラスローダはどこどこのパスからクラスをロードするのを担当して,あるクラスローダは別のパスからロードするのを担当するという感じです.

プログラム実行中に実際にロードを担当する(起点となる)クラスローダは,実行中のクラスをロードしたクラスローダです.ですが,基本的には,親の担当範囲で見つけられるクラスなら親にロードしてもらう,親がロードできなければ子がロードするという風に,上位のクラスローダから順に特定のクラスのロードを試みていきます.この仕組みは委譲(delegate)と呼ばれます.

また,クラスローダは基本的に子にはロードを委譲しません.これは結構重要なポイントです.例えば,クラスローダAの検索範囲にクラスaがあり,その子クラスローダであるクラスローダBの検索範囲にクラスbがあるとします。この場合,クラスaを実行中にクラスのロードの起点になるのはクラスローダAです.すなわち,クラスa実行中には,クラスローダBがロードを担当する範囲にあるクラスbをロード出来ないということを意味します.起点となったクラスローダより階層的に下位に存在するクラスローダは使われないのです。(が,このやり方が問題になるケースも存在するということで,委譲の順番をあえて変更しているサーバ等も一応存在する模様).

このように,ロードに優先順位というか,方向性をもたせることで,複数のクラスローダが同じクラスを重複してロードしたりして競合が発生することを防げます.このような仕組みになっているので,とあるクラスが,起点となるクラスローダの下位クラスローダ配下にしか存在しないクラスに依存する,という設計はよろしくないでしょう.

また,クラスローダは階層構造の横方向にも委譲しません.すなわち,兄弟関係にあるクラスローダ同士はお互いに影響し合わない仕組みになっています.こうすることで,単一のサーバにデプロイされる複数のアプリケーションごとに兄弟関係になるクラスローダを作ることで,それぞれのアプリケーションでバージョンが違うクラスを読み込んだりできます.

基本となるクラスローダ

基本的なクラスローダは,ブートストラップクラスローダ,拡張クラスローダ,システムクラスローダの3つです.これらは挙げた順に親子関係になっています.

ブートストラップクラスローダは JVM そのものの実行に必要になるクラスをロードする基礎的なクラスローダです.JDK のコア API に関するクラスなどがこのクラスローダにロードされます(java.lang.* など.<JAVA_HOME>/lib にあるような jar とかですね).このクラスローダは基本的には JVM 内にネイティブ実装されています.そのおかげで,そもそも最上位のクラスローダは誰がロードするのか問題(パラドックス)を解決できています.

拡張クラスローダは,拡張用ディレクトリ(一般的には <JAVA_HOME>/lib/ext)にあるクラスをロードします.なので,クラスパスを変更することなく基礎的な機能を Java に追加したい場合には,このディレクトリに追加してあげれば簡単に実現できます.

システムクラスローダは,クラスパス(環境変数)で指定している場所のクラスのロードを担当します.普段プログラムを動かすためにクラスパスを追加することがあると思いますが,そのパスのクラスはこいつが読み込んでくれています.また,このシステムクラスローダを親として独自のクラスローダを作成することも可能です(ユーザ定義クラスローダ).ユーザ定義クラスローダを作ることで,アプリケーション単位でクラスローダを割り当て,ライブラリのロード方法を変えたりできます.

ブートストラップクラスローダはネイティブ実装されているのですが,その他のクラスローダは Java オブジェクトとして存在するので,プログラム中でそのインスタンスを取得したりなんてことも簡単にできます.

クラスローディングの流れの大枠

まず,JVM がプログラムを実行中にクラスのロードが必要だと判断すると,クラスローダに処理を依頼します.クラスローダはロードすべきクラスの完全修飾名を基に処理を始めます.完全修飾名とは,com.example.Hoge のようにクラス名とパッケージ名を繋げたものです.クラスローダは自分がどの場所からクラスを探せばよいかは知っていますが,その場所のどのクラスをロードすればよいかは知らないので,この完全修飾名を与えてロードすべきクラスを決定します.

例えば,/usr/local/lib を探すクラスローダがあったとして,JVM がそれに com.example.Hoge クラスをロードしてもらう指示を出した場合,完全修飾名がパスに変換され /usr/local/lib/com/example/Hoge.class をロードしようとします.この際,クラスが見つからない場合には ClassNotFound の例外が投げられることになります.

言い換えると,読み込まれるクラスは,パッケージ名とクラス名,そしてクラスローダの3つによって決定されるということです.なので,完全修飾名が全く同じクラスが別の場所に複数存在していた場合,処理をするクラスローダによって読み込まれるクラスが変わります.また,同名の完全修飾名をもったクラスが複数のクラスローダから検索され得る場合には,一番最初に発見されたクラスがロードされる事になっています.

クラスが無事発見されると,そのクラスが正しい構造になっているかどうかを検証したり,static フィールドを初期化したり,static イニシャライザを実行したりして,クラスを使える状態にします.

おわり

このように Java ではクラスローダという機構がクラスロードを担当しています.他の言語でのクラスロードの仕組みがどうなっているのか少し気になったので,また暇な時に調べて記事にしようかなーと思います.おわり.

参考

https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-5.html#jvms-5.3
https://www.ibm.com/developerworks/jp/websphere/library/java/j2ee_classloader/1.html