Chapter 7
セキュリティと認証・アクセス権

この章が目指す最も重要な目標は、作成するアプリケーションのセキュリティを確保するということです。もちろん、手軽にできるのであれば長々と説明する必要はありません。しかしながら、INTER-Mediator自身のさまざまな設定に加えて、INTER-Mediator外のデータベースやサーバーOSといった部分にも注意が必要です。この章では、全体的なセキュリティ設定に加えて、データベースエンジンで確保すべき動作と、INTER-Mediatorのソリューションに対する設定などを説明します。

7-1Webアプリケーションセキュリティの前提

INTER-Mediatorによるシステムを作成した場合の、サーバーやクライアントにおいて、何を前提としてセキュリティ設計するべきかをまず最初にまとめておきます。

INTER-Mediatorを稼働するサーバーの前提条件

 INTER-Mediatorで作成したソリューションを稼働するには、原則として何らかのサーバーが必要です。すでにこれまでに紹介している通り、VMでの稼働も可能ですが、サーバー機での運用、クラウドサービス上のインスタンス、VPS、レンタルサーバーなど、さまざまな形態で稼働させることができます。ここでのセキュリティ上の原則は、「管理者以外はログインできない」そして「サーバーは健全に稼働している」ということです。

 管理者であれば、ログインできて、ファイルの内容を参照したり、ファイルを書き換えたりができますが、そうでないユーザーはログインができないということが大原則になります。この原則が守られていて、サーバーが意図した通りに稼働していれば、PHPのファイル、つまり定義ファイルや、あるいは別の設定ファイルなどにデータベース利用のための「パスワードを記述する」ということをしても、パスワード自体は外部に漏れません。

 また、よくあるトラブルで、PHPが稼働していないときにWebブラウザーから.phpファイルを開くと、PHPのプログラムが丸見えになるようなことがあります。この時、もちろん、プログラム自体に何かのパスワードが書かれていれば、見えてしまい、パスワードの漏洩が発生します。しかしながら、これは意図した動作ではありません。そうならないように稼働させるのが管理者の役割であり、あるいはプロバイダの役割でもあります。「Webサーバーは生きているのにPHPだけ落ちる可能性がある」という指摘もあるかもしれませんが、問題がある状況を前提にするのは、かなり信頼性が疑われるシステムであり、このようなシステムは利用するべきではありません。通常の「稼働していることが前提」のシステムは、問題があればWebサーバーを停止させるのが原則であり、言い換えれば、問題があれば一切の機能を落とすくらいの作業をしなければ、「PHPのソースが見える」だけでなく、さまざまな悪影響が出てしまう可能性があります。これは、サーバー運用のポイントでもあります。

 結果的には、サーバーでは、管理者が意図したサービスのみを、管理者以外のクライアントにサービスをする状態にするのが原則です。例えば、WebサーバーでTLS(一般にはSSLと言われますが、この章ではSSLの後継規格である「TLS」を用います)でしか接続させていないというのであれば、そのサーバーはネットワークから見ると、443番ポートしか開いていないというのが原則です。管理者がそのようにセットアップをして、第三者による変更を許さないようサーバー管理者が責任を持つ、という前提条件でWebアプリケーションは成り立っています。

INTER-Mediatorを利用するネットワークの前提条件

 Webアプリケーションなので、ネットワーク上に存在しているということは当然なのですが、業務系のアプリケーションの場合、文字通りの意味で世界中のネットワークを通るわけではなく、場合によっては、組織内のクローズドなネットワークだけを通してのアプリケーションの利用に限られていることもあります。状況に応じて脅威は変化し、対策も変わってきます。

 しかしながら、アプリケーションの開発者や利用者にコントロールできるのは、TLSの利用をするかどうかという点が一番大きいでしょう。また、アプリケーションによっては認証の利用やアクセス権(認可)の設定が必要かどうかを検討するかもしれません。認証やアクセス権については、この章の大きな目的であって各所で説明があるので、ここではTLSについての基本的なスタンスを説明します。

 執筆時点で最新規格であるTLS 1.2は、適切な暗号スイートを利用すれば、送信者と受信者以外に通信内容を取り出せないように暗号化通信を実現できます。暗号を解読するには極めて長い時間を要することから、将来致命的な弱点が見つからない限り、現実的な時間での解読はほぼ困難と言えるでしょう。なお、SSLについては攻撃手段が発見されて、通信の傍受の可能性は0ではなくなったので、利用する仕組みとしては現在は既に除外されています。

 TLSで守られるとしたら、どういう状況でしょうか? まず、クライアントとサーバーでは、通信前後に暗号化が解除される段階があるので、TLSによって、すべてが守られるわけではありません。一方、Wi-Fiの電波を解析したり、Ethernetに流れる通信結果を取り出しても前述の通り解析は事実上不可能ですので、漏洩はTLSを使う限り原則的には発生しません。通信内容を読み取られたとしても通信経路上のデータは暗号化されているからです。また、サーバーとクライアントの間には、たくさんのルーティングの機材(ルータあるいはホスト)が介在しますので、その機材を通る間のパケットを傍受される可能性もあります。しかし、TLSの場合は、中間で暗号化の解除は通常はできませんので、この脅威も取り除かれることになります。

 では、組織内のネットワークしか通過しないのなら、TLSは必要でしょうか? これは、組織内のネットワークが正しく運用されているかと、組織内のスタッフを信用できるかどうかに関わってきます。「社内だから大丈夫」と思ったら、Wi-Fiに誰もが入れるようなお粗末な管理だと、「内部ネットワーク」とは言えず、結果的にはインターネットカフェなどと同じ程度の安全性になります。つまり、組織内だからTLSは不要であるという結論を出すには、ネットワークの管理が正しく行われているかどうかに依存します。その上で、スタッフを信頼できるかどうかは、是非とも組織内でディスカッションをしてみるべきテーマです。

ブラウザーのセキュリティ

 現在、セキュリティ上の懸念点があるのは、サーバーやネットワークよりもむしろブラウザーであると言えるでしょう。ソフトウェアは作り方によってはセキュリティ上の問題点を発生します。特にXSS(クロスサイトスクリプティング)については、あらゆるWebアプリケーション開発者が常に意識すべき問題です。これについては、このすぐ後の『クロスサイトスクリプティング攻撃(XSS)とinnerHTML』で説明します。

 ブラウザーでの一番の問題は、利用者が切り替わる場合の対処です。認証が必要なサイトをあるユーザーが利用していたとして、しばらくデスクを空けていたとします。その間に、画面がスリープやロックになって、元のユーザーにしか分からないパスワードでしか解除できない状況なら問題はありません。しかし、ブラウザーのウインドウが開きっぱなしならどうでしょうか? それまでのユーザーになりすまして、別のユーザーも利用できてしまいます。これは、使用中だけでなく、使用後と思っても正しくログアウトできていない場合、やはり別のユーザーが偶然に認証済みのページを見つけてしまうかもしれません。この問題は、すべてのアプリケーションに存在し、防ぐ方法はありません。言い換えれば、利用者が端末を適切な方法で利用しない場合には、脅威として必ず存在します。もちろん、INTER-Mediatorだけの問題ではないのです。アプリケーションを実装するときに、何もしないと認証が時間切れになる仕組みを利用したり、あるいはその時間を短くするなどで、他人がなりすます可能性を少しは減るかもしれませんが、力ずくで端末を開いた状態で取り上げられるようなことがあれば、やはり脅威となり得るのです。

 ブラウザー上で稼働するJavaScriptのプログラムの場合、常に、変数の結果を参照したり、あるいは書き換えたり、場合によってはプログラムの書き換えができてしまう点に注意を払う必要があります。INTER-Mediatorでは、パスワードそのものは変数にも残さないようにしていますが、認証の手がかりとなる情報は変数に残しています。ある操作をすれば、それは参照できますが、原則として、本人が操作して見えてしまっても、本人である限りは問題ないと言えます。また、書き換えた結果、何もできなくなるのなら、それは書き換えた人の責任であり、開発する側は責任を取り必要はないと考えます。しかしながら、問題になるのは、そうして書き換えをした結果、別のユーザーになりすますことができるような状況があるかどうかです。INTER-Mediatorでは、その点はテストしており、認証時に別のユーザーになりすますことはできない仕組みになっています。少なくとも、開発時にはその点も考慮しました。

 いずれにしても、JavaScriptのプログラムをクライアントで触れる点は問題になりそうですが、それによって他のユーザーになりすましたり、あるいはシステムやデータに損傷を加えることがなければ問題はないと言えます。ただし、この点について「バグが発見されていない」というだけのことで、この点について保証できるものではありません。潜在的には何かしらの問題点がある可能性があります。バグによる脆弱性の存在の可能性はINTER-Mediatorに限らずどんなソフトウェアにも存在します。何らかの懸念点があれば、アプリケーション開発者でもフレームワークの動作を検証する必要はあるかもしれません。

クロスサイトスクリプティング攻撃(XSS)とinnerHTML

 クロスサイトスクリプティング攻撃(XSS)は、現在のWebアプリケーション開発において、必ず考慮されなければならないセキュリティ要件です。このXSSは、攻撃者が仕込んだ任意のJavaScriptが実行できる状況において発生し、その結果、例えばある特定のサイト向けのクッキー情報やページ上に表示された情報を、攻撃者のサイトにネットワーク転送してしまうような悪用が考えられます。つまり、ページ上で、任意のスクリプトが実行される状況を作ってしまうと、XSSが可能なセキュリティホールが発生します。これは、フレームワークに内在する問題ではなく、どんなフレームワークを使っても、アプリケーション側で意図せずに実装されてしまう可能性のあるセキュリティホールです。

 XSSが発生する代表的な例が、「誰でも書き込みができるBBS」の事例です。せっかくのBBSを盛り上げるために、HTMLで自由にスタイルなどを設定するようにしていたとします。すると、書き込むメッセージにSCRIPTタグがあるとします。そのSCRIPTタグのプログラムは、書き込んだ人だけでなく、BBSを閲覧している人のブラウザー上でも実行します。クッキーの情報は、記録したドメインと同じドメインのサイトに接続しないと読み出せないといった制限がかかってはいるものの、スクリプトはまさにそのサイトにおいて実行しているので、他の人のクッキーを得ることができます。クッキーに含まれる情報で認証の成立を確認する状況は比較的利用されており、クッキーを第三者に取り出されてしまうと、その第三者によってなりすましのログインができてしまう場合も最悪はあり得ることになります。

 この時、メッセージのHTMLテキストを表示するのに利用されるひとつの方法が、innerHTMLプロパティへの代入です。HTML文字列をそのまま代入できて便利なのですが、その文字列にSCRIPTタグが含まれている可能性があるため、innerHTMLの利用を控える必要があります。INTER-Mediatorでは、意図的にinnerHTMLを使用するように設定した場合と、認証パネルのカスタマイズの部分を除いて、innerHTMLは利用しないようになっています。通常のデータベースから得られたデータをタグ要素にマージするときには、DOMのAPIを使ってテキストノードとして追加をしています。

 INTER-Mediatorで制限付きながらもinnerHTMLをサポートしている理由を説明しましょう。innerHTMLの危険性がある一方で、innerHTMLがあれば必ずXSS攻撃を受けてしまうということにはなりません。任意のHTMLテキストをデータとして書き込めるユーザーを限定すれば、第三者からのXSS攻撃は防ぐことができます。例えば、ある組織の通販サイトを運用するのであれば、メッセージを書き込むのは通常は通販会社の担当者です。利用者からのメッセージは例えばテキストだけにするということが一般的です。そういう状況においては、INTER-Mediatorは「任意のスクリプトを第三者が実行できる」状態にはなっていません。そのためにも、HTMLテキストを書込み権限は認証したユーザーだけに与えることをベースにして設計する必要があります。悪意のある人に書き込み権限を与えない方策を備えていれば、innerHTMLを全面的に排除していないことが、問題にはならないと考えています。すなわち、システムを運用する組織としての信頼関係があるユーザーだけに書き込みを許可すれば、XSSの発生となる根本原因を除去できるということです。もちろん、信頼した相手が攻撃をしないという前提の元で成り立つことです。

クロスサイトリクエストフォージェリ(CSRF)を排除する

 クロスサイトリクエストフォージェリ(CSRF)は、最終的にはユーザーが意図しないサイトへ誘導されて、意図しない処理をさせられてしまう可能性のある攻撃です。攻撃者は攻撃用のWebページを公開し、あるユーザーがそのWebページを参照したとします。そのページの中にあるJavaScriptが、全く別のサイトに接続してリクエストを送るということができます。例えば、偽の「お買い得情報!」メールを送り、それを見たユーザーが攻撃者のサイトとは知らずにアクセスしたとします。その際に、そのサイトのファイル一式の中に、オンラインショップに接続して何か購入することを決定するようなスクリプトが組まれていたとしたらどうでしょう。そのユーザーがいつも使うオンラインショップであれば、認証がすでに有効になったままかもしれません。そのままJavaScriptのAJAXの機能を利用して自動的にオンラインサイトを操作して、購入決定までさせてしまうことは技術的には可能です。購入操作が無事完了したかに見えますが、商品は届かないかもしれません。もちろんオンラインショップのコントロールは難しそうですが、匿名の掲示板だとそれほど困難ではありません。ユーザー自身が知らないうちにあちこちのBBSに、攻撃者が記述したなりすましメッセージを書き込まれてしまうということにもなってしまいます。

 INTER-Mediatorでは、作成したアプリケーションがCSRFを受け入れる脆弱性を持ってしまうことを防ぐための対策も組み入れています。対策方法としては、リクエストヘッダーにX-FromおよびOriginを利用する手法を利用しました。ポイントは、HTTPリクエストがサーバーに来た時にOriginヘッダーをチェックすることです。JavaScriptで作成した通信処理はXMLHttpRequestクラスを利用しますが、通信においてはそのページが生成されたサーバーのURLがクライアント側で自動的にOriginヘッダーの値に設定されます。そこで、INTER-Mediatorのサーバーでは自分自身のFQDN値を記録し、クライアント側ではX-Fromヘッダーにその値に付与するように動作します。元サーバーから自動的に付与されるOriginがX-Fromと同じであり、それがサーバーに設定したFQDNと同じであれば、CSRF攻撃ではないと判断できます。さらに、DNSサーバーの応答を操作することでの攻撃を回避するためにHostヘッダーも確認しています。攻撃側では攻撃対象のサーバーのFQDNはもちろん分かりますが、それを攻撃プログラムに組み込んでもDNSが正しく稼働している限りはOriginは攻撃者のサーバーになるので、OriginとX-Fromは一致せずCSRF攻撃であるとみなして処理はスキップします。X-Fromを攻撃者のサーバーと同じに設定しても、サーバーに設定したFQDNとも違うので、やはり攻撃が成り立ちません。CSRF対策として自分自身のFQDNをサーバーへ設定する方法については『8-1 定義ファイルの設定内容と外部での設定』で説明します。

データベースアカウントへのアクセス権設定

 データベースエンジンには通常、アクセスするためのアカウントを設定可能です。INTER-Mediatorはデータをデータベースに保持するので、データベースアカウントを利用することで、可能な限り安全な運用が可能です。なお、SQLデータベースのひとつであるSQLiteではアカウントの設定機能はありません。設定の詳細については、『2-1 データベースからの取り出し設定』で説明があります。

 SQLiteはネットワーク接続ができませんが、その他のINTER-Mediatorが対応するデータベースはすべてネットワークからの接続ができます。しかしながら、INTER-MediatorあるいはWebサーバーで稼働するWebアプリケーションから利用するときには、ネットワーク接続が必須なわけではありません。例えば、Webサーバーとデータベースサーバーが同一の場合は、データベースへのネットワーク接続は不要です。その場合は、localhostからの接続だけに限定することで、セキュリティ面では「他のコンピューターからの直接接続」は排除できて、不正なアクセスを許す要因は取り除かれます。また、Webサーバーとデータベースサーバーが異なるホストでも、データベース側ではWebサーバーからのアクセスのみを許可することで、他のコンピューターからの接続は排除できます。不要なネットワーク処理を利用できる状態にしないというのは、データベースに限らず、すべてのサーバー運用では鉄則と言える対処です。

 MySQLやPostgreSQLなどSQLサーバーでデータベースを構築するときには、データベースサーバー自体に対して何でもできるスーパーユーザーを設定します。MySQLではroot、PostgreSQLではpgsql、SQL ServerではSAというユーザー名が一般的でしょう。また、FileMaker Serverは、各データベースにすべての設定変更が可能な管理者ユーザーを設定します。名称は任意ですが、Adminなどの名前をつけます。これらのアカウントには原則としてパスワードを設定します。PostgreSQLの場合は、OSのアカウントを利用するのですが、パスワードを利用してログインをします。これらデータベースエンジンのアカウントは、最初にデータベースを作ったり、スキーマの適用をする場合には必要になりますが、実運用、特に、Web経由のアクセスに応答するために利用することは、可能なかぎり避けるべきでしょう。理由は、「制限したユーザーでの利用の方が少しでも安全になる確率が高い」からです。

 もし、Webアプリケーションからのアクセスが、データベースからの読み出しのみの場合、データベースのユーザーは、スキーマ定義ができないことはもちろん、必要なテーブルに対してSELECTのみ、あるいは読み出しのみの権限のみを与えておきます。また、Web側から更新があるのなら、スキーマ定義はできないものの、SELECT/DELETE/INSERT/UPDATE、あるいは読み書き権限を与えておきます。こうした、スーパーユーザーでないユーザーの運用制限を加えておくことで最悪の事態が起きた場合でもデータの安全が確保されることになります。最悪の場合に考えられるのは、ソースコードごと漏洩してデータベースのスーパーユーザーが知られてしまう可能性があることや、設定を誤り、抜け道を作ってしまい、読み出ししかできないはずが認証もなくデータを消すことができてしまったといったことがあります。

 もし、サーバー管理を正しく構築し、ソリューションにセキュリティホール無く作ることができれば、スーパーユーザーでも問題ないのかもしれません。しかしながら、一種の保険として、データベースへのアクセス権を必要以上に広げないようにすることをお勧めします。そんなことは不要、あるいは過剰と思われるかもしれませんが、サーバー運用において不要なポートを閉じるのと同様な、予防的な措置として検討しましょう。

 なお、プロバイダのレンタルサーバーでは、スキーマ定義や読み書きができるひとつのアカウントしか使えないことがあります。その場合は仕方ありませんので、他の手法でセキュリティを確保することにします。

セクションのまとめ

 Webアプリケーションは、「サーバー側を誰もが自由に参照したり改変することはできない」という前提のもとで、セキュリティ対策を行っています。つまり、Webサーバーにログインできるユーザーを制限することが大前提です。また、データベースを利用するためのユーザーについても、余分な処理をできないようにしておくことで、他の問題が発生してもセキュリティが確保される確率は幾分は高くなるでしょう。

7-2ユーザー認証とアクセス権適用を行う仕組み

INTER-Mediatorは従来形式のWebアプリケーションと違い、クライアントサイドでの処理がむしろ主になっています。認証とアクセス権適用の考え方については、クライアントとサーバーでの役割が従来とは異なります。ただし、アプリケーション利用者にとっては違いがないようになっています。このセクションでは、INTER-Mediatorの認証やアクセス権の機能に加えて、その実現に必要なことを説明します。

認証とアクセス権

 改めて、「認証(Authentication)」についての定義を記載します。認証は、利用者の特定を行い、アカウントが確定することです。ユーザー名とパスワードを入力するのが認証の代表的な手法です。何らかの方法で、アカウント、つまりシステムに記録された利用者のどれかを確定することが認証で実現されることです。もちろん、他のアカウントになりすましたり、他のアカウントとして振るまえてしまうということがないようにする必要があります。ユーザー名とパスワードを使う方式ではさまざまなセキュリティ的なリスクがありますが、一番大きいものが「パスワードの漏洩」です。言い換えれば、パスワードはそのアカウントに対応する人しか知りえないという前提の下で成り立つセキュリティの確保の手法です。

 「アクセス権(Authorization)」は、「認可」とも呼ばれます。通常は認証によって確定したアカウントに対する処理範囲の制限を行う機能です。制限は、アプリケーション開発者やあるいはシステム管理者によって指定されます。一般には、特定の処理だけに制限するような設定、ないしは特定のアカウントに対してだけ特定の処理の許可を与えるといった設定が可能です。アクセス権についてのセキュリティ的なリスクとしては、もちろん、フレームワークのバグや、バックドアなどのアクセス権回避手段が存在しないといった、フレームワークの実装面のことがあります。また、アクセス権の設定は複雑になりがちであり、設定のミスが起こりやすいとも言えるので、作成したアプリケーションの十分な実証テストは欠かせません。

アカウントとグループ

 INTER-Mediatorは、アカウントとして「ユーザー」「グループ」、そして内部的なアカウントとして「クライアント」の3種類をサポートします。グループは、ユーザーの集合ではありますが、グループにグループを所属させることができます。ユーザーは自分が所属するグループの一覧を得て、アクセス権の適用を受けます。

 ユーザーのレコードは、データベースエンジンに組み込まれたユーザーを利用する方法(ネイティブ認証)、アプリケーションが利用するデータベースに含まれるテーブルあるいはビューを利用する方法(ユーザー認証)が用意されています。LDAPやOAuth2による認証にも対応していますが、その場合には後者のアプリケーションが利用するデータベースでのユーザー用テーブルは必須となります。

 一方、グループについては、INTER-Mediator側で定義したものだけが指定できます。データベースエンジン側で定義したものに対してのアクセス権設定はできません。ユーザー認証時はもちろん、ネイティブ認証時にもアプリケーションが利用するデータベースにグループを管理するためのテーブルを定義し、そのグループレコードの利用のみです。

 クライアントのアカウントは、通信のやり取りが発生したときにINTER-Mediatorによって自動的に割り振られるコードです。専用のテーブルは不要ですが、認証のための情報を記録するテーブルのひとつのフィールドとして記録されます。このクライアントコードを利用して、クライアント、正確には「ブラウザーのウインドウ」を識別します。

ユーザーのテーブル

 ネイティブ認証以外の手法では、ユーザーのテーブルが必要です。既定のテーブル名は「authuser」としています。この名前は設定で別のものにもできますが、以下、特定しやすいように、ユーザーアカウントのテーブルはauthuserテーブルと呼ぶことにします。なお、テーブル名は任意の名前を指定できますが、フィールド名は決められたもののを使用する必要があります。表7-2-1に必要なフィールドを記述します。認証だけならテーブル自体は読み込みのみのアクセス権でも構いませんが、パスワードの変更をログインパネル上で行う場合には、パスワードのフィールドは書き込み可能である必要があります。また、テーブルの内容をアプリケーションで追加編集する場合には、一般には読み出しだけでなく変更や追加、削除の権限も必要になります。サポートするデータベースのサンプルファイルあるいはサンプルスキーマには、authuserテーブルそのものあるいは生成コマンドが含まれているので、自分で作成するアプリケーションの場合はその部分をコピーすると良いでしょう。

フィールド名型の例説明
idINT AUTO_INCREMENT連番の数値を入れて、キーフィールドとする
usernameVARCHAR(48)ユーザー名(重複した名前が定義されないようにする)
hashedpasswdVARCHAR(48)パスワードのハッシュ値(パスワード変更があるなら要書き込み)
emailVARCHAR(100)ユーザーのメールアドレス(メールアドレスをユーザー名にするときには必須)
limitdtDATETIMELDAP、OAuth2で必要。キャッシュしたアカウントの期限
表7-2-1 authuserテーブルのフィールド

 パスワードのフィールドには、パスワードはそのまま入れずにダイジェスト関数によって処理された値を使います。計算方法は以下のとおりです。独自に生成する場合には、saltを常に異なる値にすることが必要です。なお、JavaScript側のライブラリの制約により、パスワードおよびsaltはASCII文字として表現可能な範囲にします。コントロールコードや漢字は利用しないようにします。シェルスクリプトで生成する場合のサンプルは、INTER-Mediatorのレポジトリにあるdist-docs/usergenerator.shを参照してください。

リスト7-2-1 hashedpasswdフィールドの値の求め方
pw:パスワード
salt:4バイトのソルト値
sha1():SHA-1によるダイジェスト値を求める関数
hex():16進法による表記に変換する関数
+:文字列の結合

hashedpasswdフィールドの値 = hex(sha1(pw + salt)) + hex(salt)

 これらのフィールドは必須のフィールドですが、任意のフィールドを追加してもかまいません。例えば、VARCHAR(20)型のrealnameフィールドに、ユーザーのフルネームを入れておけば、ログインした人の氏名をページ上に表示するようなことにも利用できるでしょう。

 もし、すでに別の名前でユーザーテーブルが作られているのであれば、SQLデータベースの場合には、authuserビューを定義し、フィールド名はビュー定義のコマンドで別の名前に付け替えるような記述を行えば良いでしょう。FileMakerの場合には、レイアウト名をauthuserにすることで、任意のテーブルを利用できます。ただし、既存のフィールドが作られている場合ではフィールド名が表7-2-1のようになっていない可能性があります。その場合はフィールド名を変更するか、あるいは計算フィールド等フィールドの追加を行い、表に示したフィールドが存在するように見えるようにします。

グループのテーブル

 グループを構成するテーブルは2つ必要です。まず、ひとつはグループそのものを定義する「authgroup」テーブルです。加えて、ユーザーやグループの所属関係を定義する「authcor」テーブルです。いずれも、任意の名前を利用できますが、テーブルの種類を特定する場合、authgroupテーブル、authcorテーブルと呼ぶことにします。表7-2-2と表7-2-3に、必要なフィールドを記述します。

フィールド名型の例説明
idINT AUTO_INCREMENTグループを識別するための番号
groupnameVARCHAR(48)グループ名
表7-2-2 authgroupテーブルのフィールド
フィールド名型の例説明
idINT AUTO_INCREMENTレコードを識別するための番号
user_idINT所属するユーザーのidフィールドの値
group_idINT所属するグループのidフィールドの値
dest_group_idINT所属されるグループのidフィールドの値
表7-2-3 authcorテーブルのフィールド

 authgroupテーブルは単にグループ名とid番号の割り当てのためのものです。authcorテーブルのひとつのレコードでは、user_idフィールドもしくはgroup_idフィールドのどちらかを入力し、一方はNULL(FileMakerでは"")にします。dest_group_idフィールドにはauthgroupテーブルに存在するidフィールド値を指定します。このauthcorテーブルのフィールドは、authuserおよびauthgroupテーブルのidフィールドの値を設定します。ユーザー名やグループ名を指定するのではありません。

ユーザーレコード生成のためのスクリプト

 データベースのスキーマを読み込む段階で、すでにいくつかのユーザーを作っておきたい場合もあるでしょう。もちろん、自分でハッシュを計算することで可能ですが、INTER-Mediatorには、macOSあるいはLinuxで利用できるユーザーレコード作成のコマンドが用意されています。INTER-Mediatorフォルダー内のdist-docsフォルダー内にある「passwdgen.sh」というシェルスクリプトです。

 このシェルスクリプトを使えば、hashedpasswdフィールドの値を求め、場合によっては、SQLステートメントの形式で得られます。INTER-Mediator/dist-docsがカレントディレクトリにあるとして、例えばリスト7-2-2のようにコマンド入力できます。行の最初に$があるのがコマンド入力行です。最初のコマンド例は、引数として--userでユーザー名、--passwrodでパスワード文字列を指定しており、出力は、CSVファイルの形式で、ユーザー名、パスワード、hashedpasswdフィールドの値の順に表示されます。ソルトは乱数で生成しています。2つ目のコマンド例は、さらに、--sqlを引数に指定しており、これにより、SQLステートメントの形式で出力をしています。

リスト7-2-2 passwdgen.shスクリプトの利用例
$ ./passwdgen.sh --user=test --password=testpassword
'test','testpassword','d72f7de01c7b2c16bf56dc9d8d501204f454b75e566d2c44'
$ ./passwdgen.sh --user=test --password=testpassword --sql
INSERT authuser(username,initialpass,hashedpasswd) VALUES('test','testpassword','5221ba90506becd7dcef0550ad344bec1173ca832b496020');

 一方、CSVファイルを用意して、そのファイルを「./passwdgen.sh --sql --csv=CSVファイルのパス」のように指定すると、上記の処理をCSVファイルから取り出して行うので、多数のユーザー用のSQLステートメントを生成できます。CSVファイルは1列目がユーザー名、2列目がパスワードです。カンマで区切りますが、それぞれのフィールドは、シングルクォートやダブルクォートで囲っても構いませんし、囲わなくても構いません。

 同じフォルダーに、usergenerator.shというスクリプトファイルもあり、こちらは、パスワードの自動生成などを行います。なお、引数は特に取りません。なお、これらのスクリプトでは不満がある場合もあるかもしれません。いずれもシェルスクリプトですので、必要に応じて改良して使ってください。また、スクリプトのソースはユーザー自動生成の方法の参考になると思います。

ハッシュ値用テーブルの内容

 さらに認証を行う場合、ネイティブ認証でもユーザー認証でも、表7-2-4に示すテーブルが必要です。このテーブル名についてもカスタマイズは可能ですが、ここでは既定値の「issuedhash」テーブルと呼ぶことにします。通信を行うたびにレコードを生成するので、1回のページ表示でたくさんのレコードを作成します。そのため、このテーブルをFileMakerで運用するにはパフォーマンス上の問題が発生しますが、FileMaker Serverで運用しつつ、issuedhashテーブルのみをSQLiteで運用するということもできるようになっています。FileMakerではこの方法により、パフォーマンスを大きく損なうことなく認証処理ができるようになっています。

フィールド名型の例説明
idINT AUTO_INCREMENTレコードを識別するための番号
user_idINTauthuserテーブルのキーフィールドとなるid値
clienthostVARCHAR(48)クライアントを識別する自動生成されるコード
hashVARCHAR(48)チャレンジに使うハッシュ値。実際には24バイトの16進数文字列
expiredDateTimeチャレンジの有効期限を示すタイムスタンプ値
表7-2-4 issuedhashテーブルのフィールド

 このテーブルの意味を理解するには、INTER-Mediatorのクライアントサーバー間でのプロトコルを理解する必要があります。ここで解説は行いますが、機能を利用する上ではこの仕組みまで知る必要はありません。セキュリティのアセスメントが必要な方は頑張って理解してください。

 表7-2-5は、クライアントからサーバーに対してデータベースアクセス等の要求がある場合のやり取りを示したものです。変数名と計算方法はリスト7-2-3に示します。このプロトコルは、ログインパネルでユーザー名とパスワードを入力した後に遂行されるものとしてください。まず、単にアクセス要求と応答があるのではなく、チャレンジ要求が最初にあり、チャレンジをもとに要求を出したときに、サーバー側で認証が判定されるという点が大きな流れです。もちろん、認証が成立したら、データベースから取り出したデータを返したり、更新処理に入ります。認証が成立しなければ、ログインパネルを再度表示し、このプロトコルを最初からやり直します。

クライアント側での処理転送される認証情報サーバー側での処理
チャレンジ要求→user→データベースよりhpwを取得しsaltを求める
chを乱数より生成,cidが未発行なら乱数で生成
user, cid, ch, 日付時刻をデータベースに記録
resを求める←salt, cid, ch←
データベース要求→cid, user, res→issuedhashテーブルより,cidからチャレンジを取得
res'を計算し、resと同じなら認証成立
表7-2-5 認証を伴う処理の場合のINTER-Mediatorプロトコル
リスト7-2-3 変数と式
user:ユーザー名(ユーザーが入力)
pw:本来のパスワード(ユーザーが入力)
salt:ユーザーごとに異なるソルト(4バイトのASCII文字)
hpw:データベースに保存されているパスワードのハッシュ値
pw':入力したパスワード
cid:クライアントid(16進数で記述)
ch:サーバーから送られるチャレンジ(16進数で記述)
res:クライアントから送られるレスポンス
res':サーバー側の情報から得られるレスポンスの期待値
sha1(m):SHA-1によるダイジェスト値を求める関数
hmac(k, m):HMAC-SHA256で求められるMAC値を求める関数。ハッシュ関数はSHA-256,鍵がk,メッセージがm
hex(m):16進法による表記に変換する関数(hex(m) + hex(n) = hex(m + n)が成り立つ)
+:文字列の結合

hpw = sha1(pw + salt) + salt
res = hmac(sha1(pw' + salt) + salt, ch)
res' = HMAC(hpw, ch) = hmac(sha1(pw + salt) + salt, ch)
pw = pw' であれば res' = res となる

 まず、ログインパネルでユーザー名とパスワードを入力します。そして、ユーザー名(user)をサーバーに送ります。サーバー側では、そのユーザーのパスワードハッシュ値(hpw)とidフィールドの値をauthuserテーブルから取得します。hpwの最後の8バイトからソルト(salt)が得られます。そして、クライアントid(cid)とチャレンジ(ch)を乱数で生成します。そして、issuedhashテーブルにレコードを作成して、ユーザーのid値、cid、ch、日時を記録します。クライアントへは、応答としてsalt、cid、chを送ります。クライアントはデータベースの要求に加えてres値を計算して、サーバーへの要求に付加します。res値は、応答で得られた値と、入力したパスワードから求めておきます。また、要求にはuser、cidも返します。要求を受け取ったサーバーは、issuedhashテーブルを検索して、chを求めます。そして、ユーザー名から得られるsalt、返されたcidをもとに、res'を計算します。このres'がresと等しければ、クライアント側で正しいパスワードが入力されたと判断できるということです。resとres'は、式の上ではpwとpw'の部分だけが違います。

 この手法の大きな特徴は、ネットワーク上に流れるデータからパスワードを求めることができないということです。ネットワーク上のデータは、user、salt、cid、ch、resであり、これらの値からパスワード自体を求めることはできません。ハッシュ値hpwをデータベースに保存はしますが、パスワードはもちろん、そのハッシュ値自体もネットワーク上を流れないということです。この一連の作業で使われるチャレンジ(ch)は、サーバー側では共有しません。原則として1回のやり取りの間だけ有効になり、リクエストがあるたびに生成をします。したがって、resの値は要求ごとに異なる値になります。

 クライアントid(cid)については、可能な限り再利用を行います。この認証だけなら、cidでなくても認証はできそうに見えるかもしれませんが、同一ユーザーで同時に複数のページを利用している場合があることを考えれば、チャレンジ要求とデータベース処理要求を結びつける意味では必要です。

 なお、認証をすれば通常は1回の通信が2回へと倍になるのではないかと思われるかもしれませんが、より効率的な動作をします。データベース処理に対する応答には、次の要求のためのチャレンジ(ch)などのデータがすでに含まれていて、2回目以降は、データベースの応答と次のチャンレンジ応答を1回の応答で済ませてしまいます。したがって、認証がない時にn回の通信をする場合、認証があればn+1回の通信を行うことになり、1回、つまり最初のチャレンジ要求だけが増えることになります。

その他の手法

 INTER-Mediatorでのさまざまな手法を紹介する前に、INTER-Mediator外で適用できる手法を紹介しましょう。まず、最初に、WebサーバーでサポートするHTTP認証です。Apacheだとモジュールの追加で認証ができるようになります。.htpasswdファイルに設定を記述するなどの手法があります。INTER-Mediatorは、HTTP認証が機能するサーバー内で稼働することもできます。単に認証だけでいいのであれば、INTER-Mediatorの認証機能は一切使わないで、Apache側の設定だけで認証はできます。

 セキュリティを高める手法として、クライアントのIPアドレス制限をすることがあります。Apacheの設定ファイルで、アクセス可能なアドレス範囲を指定することができるので、特定の場所からのアクセスだけを許可したいような場合には、IPアドレスによる制限を有効に使うことできるでしょう。あるいは同等な仕組みをファイアウォールで実現してもいいかもしれません。

演習ユーザー管理の簡易アプリケーションを使ってみる

 INTER-Mediatorにはauthuser, authgroup, authcorテーブルの内容を編集する簡易アプリケーションが付属しています。テーブルの定義以外に、このアプリケーションをベースにして、独自のユーザー管理アプリケーションを作成することもできるでしょう。この演習では、単にアプリケーションの存在と動作の確認だけ行います。実際の利用は、この後のセクションで何度か出てきます。

ユーザー管理アプリケーションを参照する

1ここからの作業は、Webブラウザー上で行います。まず、INTER-MediatorがインストールされたVMを、VirtualBoxを使用して起動します(『1-2 演習を行うための準備』を参照)。続いて、ブラウザーで、「http://192.168.56.101」に接続します。
2「ユーザー管理ページサンプル」というタイトルの部分をクリックします。ユーザー名とパスワードを入力するパネルが表示されるので、ユーザーに「user1」、パスワードに「user1」と入力して、「ログイン」ボタンをクリックします。
このアプリケーションは「認証が通れば参照して変更できる」ようになっていますが、一般には、こうしたアプリケーションのユーザー管理機能は特定のグループのユーザーだけが参照し変更できるようにするのが安全な利用方法です。
3ページが表示されました。「User Accounts」と「Group Accounts」の2つのテーブルが表示されています。
パスワードの変更はこの段階では行わないでください。変更するには、ページ冒頭にある「New Password」の右にあるテキストフィールドにパスワードを入力して、User AccountsのHashed Password列の該当するユーザーの「Set」ボタンをクリックします。この操作により、ソルトの自動生成やSHA-1の計算などを自動的に行って、hashedpasswdフィールドへ正しい値を設定します。

設計内容を確認する

1このアプリケーションの定義ファイルを参照します。GitHubにあるソースコードを参照しましょう。こちらをクリックして、内容を参照します。
2認証の設定は次のセクションで説明します。ここでは関連テーブルの内容の把握が主な目的です。定義ファイルを見ると、次の5つのコンテキストが定義されています。nameキーの値と、()内にはviewおよびtableキーの値をピックアップしました。また、relationキーの有無も記載しました。
3ページの最初の方に、このファイルへのパス「INTER-Mediator/Auth_Support/MySQL_contexts.php」が見えている箇所があります。ここで、「Auth_Support」をクリックすると、Auth_Supportフォルダーの内容が表示されます。そこにあるページファイルの「MySQL_accountmanager.html」をクリックして、MySQL_accountmanager.htmlファイルの中身を表示します。
4User Accountsの見出しの下のテーブルを見てみます。authuserコンテキストのusernameやhashedpasswdフィールドなどが見えています。また5列目は、belonggroupコンテキストがSPANタグで展開されていますが、SELECTタグによるポップアップメニューは、belonggroupコンテキストの関連レコードdest_group_idを表示するようになっています。また、ポップアップメニューの選択肢はgroupnameコンテキストにより、authgroupテーブルのすべてのレコードが表示されています。
5列目は、結果として、そのユーザーが所属するグループの数だけポップアップメニューが並びます。つまりひとつのリピーターにひとつのポップアップメニューがあるので、該当するauthuserテーブルのidフィールドと同じ値をauthcorテーブルのuser_idフィールドに持つ関連付けられたレコードの数だけ繰り返されます。dest_group_idフィールドはauthgroupテーブルのidフィールドの値が入力されていますが、実際のグループ名に変換するのはポップアップメニューの仕組みを使っています。この点は、このすぐ後にauthcorテーブルの内容を確かめるので、その時に改めて内容を確認すると良いでしょう。
authuserコンテキストのbelongingフィールドは、元のテーブル定義にはありません。これは、サーバー側のPHPプログラム(同じフォルダーにあるUserList.php)によって、検索後にサーバーサイドの処理で付加されたフィールドです。この仕組みは、『Chapter 8 サーバーサイドでのプログラミング』で説明します。
5スクロールして、Group Accountsの見出しの下のテーブルを見てみます。authgroupコンテキストのgroupnameフィールドが見えています。2列目は関連付けられたgroupingroupコンテキストのdest_group_idフィールドの値を持つポップアップメニューが定義されています。そして、ポップアップメニューの選択肢はgroupnameコンテキストから得ています。2列目のポップアップメニューは、User Accountsテーブルの5列目と同様な構成になっています。

ユーザーとグループの対応関係を確認する

1「ユーザー管理ページサンプル」をクリックして表示した、ユーザー管理アプリケーションのページを表示します。通常は、以前に開いたタブあるいはウインドウがあるので、それを呼び出せば良いでしょう。
2User Accountsのうち、Groupsの列を見て、group1に含まれているのは、user1とuser2、user3の3つのアカウントであることを確認します。
3Group Accountsのgroup1は、さらにgroup3に含まれていることを確認します。
4User Accountsの中で、user1とuser2、user3は、Groupsの列を見る限りはgroup1にしか登録されていません。しかしながら、Belongingsの列を見ると、group1とgroup3に含まれていることになっています。つまり、group1に含まれるユーザーは自動的にgroup3にも含まれることになります。
5逆にgroup3に含まれるユーザーは、Belongingsの列にgroup3があるユーザーで、user1〜user5が相当します。言い換えれば、group3に対してアクセス権を与えることで、user1〜user5に与えるということができます。
6このように、グループは単にユーザーの分類ではなく、グループを所属させることによる階層的な構成をとることもできます。authuserとauthgroupテーブルの内容は、ページファイルに見える通りですが、authcorテーブルはこの状態では以下のような内容です。なお、フィールドの内容は実際にはidの整数値ですが、それだと分かりにくいので、対応するusernameあるいはgroupnameフィールドの値を()で付記しました。
user_idgroup_iddest_group_id
1(user1)NULL1(group1)
2(user2)NULL1(group1)
3(user3)NULL1(group1)
4(user4)NULL2(group2)
5(user5)NULL2(group2)
4(user4)NULL3(group3)
5(user5)NULL3(group3)
NULL1(group1)3(group3)
表7-2-6 authcorテーブルの値

演習のまとめ

セクションのまとめ

 INTER-Mediatorでの認証やアクセス権設定では、ユーザーやグループを使用します。それらは、authuser、authgroup、authcorのそれぞれのテーブルに記録しておくのが基本です。これらのテーブルの内容を編集するためのWebアプリケーションがINTER-MediatorのレポジトリのAuth_Supportフォルダーに作られています。さらに、認証をチャレンジ-レスポンスによって行うためのissuedhashテーブルも必要です。認証のための通信では、パスワードやそのパスワードから求められてデータベースに記録されたハッシュ値を通信経路上に流すことなく、認証が行われます。

7-3認証とアクセス権の設定

認証やアクセス権に対する設定は、定義ファイルに行います。これらの設定を、JavaScriptでカスタマイズすることはできません。言い換えれば、設定はクライアント側から変更することを一切できないようにすることで、セキュリティの確保を行っています。このセクションでは、設定方法と設定例を説明します。

認証に関する設定を行う場所

 認証やアクセス権に関する設定は、定義ファイルで行います。JavaScriptのAPIは用意していないので、原則として、すべて定義ファイル側で行うと考えてください。設定項目は多岐に渡ります。まず、リスト7-3-1に示したのは、定義ファイルのPHPによる記述で記述可能なキーをすべて示したものです。それぞれのキーに対する設定内容は、後で示します。IM_Entry関数の最初の引数(コンテキスト定義)に指定するコンテキスト定義に設定する項目があり、加えてIM_Entry関数の第2引数(オプション設定)に指定する項目もあります。

リスト7-3-1 定義ファイルでの認証やアクセス権設定の場所
IM_Entry(
	array(	//コンテキスト定義
		array(	
			"name"=>"context",	// コンテキスト名
			"authentication"=>array(
				"all" => array(
					"user" => array( .... ),
					"group" => array( .... ),
					"target" => "....",
					"field" => "....",
				),
				"read" => array(
					/* "all" と同様 */
				),
				"update" => array(
					/* "all" と同様 */
				),
				"create" => array(
					/* "all" と同様 */
				),
				"delete" => array(
					/* "all" と同様 */
				),
			),
			"protect-writing" => array( .... ),
			"protect-reading" => array( .... ),
		),
	),
	array(	//オプション設定
		"authentication"=>array(
			"user" => array( .... ),
			"group" => array( .... ),
			"user-table" => "string",
			"group-table" => "string",
			"corresponding-table" => "string",
			"challenge-table" => "string",
			"authexpired" => "string",
			"storing" => "cookie|cookie-domainwide|session-storage",
			"realm" => "string",
			"email-as-username" => "boolean",
			"issuedhash-dsn" => "string",
		),
		"media-context" > "string",
	),
....);

 定義ファイルエディターでのコンテキスト内の設定は図7-3-1のようなもので、「Show All」ボタンをクリックすることで表示されます。最初の状態では認証関連の設定は見えていません。このうち、リスト7-3-2で、「array(...)」つまり配列で指定が必要な項目については、半角のカンマ(,)で区切ってテキストを記述することで、それぞれを要素とする配列をキーに対する値として設定します。設定値については、表7-3-1に示します。表に記載の内容も、順次説明します。

図7-3-1 定義ファイルエディターでのコンテキスト定義内の認証関連設定
指定場所キーキー値と動作
コンテキスト/authenticationallCRUDのすべての操作に対する設定をまとめて与える
userユーザー名の配列を指定すれば、それらのユーザーのみに許可が与えられる。省略するとすべてのユーザー
groupグループ名の配列を指定すれば、それらのグループのみに許可が与えられる。省略するとすべてのグループ
target"table"あるいは省略の場合テーブルに対してアクセス権を設定、"field-user"あるいは"field-group"なら次のfieldキーの値で指定したフィールドにあるデータをユーザー名ないしはグループ名として、レコード単位でそのユーザーあるいはグループに対してのみアクセス権を付与する
fieldレコード単位のアクセス権設定において、ユーザー名やグループ名が入力されるフィールドの名前
readテーブルからの読み出し(クエリー)に対するアクセス権を設定する
user/group/target/field("all"の場合と同じ)
updateレコードへの更新に対するアクセス権を設定する
user/group/target/field("all"の場合と同じ)
createテーブルへの新規レコード作成に対するアクセス権を設定する
user/group/target/field("all"の場合と同じ)
deleteテーブルからのレコード削除に対するアクセス権を設定する
user/group/target/field("all"の場合と同じ)
media-handlingtrueを指定すると、メディア向けのチャレンジを生成してクライアントに送る
コンテキストprotect-writingフィールド名を配列で指定する。これらのフィールドはクライアントからの更新を受け付けなくなる
protect-readingフィールド名を配列で指定する。これらのフィールドはクライアントからの読み出しを受け付けなくなる
表7-3-1 コンテキスト定義内の認証関連設定

 定義ファイルエディターでのオプション設定にある認証関連の設定は図7-3-2のようなものです。こちらにも、userおよびgroupは配列での指定が必要であり、複数の要素を設定する必要がある場合にはカンマで区切って指定します。設定値については、表7-3-2に示します。表に記載の内容も、順次説明します。

図7-3-2 定義ファイルエディターでのオプション設定内の認証関連設定
指定場所キー値と動作
オプション/authenticationuserユーザー名の配列を指定すれば、それらのユーザーのみに許可が与えられる。値が「database_native」ならば、ネイティブ認証。省略するとすべてのユーザー
groupグループ名の配列を指定すれば、それらのグループのみに許可が与えられる。省略するとすべてのグループ
user-tableユーザーテーブルの名前がauthuserでない場合にここにその名前を指定する
group-tableグループテーブルの名前がauthgroupでない場合にここにその名前を指定する
corresponding-tableグループ対応テーブルの名前がauthcorでない場合にここにその名前を指定する
challenge-tableハッシュテーブルの名前がissuedhashでない場合にここにその名前を指定する
storing認証情報を記録して再利用する方法を指定する。"cookie"ならそのディレクトリ内で共有、"cookie-domainwide"は同一ドメイン内で共有、"session-storage"はブラウザーのデータベースを利用して共有
authexpired認証情報の記録のタイムアウト。ここで指定した時間、通信がないと認証状態を無効にする
realm認証領域名。ログインパネルに表示される。また、認証状態の記録時のキー名に付加される。この文字列には半角のドットは指定しない
email-as-usernameemailフィールドの値をユーザー名として受け入れる場合にはtrue、省略はfalseと同じ
issuedhash-dsnisshedhashテーブルが存在するデータベースのDSNを指定する。省略時は他で指定されるデータベースでisshedhashテーブルを運用
オプションmedia-contextメディアアクセスに認証を設定するとき、ここで指定した名前のコンテキストにアクセスしたときにメディアの認証用のトークンをクライアントに届ける
表7-3-2 オプション設定内の認証関連設定

 ユーザーやグループなど、同様な設定がコンテキスト定義にもオプション設定にもあります。もちろん、オプション設定は、定義ファイルで定義したすべてのコンテキストに対して適用されるのに対して、コンテキストへの定義はそのコンテキストだけに適用されるものです。ただし、すべての設定が両方にあるわけではなく、実運用を考慮してオプション設定には全体的な設定、コンテキスト定義には詳細な設定が存在するようにしました。

設定例による認証の設定

 設定の詳細も重要ですが、複雑なので、ここと次のセクションで、用途に応じてどのような設定を行うのかを例で示します。以下の表記は定義ファイルでのPHPでの記述を行います。なお、いずれも、『7-2 ユーザー認証とアクセス権適用を行う仕組み』で説明したテーブルが用意されていて、ユーザーやグループが定義されていることを前提として説明します。

すべてのコンテキストで認証が必要

 あるページにおいて、認証を必要とするものの、認証さえできれば、ページ内での読み書きすべてができるといった、非常にシンプルな動作の場合は、リスト7-3-2のように、オプション設定にauthenticationキーの値さえあれば構いません。なお、定義ファイルでは、値が「array()」という設定はできません。そのため、storingキーで認証継続の設定や、authexpiredキーで認証継続の時間等をデフォルトと同じでもいいので設定をして、authenticationキーに要素のある配列が設定されるようにします。

リスト7-3-2 すべてのコンテキストで認証が必要になる定義ファイル
IM_Entry(
	array(	/* コンテキストにauthenticationの指定はなし*/	),
	array(
		'authentication' => array( ),	//項目のみ
	),
	...
)

特定のユーザーのみがログインできる

 ページ全体にわたって、特定のユーザーのみがログインして利用できるようにするには、リスト7-3-3のように、オプション設定のauthenticationキーに、userキーの配列を指定します。この設定は定義ファイルエディターではuserの項目に「user3, user4」のように指定をします。カンマの前後の空白は無視します。したがって、読みやすくするために自由に空白を入れることができます。ここでは、user3ないしはuser4はログインができて、ログイン後は読み書きの処理が全て許可される状態になります。その他のユーザーは、たとえパスワードが正しくても、認証エラーとなってログインができず、データ処理は一切できない状態になります。なお、この目的だけなら、オプション設定のauthenticationキーは不要ですが、認証継続の方法などの指定が必要ならば、オプション設定の記述を行います。

リスト7-3-3 特定のユーザーのみがログインできる定義ファイル
IM_Entry(
	array(	/* コンテキストにauthenticationの指定はなし*/	),
	array(
		'authentication' => array(
			'user' => array( 'user3', 'user4' ),
		),
	),
	...
)

特定のグループのユーザーのみがログインできる

 ページ全体にわたって、特定のグループに所属するユーザーのみがログインして利用できるようにするには、リスト7-3-4のように、オプション設定のauthenticationキーに、groupキーの配列を指定します。この設定は定義ファイルエディターではgroupの項目に「group1, group2」のように指定をします。カンマの前後の空白は無視します。ここでは、group1ないしはgroup2に所属するユーザーはログインができ、ログイン後は読み書きの処理が全て許可される状態になります。その他のユーザーは、たとえパスワードが正しくても、認証エラーとなってログインができず、データ処理は一切できない状態になります。なお、この目的だけなら、オプション設定のauthenticationキーは不要ですが、認証継続の方法などの指定が必要ならば、オプション設定の記述を行います。

リスト7-3-4 特定のグループのユーザーのみがログインできる定義ファイル
IM_Entry(
	array(	/* コンテキストにauthenticationの指定はなし*/	),
	array(
		'authentication' => array(
			'group' => array( 'group1', 'group2' ),
		),
	),
	...
)

ログインの継続方法と設定

 ログインパネルでログインをした結果、その後にログイン状態を続けるための情報が、JavaScriptの変数に記憶されます。しかしながら、その記憶結果はページを移動すると消えてしまいます。ひとつのサイトで複数のページファイルを使って構成する場合、ひとつのページで認証が成功すると、別のページでもログインパネルが表示されずに、認証を継続してもらいたいと考えるでしょう。INTER-Mediatorはそのために、認証情報をページ間で継続する方法を提供しています。設定は、オプション設定にあるstoringとauthexpiredキーを利用します。

 認証を継続するために保存するのは、ユーザー名とクレデンシャル、つまり、パスワードにハッシュ処理をした値です。この値は、データベースのauthuserテーブルのhashedpasswdフィールドに設定される値と同じものです。この値からパスワードそのものはハッシュ関数による処理値なので分かりませんが、他者に知られることによって、定義ファイルへなりすましてアクセスができるようになるため、知られては困る情報です。

 storingキーに設定できる文字列は、session-storage、cookie、cookie-domainwide、noneのいずれかです。noneだと保存をしないので、ページが切り替わると認証もやり直しが必要です。既定値、つまり無設定の場合はcookieが選択されます。cookieとcookie-domainwideはどちらもクッキーを利用して、authexpiredキーで指定した秒数の生存期間を持つクッキーを生成します。前者はページのディレクトリ、後者はページが含むドメイン全体にわたって認証情報が共有されます。authexpiredキーは省略すると、3600秒つまり1時間と成ります。この設定は、最後にアクセスしてからの時間であり、「何もしない時間がauthexpiredだけあればクッキー情報は消える」ということになります。

 クッキーやセッションストレージに記録される情報を表7-3-3にまとめました。なお、realmキーの値を設定すると、これらのキーの後にアンダーラインに続いて指定されます。realmが「Sample」なら、最初のキーは「_im_username_Sample」となります。異なるサイトで別々に認証したいのは当然なので、通常はサイトに固有のrealmの値を指定する必要があるでしょう。

キー名記録内容
_im_usernameログインしているユーザー名
_im_credentialパスワードにハッシュ処理をした値
_im_mediatoken画像等のメディア認証のための情報
_im_crypted暗号化したパスワード(ネイティブ認証、LDAP認証で使用)
表7-3-3 クッキーやセッションストレージに記録される情報

 クッキーでの記録はブラウザーを終了しても時間が来るまで残るので、その意味では便利です。期間を1週間くらいにしておくと、ほぼずっと認証が継続されるようになります。authexpiredキーの値を0にするとWebブラウザー終了時にクッキーが削除されます。しかしながら、クッキー自体がHTTPの通信でネットワーク上に送信されます。サーバー側ではその値は使用しないのですが、ネットワーク上に送信されるクッキー情報には注意を払う必要があります。したがって、クッキーを使用するときには、必ずTLSを使用してクライアントとサーバー間の通信を暗号化し、通信結果の傍受ができないようにしてください。

 session-storageの場合はネットワークに流れないので、クッキーより若干安全と言えなくもないですが、クッキーやセッションストレージに共通の脅威があります。それは、ログイン中のブラウザーを他人が使うことです。あるユーザーがログインをしてサイトを利用している時、PC/Macを操作できる状態で席を立つとします。すると、別の人が開いているブラウザーを使用して、元からログインをした人になりすますことができます。この問題は、すべての認証を伴うWebサイトに発生する問題点であり、INTER-Mediatorも例外ではありません。もちろん、クリックするたびにパスワード入力をすれば防げるとはいえ、それではシステムを利用する気にはなれないでしょう。この問題は、「ログアウトしない」という点に集約できます。利用者が席をはずすときには、ログアウト、あるいはOSの機能を利用してロックするのが現在のPC/Macの利用の上では原則です。INTER-Mediatorはその対策はできないですが、それは他のフレームワークやシステムも同様です。

認証の動作の設定

 通常は、authuserのusernameフィールドの値をユーザー名としますが、電子メールアドレスをユーザー名としたい場合もあります。その時は、オプション設定に、email-as-usernameキーと値を追加し、値はtrueにします。これで、authuserテーブルのemailフィールドの電子メールアドレスでも認証ができます。ただし、内部的には、usernameの値を使うので、usernameは一意な値を指定しておく必要があります。電子メールでの認証を可能にした場合、電子メールアドレスをusernameフィールドの値に変換するためにデータベース処理が増えます。SQLサーバーはその程度であればあまり問題にならないでしょうが、FileMakerの場合はパフォーマンスに影響が出る可能性があるので、慎重に導入しましょう。

 オプション設定にあるissuedhash-dsnキーには、issuedhashテーブルに対するDSNを別途指定することができます。例えば、FileMakerデータベースではissuedhashテーブルのやり取りに多数のデータベース処理を行うと目に見えてパフォーマンスが落ちます。そこで、issuedhashテーブルをSQLiteで運用することができるように、こうした設定を追加しました。issuedhashテーブルは独立して利用できるように設計されています。authuserテーブルなどはFileMaker側に持ち、issuedhashテーブルだけをSQLiteで運用します。データベースを保存したパスなどを含むDSNを値として指定します。なお、INTER-Mediatorに含まれているdist-docs/sample_schema_sqlite.txtを利用してデータベースファイルを作り、それを利用することで問題ありません。他のテーブルは無視されます。こうして作成したSQLiteのデータベースを/var/db/imに置いたとすれば、issuedhash-dsnキーの値としては以下のように指定して利用することができます。

リスト7-3-5 issuedhash-dsnキーの指定例
'issuedhash-dsn' => 'sqlite:/var/db/im/sample.sq3',

ネイティブ認証について

 ネイティブ認証は、データベースに記録されているユーザーを利用した認証です。MySQLやPostgreSQLではSQLコマンドを使って定義し、パスワードの設定を行います。FileMakerの場合は、メニューからグラフィカルユーザーインターフェースで追加などができます。これらのユーザーを使った場合には、アクセス権の設定を、スキーマレベルで定義して、データベースエンジン側の仕事とするか、あるいはauthgroup、authcorテーブルを用意して、INTER-Mediator側での仕事にするか、2通りの方法があります。

 ネイティブ認証の設定は、オプション設定のuserの設定に特別なキーワードを設定することで行えます。この設定があれば、authuserテーブルは使用しません。ログインパネルで入力したユーザー名とパスワードを利用して、データベースへのアクセスを行います。

リスト7-3-6 ネイティブ認証を行う場合の定義ファイルの記述
IM_Entry(
	array( ... ),	// コンテキスト定義
	array(		// オプション設定
		'authentication' => array(
			'user' => array('database_native'),
		),
	),
	array( 'db-class' => 'FileMaker_FX',),
	...
);

 ネイティブ認証では、クライアントからサーバーに対して、データベースアカウントのパスワードを伝達する必要があります。そこで、サーバー側に、非対称鍵を保持し、公開鍵をクライアントに送りそれを利用してパスワードを暗号化し、サーバーに伝達しています。この鍵は、OpenSSLがインストールされていればコマンドを入れれば生成できますが、サーバーを立てるごとに新たに生成して登録の必要があります。登録する場所はPHPのプログラムでの記述が必要なので、『8-2 PHPでの記述が必要な認証処理』で説明を行います。

ログインパネルとカスタマイズ

 INTER-Mediatorではログインパネルを自動的に生成します。どのようなログインパネルなのかはこの後の演習の画面ショット等で確認してください。ログインパネルのページがあるわけではなく、既存のページで認証が要求されると自動的にオブジェクトをページに追加して、ログインパネルを表示します。背景が薄く見えるようなスタイル付けをしています。

 なお、ログアウトは、ページネーションコントロールに表示されるので、ページネーションの表示の定義が行われていれば、ログアウトボタンもページ上に表示されます。あるいは、以下の1行のプログラムを実行するボタン等を追加してください。なお、この1行の実行にも、ページファイルを読み込むSCRIPTタグが必要です。

リスト7-3-7 ログアウトを行うJavaScriptのプログラム
INTERMediatorOnPage.logout();

 INTER-Mediator標準のログインパネルの動作等をカスタマイズするためのJavaScriptのAPIをリスト7-3-8に紹介します。いずれも、INTERMediatorOnPage.doBeforeConstructメソッド(『6-1 ブラウザーを判断するページ』を参照)内など、ページ合成の前に実行します。設定が全てプロパティとなっているので、代入文による利用がほとんどでしょう。

リスト7-3-8 標準のログインパネルのカスタマイズ
//認証の失敗回数の変更(既定値は4)
INTERMediatorOnPage.authCountLimit = 2;

//パスワード変更の機能をログインパネルに出さない
INTERMediatorOnPage.isShowChangePassword = false;

//ログインパネルのスタイル設定をしない
INTERMediatorOnPage.isSetDefaultStyle = false;

//ログインパネルのRealm名を置き換える
INTERMediatorOnPage.authPanelTitle = 'なんとかシステム';

//ログインパネル自体を独自のHTMLに置き換える
INTERMediatorOnPage.loginPanelHTML = "....";

 ログインパネルの各要素は、スタイルシートを利用して、スタイルの設定ができます。ただし、「INTERMediatorOnPage.isSetDefaultStyle = false;」を実行して、INTER-Mediator側のスタイル設定をしないようにします。そして表7-3-4に示す、クラスあるいはID属性の要素に対して、スタイル設定を行います。もちろん、一部の設定のみを変更しても構いません。

分類キーワードタグ設定対象
class_im_authpanelDIV認証パネル全体
_im_authlabelSPAN「ユーザー名」などのラベル
id_im_usernameINPUTユーザー名のテキストフィールド
_im_passwordINPUTパスワードのテキストフィールド
_im_authbuttonBUTTONログインボタン
_im_newpasswordINPUT新しいパスワードのテキストフィールド
_im_changebuttonBUTTONパスワード変更ボタン
表7-3-4 ログインパネルのスタイル設定のためのセレクタ

 ログインパネル自体を完全に置き換えるには、リスト7-3-8のリストの最後にあるINTERMediatorOnPage.loginPanelHTMLプロパティへ、HTMLで記述した文字列の代入を行います。ここでのHTMLは、HTMLタグから始まるものではなく、通常はDIVタグで開始することになると思われます。この時、HTML内では、表7-3-4の1列目がidの要素が含まれているようにします。例えば、「<input type="text" id="_im_username"/>」などの要素が含まれている必要があります。少なくとも、最初の3つは必要ですが、パスワードの変更が不要なら、最後の2つの要素は用意する必要はありません。また、FORMタグは必要ありません。

パスワードのリセット

 パスワードのリセットは、ログインパネルにその機能を含めてあります。この時、現在のパスワードを正しく入力しないと、パスワードの変更はできません。もし、独自にパスワード変更のページを作成したいとしたら、JavaScriptでリスト7-3-9のAPIを呼び出すことで可能です。

リスト7-3-9 パスワード変更のAPI
INTERMediator_DBAdapter.changePassowrd(ユーザー名, 旧パスワード, 新パスワード);

 なお、現在の自分のパスワードが分からなくなってしまった場合は、事前に登録してある電子メールアドレスへのメッセージを利用してパスワードをリセットする機能もINTER-Mediatorで利用できますが、PHPを利用したプログラミングが必要です。こちらについては、『8-2 PHPでの記述が必要な認証処理』で説明を行います。

演習認証の実現とパスワード変更

 この演習では、認証が必要なページを作成するための必要な記述と、実際の認証の動作を確認します。設定により、INTER-Mediatorによってログインパネルが表示されてログインができるようになります。また、ユーザーが自分自身のパスワードを変更する方法を説明します。

最初のページ用の定義ファイルに必要な設定を行う

1ここからの作業は、Webブラウザー上で行います。まず、INTER-MediatorがインストールされたVMを、VirtualBoxを使用して起動します(『1-2 演習を行うための準備』を参照)。続いて、ブラウザーで、「http://192.168.56.101」に接続します。「トライアル用のページファイルと定義ファイル」というタイトルの部分を特定します。
2「def24.phpを編集する」をクリックし、定義ファイルエディターでdef24.phpファイルを編集します。(もし、24番を他の用途に使ってしまっていれば、別の番号のファイルを利用してください。異なる番号のセットを利用した場合、ソースコードの記述が変わる部分がありますが、以下の手順では可能な限り注記します。)
3Contextsの中のQueryと書かれた背景がグレーの部分を特定します。そして、その次の行の右の方にある「削除」をクリックして、Queryの設定がある行を削除します。
4「レコードを本当に削除していいですか?」とたずねられるので、OKボタンをクリックします。
5同様に、Sortingの次の行にある「削除」ボタンを押し、確認にOKボタンをクリックして、こちらの設定も削除しておきます。
6Contextsでは、name、table、viewに「invoice」、keyに「id」、pagingに「true」repeat-controlに「confirm-insert confirm-delete」、recordsおよびmaxrecordsに「1000」と指定します。Contextsにあるその他のテキストフィールドは空白にします。この設定によりinvoiceテーブルを表示し、ページナビゲーションは表示しますが、ページの移動はおそらく発生しないくらいの1ページのサイズになります。
7Database Settingsに設定を行います。
[MySQL]の場合
db-classは「PDO」のままでかまいません。dsnに「mysql:host=localhost;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
[FileMaker]の場合
db-classを「FileMaker_FX」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「192.168.56.1」、portに「80」、protocolに「http」、datatypeに「FMPro12」と入力します。
8Debugについては、「false」にすると、デバッグ情報が出なくなります。なお、デバッグ情報をみながら動作を確認したい方は、「2」のままにしてこの後の作業を行ってください。

最初のページのページファイルの作成

1「http://192.168.56.101」で開いたページに戻り「page24.htmlを編集する」をクリックし、ページファイルのpage24.htmlを編集するページファイルエディターを開きます。HTMLでの記述内容を以下のように変更します。太字が追加する箇所を示します。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。番号にかかる部分は、SCRIPTタグのプログラムの内部にもあります。)
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <title></title>
  <script type="text/javascript" src="def24.php"></script>
</head>
<body>
<div id="IM_NAVIGATOR"></div>
<table>
  <tbody>
    <tr>
      <th>作成日</th>
      <td><input type="text" data-im="invoice@issued"/></td>
	<td></td>
    </tr>
    <tr>
      <th>タイトル</th>
      <td><input type="text" data-im="invoice@title"/></td>
      <td></td>
    </tr>
  </tbody>
</table>
</body>
</html>
2「http://192.168.56.101」で開いたページに戻り「page24.htmlを表示する」をクリックします。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)初期状態では3レコードが作られていますが、別の演習でレコードが増減しているかもしれません。ここでは内容はなんでも構いませんので、レコードが見えていることをまずは確認してください。

ページを開くのに認証を必要とするように変更する

1def24.phpを定義ファイルエディターで編集します。すでにタブあるはウインドウで見えている場合はそれを呼び出します。閉じてしまった場合には、「http://192.168.56.101」で開いたページに戻り「def24.phpを編集する」をクリックします。
2ページの最初にある「Show All」ボタンをクリックして、設定項目を全て表示します。そして、ページの最後の方に移動し、Optionsの中の「Authentication and Authorization」の「storing」に「session-storage」と入力します。Tabキー等をクリックして別のテキストフィールドに移動し、確実に設定を保存してください。
3page24.htmlを表示します。すでにタブあるはウインドウで見えている場合はそれを呼び出します。そして、ブラウザーの更新ボタンをクリックして、画面の更新を行ってください。閉じてしまった場合には、「http://192.168.56.101」で開いたページに戻り「page24.htmlを表示する」をクリックしてページを開きます。
4すると、ログインパネルが表示されます。このログインパネルは、INTER-Mediator標準のもので、フレームワークが生成しています。
5まずは、正しいユーザー名とパスワードを入力してみます。ユーザー名とパスワードいずれも「user1」と入力して、「ログイン」ボタンをクリックします。
6ページが表示され、データベースの内容が表示されています。見えている内容が、認証を必須にする前と同様であれば問題はありません。
ここではすでに認証に必要なテーブルが用意されていて、ユーザーも最初から設定されているので、ページの定義ファイルに認証が必須になるような設定を追加するだけで認証ができるようになっています。作業は簡単ですが、実際の開発ではテーブルを用意したりといった作業が発生する点は知っておきましょう。
7ページナビゲーションには、ログインしているユーザーとして「user1」が見えています。また、「ログアウト」ボタンも見えています。「ログアウト」ボタンをクリックして、ログアウトしておきます。

認証とパスワード変更の動作を確認する

1ログアウトすると、またログインパネルが表示されます。ここで、ユーザー名は「user1」として、パスワードに空欄あるいは「user1」以外の間違えたパスワードを入力して、「ログイン」ボタンをクリックします。赤い文字でエラーが表示されログインができず、ページの内容やデータベースのデータは表示されません。
2間違ったパスワードを何度も入力してみてください。5回目に認証エラーとなって、これ以上作業はできなくなります。パスワードを空欄で入力した場合はカウントされませんので、適当に間違えたパスワードを何か入力してください。
3ブラウザーの更新ボタンをクリックして画面を更新します。するとログインパネルがまた表示されます。続いて、パスワードの変更を行ってみます。
4ユーザー名に「user1」と入力します。パスワードは「user1」以外の文字列を入力します。つまり、パスワードは違っている状況であるとします。「新パスワード」に「1234」と入力します。「パスワード変更」ボタンをクリックします。すると、パスワードの変更に失敗したことがレポートされています。この状態ではパスワードは変わっていません。
5ユーザー名とパスワードに、いずれも「user1」と入力します。つまり、正しいユーザー名とパスワードを入力します。「新パスワード」に「1234」と入力します。パスワードはなんでも構いませんが、以下の手順をこの通り進めるために、ここでは「1234」と入力してください。そして「パスワード変更」ボタンをクリックします。パスワード変更が成功したメッセージが表示されます。

認証の継続の様子を確認する

1ユーザー名「user1」、パスワード「1234」でログインを行って、認証された状態でページを表示します。ここでページのウインドウあるいはタブを閉じます。
2「http://192.168.56.101」で開いたページで「page24.htmlを表示する」をクリックしてページを再度開きます。
3ログインパネルが表示されます。つまり、ウインドウを閉じる前の認証状態は、ここでは保持されなかったことになります。
storingの設定は、認証を継続させるための情報を残す手段を指定します。session-storageだと、ブラウザーに組み込まれたキー-バリュー形式のデータベースに記録しますが、記録はウインドウあるいはタブが存続している間だけ保持されるものです。
4def24.phpを定義ファイルエディターで編集します。すでにタブあるはウインドウで見えている場合はそれを呼び出します。閉じてしまった場合には、「http://192.168.56.101」で開いたページに戻り「def24.phpを編集する」をクリックします。
5ページの最初にある「Show All」ボタンをクリックして、設定項目を全て表示します。そして、ページの最後の方に移動し、Optionsの中の「Authentication and Authorization」の「storing」に「cookie-domainwide」と入力します。Tabキー等をクリックして別のテキストフィールドに移動し、確実に設定を保存してください。
6page24.htmlを表示します。すでにタブあるはウインドウで見えている場合はそれを呼び出ます。そして、ブラウザーの更新ボタンをクリックして、画面の更新を行ってください。閉じてしまった場合には、「http://192.168.56.101」で開いたページに戻り「page24.htmlを表示する」をクリックしてページを開きます。
7すると、ログインパネルが表示されます。正しいユーザーである「user1」およびパスワードの「1234」でログインを行います。もちろん、ログインはできます。
8ブラウザーを一度終了します。その後、page24.htmlを開きます。ウインドウに再度表示されないのなら、「http://192.168.56.101」のアドレスのページを開き、「page24.htmlを表示する」をクリックしてページを開きます。
9認証パネルは表示されずにページが表示されることを確認します。この場合、認証情報がクッキーに記憶され、ページのウインドウがなくなっても保持されるようになっています。そのため、次にページを開いたときには以前の認証結果が継続されます。

認証の継続の仕組みを確認する

1クッキーにどのような情報が記録されているかを確認してみます。以下の操作はChromeを使った場合で、その他のブラウザーでの参照方法はここの手順の後にまとめておきす。他のブラウザーでもクッキーの情報は参照できます。場合によってはクッキーの情報は、ここでの画面ショットを見るだけでも結構です。
2アドレスバーの右端の「三」のアイコンをクリックして、「設定」を選択すると、設定のページが表示されます。ページ末尾の「詳細設定を表示」のリンクをクリックします。その後に表示される「プライバシー」の見出しの「コンテンツの設定」をクリックします。
3「コンテンツの設定」パネルが表示されます。そこにある「すべてのCookieとサイトデータ」のボタンを表示します。
4「Cookieとサイトデータ」のパネルが表示されます。そこで、「192.168.56.101」のサイトのクッキーを探します。場合によっては右の方に検索窓を利用します。3つのクッキーが見えています。_im_で始まるこれらの3つのクッキーが認証で使用されます。ここまでの手順では、realmの設定を空欄にしましたが、realmに文字列を設定すると、ここでのクッキー名にrealmの文字列が追加されます。
5_im_usernameはユーザー名が設定されます。_im_cryptedは、ネイティブ認証やLDAP、OAuthでのキャッシュによる認証で使われ、パスワードを暗号化したものです。_im_credentialはパスワードに対してハッシュ関数で計算した結果が見えています。この値は、第三者に漏れるとそのユーザーとしてサーバーにアクセスできてしまう可能性があるものです。
6データベースに保存されているhashedpasswdフィールドの値を調べてみます。データベースエンジンによって操作が違います。
7[MySQLの場合]「http://192.168.56.101」のアドレスのページを開き、「ユーザー管理ページサンプル」をクリックして、ユーザー管理アプリケーションを起動します。ログパネルでは、ユーザー名に「user1」、パスワードに「1234」を入力して、認証を行います。
8[MySQLの場合]user1のHashed Passwordの列の値が、_im_credentialの値と同じになっています。つまり、データベースに記録した値と同じものをクライアントで記録しているということです。
9[FileMakerの場合]FileMaker Proで、Test_DBデータベースを開き、「authuser」レイアウトを開きます。そこで、該当するユーザーのhashedpasswdフィールドの値を確認してください。
セキュリティ上で考慮しなければならない点は、すでに説明しています。認証情報をクッキーに入れる場合にはクッキーのデータがHTTPのやり取りでネットワークを流れるので、少なくとも、TLSを利用して、通信を傍受されないようにすることは必須です。セッションストレージの場合も同様に記録されますが、その場合には_im_credentialの値はネットワークは流れません。

Chrome以外のブラウザーでのクッキーの表示方法

 [Firefoxの場合]
アドレスやツールアイコンがあるバーの右端にある「三」マークのアイコンをクリックし、パネルから「設定」を選択します。左側で「プライバシー」を選択し、「履歴」の中にある「Cookieを表示...」ボタンをクリックすると、サイトごとのクッキーを参照できます。

 [Internet Explorerの場合]
F12キーを押して開発者ツールを表示し、「コンソール」タブの画面のコマンド入力プロンプトで、「document.cookie」と入力してreturnキーを押します。

 [Safariの場合]
環境設定の「詳細」で、「メニューバーに“開発”メニューを表示」のチェックを入れます。クッキーを調べたいページが表示している状態で、「開発」メニューの「Webインスペクタを表示」を選択します。そして、「ストレージ」のタブを選択すると、左側にCookieや「セッションストレージ」の項目があるので、選択するとそれらを表示します。

演習のまとめ

このセクションのまとめ

 認証のための設定は、多くは定義ファイル上で行うことで実現可能です。むしろ、データベースにユーザーやグループのテーブルを用意する方がよほど手間がかかる仕事と言えるでしょう。このセクションでは認証を実現する方法やそこでのさまざまな設定、ログインパネルやそのカスタマイズなどを説明しました。しかしながら、認証を伴うアプリケーションはセキュリティ上の問題を抱えやすいシステムでもありますので、どういう原理で認証が稼働していて、やってはいけないことや、運用上どうしても必要になることに対してなぜそのような結果になっているのかをなるべく正しく理解をするようにしましょう。

7-4コンテキストにおけるアクセス制御

アクセス権の制御は、基本となる4つのデータ操作のCRUD(Create Read Update Delete)について、それぞれ、どのユーザーあるいはグループに対して許可しているのかをコンテキスト定義で規定します。操作ごとのアクセス権は、オプション設定では指定はできず、個々のコンテキスト定義で行います。

設定例によるアクセス権の設定

 前のセクションに続いて、用途に応じてどのような設定を行うのかを例で示します。以下の表記は定義ファイルでのPHPでの記述を行います。なお、いずれも、『7-2 ユーザー認証とアクセス権適用を行う仕組み』で説明したテーブルが用意されていて、ユーザーやグループが定義されていることを前提として説明します。また、設定の詳細は、『7-3 認証とアクセス権の設定』の最初の部分に表で示してあります。

読み出しはできて書き込みができないコンテキスト

 読み出しはできても書き込みを一切できないようにしたい場合、認証の設定を使わない方法もあります。リスト7-4-1のように、コンテキストのviewキーの値には存在するビューやテーブル名を指定し、tableキーの値には存在しないエンティティ名(ビューあるいはテーブルの名前)を指定します。authenticationキーの設定はありません。もちろん、その状態で書き込みをしてもエラーになるということで何も起こらないことを意図したものです。ユーザーインターフェースで書き込み可能なものを配置しないだけでは、定義ファイルへネットワークを通じて直接アクセスされた場合に書き込みや更新ができてしまう可能性があります。この設定は、そうした意図しないアクセスからも、書き込み処理を排除してデータを保護します。単に読み出しだけでいいようなコンテキストは認証の有無にかかわらずよくあり、安全面からも、なるべくtableキーの値に存在しないエンティティ名を指定するようにすべきです。また、認証がされていても書き込みを排除したい場合には、この手法は有効です。

リスト7-4-1 読み出しはできて書き込みができないコンテキスト定義
IM_Entry(
    array(
        array(
            'name' => 'person',
            'view' => 'person_view',	//データベースには存在する
            'table' => 'dummydummy',	//データベースには存在しない
            'records' => 1,
            'key' => 'id',
        ),
    ),
    array( ... ),
    ...
);

特定のユーザーが読み込みだけの権限でログインできる

 特定のコンテキストで、認証を必要とし、加えて書き込みができないようにするには、リスト7-4-2のような定義ファイルを作成します。この例では、コンテキストのtableキーに存在しないテーブル名を指定して、書き込み処理がエラーになるようにしています。そして、adminユーザーは認証して読み出しができますが、書き込みができるユーザーは誰もいないという状態になります。この時、この定義ファイルの他に、authenticationキーのないコンテキストがあれば、どのユーザーもログインを行うことなくデータベースを利用できます。

リスト7-4-2 特定のユーザーが読み込みだけの権限でログインできるコンテキストを含む定義ファイル
IM_Entry(
	array(	
		array(
			'name' => 'mycontext',
			'view' => 'mycontext',
			'table' => 'dummydummy',	//実在しないテーブル名
			'authentication' => array(
				'read' => array(
					'user' => array( 'admin' ),
				),
			),
		),
	),
	array(	/* 'authentication' キーはなし */ ),
	...
)

認証したユーザーに対して読み出しと更新のみを許可するコンテキスト

 データベースの4つの主要な処理であるCRUDそれぞれに対するアクセス権を、コンテキスト定義の中で設定することができます。この時、read、update、create、deleteの4つの操作をすべて記述してください。そして、その中で、userあるいはgroupキーで許可を与えるユーザーやグループを配列で与えます。それぞれの操作で一切許可しないものは、架空のグループを割り当てておけば良いでしょう。リスト7-4-3のコンテキストmycontextでは、adminsグループのユーザーであればログインをして、読み出しと更新処理はできますが、dummyグループは存在しないグループなので、レコード作成や削除ができなくなります。ここでは、viewとtableキーがないので、「mycontext」テーブルに読み書き処理が行われます。

リスト7-4-3 認証したユーザーに対して読み出しと更新のみを許可するコンテキストを含む定義ファイル
IM_Entry(
	array(	
		array(
			'name' => 'mycontext',
			'authentication' => array(
				'read' => array('group' => array( 'admins' ),),
				'update' => array('group' => array( 'admins' ),),
				'create' => array('group' => array( 'dummy' ),),
				'delete' => array('group' => array( 'dummy' ),),
			),
		),
	),
	array( /* 'authentication' キーはなし */ ),
	...
)

認証したユーザーに対して全ての操作を許可するコンテキスト

 CRUDのそれぞれの操作に対する設定が全て同じであるのなら、allキーを使って1行で指定が可能です。リスト7-4-4のコンテキストmycontextでは、adminsグループのユーザーであればログインをして、データベースの全ての処理ができます。

リスト7-4-4 認証したユーザーに対して全ての操作を許可するコンテキストを含む定義ファイル
IM_Entry(
	array(	
		array(
			'name' => 'mycontext',
			'authentication' => array(
				'all' => array('group' => array( 'admins' ),),
			),
		),
	),
	array( /* 'authentication' キーはなし */ ),
	...
)

コンテキストでのフィールド単位の制限

コンテキスト定義の中では、特定のフィールドに対する書き込みや読み出しできないようにする設定が可能です。リスト7-4-5のように、protect-writingあるいはprotect-readingというフィールドの設定ができます。例えば、外部キーフィールドや、次のセクションで説明するユーザー名のフィールドに対して更新できないようにしたり、検索やソートでは使うものの読み出しはできないようにしたいフィールドがあれば、その名前を配列で指定します。
リスト7-4-5 コンテキストでのフィールド単位の制限を含む定義
IM_Entry(
    array(
        array(
            'name' => 'person',	// view, tableは省略
            'records' => 1,
            'key' => 'id',
            'protect-writing' => array( 'id' ),
            'protect-reading' => array( 'username' ),
        ),
    ),
    array( ... ),
    array( ... ),
    false
);

演習コンテキストにアクセス権を設定する

コンテキストに対するアクセス権の設定を行ってみます。ここでの演習は、『7-3 認証とアクセス権の設定』の『認証の実現とパスワード変更』で作成したページを続けて利用します。データベースの4つの処理に対して、それぞれ可能なユーザーやグループの設定ができます。ここではグループの設定と、権限がない場合にはデータベース処理がなされないことを確認します。

アクセス権の設定と動作を確認する

1前のセクションの演習から続けて作業を行う場合には、いったんWebブラウザーを終了して、改めて起動し、「http://192.168.56.101」に接続してVMのホームを表示してください。
2def24.phpを定義ファイルエディターで編集します。すでにタブあるはウインドウで見えている場合はそれを呼び出します。閉じてしまった場合には、「http://192.168.56.101」で開いたページに戻り「def24.phpを編集する」をクリックします。
3ページの最初にある「Show All」ボタンをクリックして、設定項目を全て表示します。そして、コンテキストの途中にある「Authentication, Authorization and Security」で入力を行います。read:groupに「group2」、update:group、create:group、delete:groupに存在しないグループである「dummy」を指定します。 最後にTabキーを押して、4つのテキストフィールドに対応するフィールドの更新を確実に行うようにします。(read:groupは画面ショットではload:group、new:groupはcreate:groupに対応します)
ここでの設定は、このコンテキストからの読み出しはgroup2に対して許可するが、その他の更新や新規レコード、レコード削除はdummyグループに対してのみ許可することを意味します。しかしながら、dummyグループは存在しないので、すべてのユーザーは更新などの書き込み処理はできません。コンテキストのユーザーやグループの設定を行うときには、4つのデータベース処理のすべてに対して設定を行ってください。空欄にすると、設定がないものとみなしてしまいます。
4page24.htmlを表示します。すでにタブあるはウインドウで見えている場合はそれを呼び出ます。そして、ブラウザーの更新ボタンをクリックして、画面の更新を行ってください。閉じてしまった場合には、「http://192.168.56.101」で開いたページに戻り「page24.htmlを表示する」をクリックしてページを開きます。
5ログインパネルが表示されるので、ユーザー名とパスワードに「user2」と入力して、「ログイン」ボタンをクリックします。user2はgroup2には所属していないために、正しいパスワードを入力してもページの表示は行いません。つまり、アクセス権が設定されていないので、認証しても認証エラー扱いにして、実際のデータの表示をできないようにしています。
6group2に所属する「user4」でログインします。ユーザー名もパスワードも「user4」です。すると、データベースの内容を表示することができました。ログインユーザーがuser4であることを確認してください。
7ここで、適当なテキストフィールドのデータを変更してTabキーを押して、更新を行います。ここで、このコンテキストのupdate:groupに存在しないグループのdummyが設定されていることを思い出してください。
8すると、またログインパネルが表示されます。ここで、ユーザー名とパスワードに「user4」と入力して「ログイン」ボタンをクリックします。
9ページが表示されます。ここでは書き直したデータが見えていますが、このデータはデータベースへは書き込まれていません。それを確認しましょう。
10ブラウザーの更新ボタンをクリックして、画面を更新し、データベースの値を再度取り出してみます。すると、フィールドに見えている値は修正前の値になっています。したがって、実際には、データベースの更新はアクセス権がないために行われませんでした。
更新できない時の動作としてはあまりスマートな感じではありませんが、ここでの演習は、「コンテキストにアクセス権を設定すれば、更新が阻止できる」ことを示すためのものです。もし、あるデータベースフィールドの内容を表示しかしないのであれば、ここで作ったページのようにテキストフィールドを利用することはあり得ません。DIVやSPANタグで表示をすれば済みます。しかしながら、利用者がサーバー通信を独自に構築して更新をしようとしても、コンテキストの設定で阻止ができることをこの演習で確認していただきました。コンテキストの設定はサーバー側で記録されているので、コンテキストのアクセス権の設定はユーザーには変更できず、アクセス権の設定は設計者の意図通りに機能します。

レコード作成と削除の権限がない場合の動作

1def24.phpを定義ファイルエディターで編集します。すでにタブあるはウインドウで見えている場合はそれを呼び出します。閉じてしまった場合には、「http://192.168.56.101」で開いたページに戻り「def24.phpを編集する」をクリックします。
2ページの最初にある「Show All」ボタンをクリックして、設定項目を全て表示します。そして、コンテキストの途中にある「Authentication, Authorization and Security」にあるupdate:groupに「group2」を指定します。Tabキーを押して、テキストフィールドに対応するフィールドの更新を確実に行います。
3page24.htmlを表示します。すでにタブあるはウインドウで見えている場合はそれを呼び出ます。そして、ブラウザーの更新ボタンをクリックして、画面の更新を行ってください。閉じてしまった場合には、「http://192.168.56.101」で開いたページに戻り「page24.htmlを表示する」をクリックしてページを開きます。
4適当なフィールドの内容を変更してTabキーを押し、データの更新を行います。
5問題なく、更新ができました。今度は、group2に対して更新の権限が与えられているので、group2に所属するuser4での更新ができたということです。
6何からのレコードの「削除」ボタンをクリックして、レコード削除を試みます。確認のダイアログボックスでは、OKをクリックします。
7更新時と同様に新たに認証パネルが表示されました。user4でログインをしても、なんどやってもログインパネルが表示されます。
8ブラウザーの更新ボタンをクリックして、ページを再表示します。削除しようとしたレコードは削除されずに残っています。つまり、user4には削除の権限が与えられていないので、レコード削除ができないのです。
9ページネーションコントロールにある「レコード追加: invoice」をクリックして、レコードの追加を試みます。ダイアログボックスが表示されれば、OKをクリックします。以降、適当にダイアログボックスへ対処してください。
10ログインパネルが表示されます。user4で認証すると、また、ダイアログボックスが表示されます。これが繰り返されます。
11ブラウザーの更新ボタンをクリックしてみます。レコードが追加された形跡はありません。user4にはレコード作成の権限が与えられていないのです。
レコード作成や削除ができない時の動作としてはあまりスマートな感じではありませんが、ここでの演習は、「コンテキストにアクセス権を設定すれば、レコード作成や削除が阻止できる」ことを示すためのものです。もし、あるコンテキストに対してレコード作成や削除をしないのであれば、ここで作ったページのように作成や削除のボタンを表示することはあり得ません。しかしながら、利用者がサーバー通信を独自に構築してレコードの作成や削除をしようとしても、コンテキストの設定で阻止ができることをこの演習で確認していただきました。コンテキストの設定はサーバー側で記録されているので、コンテキストのアクセス権の設定はユーザーには変更できず、アクセス権の設定は設計者の意図通りに機能します。

演習のまとめ

このセクションのまとめ

 コンテキスト定義において、CRUDの各操作が可能なユーザーやグループの指定ができるので、特定のユーザーに対するアクセス権の付与が可能です。また、tableキーの値に存在しないテーブル名を与えて、書き込み処理を阻止するという手段も利用できます。

7-5レコード単位のアクセス権とメディアデータのアクセス権

同一のテーブルでレコードごとに違うユーザーに読み書き権限を与えて運用できます。そして、各レコードは他のユーザーからは操作できないように保護されています。また、写真のファイル等の読み出しにおいて、レコード単位のアクセス権を適用することもできるので、画像などもユーザーごとに保護した状態で参照ができます。

レコード単位のアクセス権

 同一のテーブルの個々のレコードに対してアクセス権を設定するためには、そのレコードが誰に対してアクセス権を持っているのかを何らかの方法で記録が必要です。そのために、そのテーブルに、ユーザー名あるいはグループ名を記憶するフィールドを設けて、そのフィールドに設定されているユーザーあるいはグループに対しての権限が与えられるような動作をフレームワークが行います。

 設定はコンテキスト定義のauthenticationキーの配列の中で、操作名をキーにした配列で、targetキーとfieldキーを指定します。targetキーは、「field-user」ならfieldキーで指定したフィールドにある名前のユーザーに対して権限が与えられます。targetキーの値が「field-group」ならfieldキーで指定したフィールドにある名前のグループに対して権限を与えます。targetキーが「table」あるいは省略の場合には、テーブル全体で同一のアクセス権の設定となります。

 fieldキーに指定するフィールドは文字型にします。そのフィールドには、authuserテーブルのusernameフィールドの文字列を入力します。主キーとなるidフィールドの値ではなく、ユーザー名の文字列を入力します。また、電子メールをユーザー名に使う場合でも、対応するusernameフィールドの値を設定します。

 クエリーやレコード削除、更新の場合の検索条件に、ANDで「fieldのフィールド名=ログイン中のユーザー名」という検索条件を付与します。したがって、全ての操作で、他のユーザーのレコードに対してデータの削除・更新が行われることはありません。また、新規にレコードを作成するときに、fieldで指定したフィールドにユーザー名を自動的に設定します。こうして、レコードの生成から更新、削除に至る全ての場面で、ログインしているユーザーに権限のあるレコードだけが処理対象になるということです。また、言い換えれば、他人のレコードを削除しようとしても認証しない限りはできないのです。ただし、このAND演算がポイントであるため、FileMakerの場合に検索条件をORで構成している場合には、レコード単位のアクセス権の設定は適用できません。

 リスト7-5-1はレコードごとのアクセス権を、chatというコンテキストに対して設定しています。オプション領域には、user1あるいはgroup2に所属するユーザーのみが、まず認証してログインできることが定義されています。その上でコンテキスト定義のauthenticationでは、全てのデータベースオペレーションに対して、userという名前のフィールドにユーザー名を記録して、各レコードはuserフィールドのユーザーだけが読み書きできるようになります。さらに、protect-writingキーで、userフィールドの更新を阻止します。この設定を行っても、新規レコードを作るときには、userフィールドにはログインしているユーザーのユーザー名が設定されます。

リスト7-5-1 レコードごとのアクセス権を設定した例
IM_Entry(
    array(
        array(
            'records' => 100000000,
            'name' => 'chat',
            'key' => 'id',
            'repeat-control' => 'delete',
            'authentication' => array(
                'all' => array(
                    'target' => 'field-user',
                    'field' => 'user',
                ),
            ),
            'protect-writing' => array( 'user' ),
        ),
    ),
    array(
        'authentication' => array( // オプション設定
            'user' => array('user1'), // ログイン可能なユーザー
            'group' => array('group2'), // ログイン可能なグループ
        ),
    ),
    array('db-class' => 'PDO'),
    false
);

データベース外のファイルに対して認証アクセスする

 データベースからデータを取り出すときに、認証を確認し、アクセス権を適用しますが、一方で、画像などのようにデータベースに入れない情報の取り出しについても認証やアクセス権を設定したい場合もあります。ここでは、画像ファイルなどのデータを「メディアデータ」と呼ぶことにします。

 メディアデータを送信するための準備としては、メディアデータのファイルを保存するディレクトリをWebサーバーの公開ディレクトリ外に用意して、ディレクトリやファイルに適切な、すなわちWebサーバーのプロセスが、読み出し可能なアクセス権を設定しておくことです。そして、そのディレクトリのパスを、オプション設定(IM_Entry関数の2つ目の引数)のmedia-root-dirキーの値に指定します。これは、Webサーバーのドキュメントディレクトリ外にします。ドキュメント内部では、場合によっては偶発的にパスが漏洩してしまって、認証せずに画像を見られてしまうかもしれません。

 画像などのメディアデータをWebブラウザーから参照させるには、定義ファイルへのアクセスを利用します。定義ファイル自体はINTER-Mediatorの本体を取り出したり、データベースアクセスを行って結果を送り返すなどさまざまな用途に利用しますが、URLのパラメーターにmedia=で値を指定すると、その値を相対パスとしたファイルの内容を取り出します。基準となるパスは、media-root-dirキーの値です。

 例えば、nameキーの値がcontextというコンテキストがあって、そのコンテキストのimagefileフィールドに、画像ファイル名が入力されているとします。そのコンテキストにおいて、認証が通るか、オプション設定にあるユーザーやグループでの認証が通った時に、画像に対しても認証が通ったクライアントからのリクエストのみ受け付けるには、まず、コンテキストをリスト7-5-2のように記述します。必要最低限の記述を示します。

リスト7-5-2 メディアデータの読み出しに認証を適用する定義ファイルの例
IM_Entry(
    array(
        array( //コンテキスト定義
            'name' => 'context',
            'key' => 'id',
            'authentication' => array(
               'media-handling' => true,
            ),
        ),
    ),
    array( // オプション設定
        'media-root-dir' => '/var/www/files',
        'authentication' => array(
        ),
    ),
    array('db-class' => 'PDO'),
    false
);

 media-handlingがあれば、このコンテキストに対して読み出しのリクエストがあった時、そのレスポンスに、ランダムなコードを返します。そのコードを、定義ファイルのアクセス時にクッキーとしてサーバーに送り、そのコードがサーバーによって発行されたものであるならば、認証されたものとみなします。サーバーとクライアントで認証情報をやり取りする方法はクッキーで固定されています。1回のレスポンスで返るコードは、複数のメディアデータへのアクセスに利用できるので、逆に言えば、セキュリティ面には注意が必要です。この方法でメディアデータに認証を設定するのであれば、TLS 1.2での通信回線上の暗号化が必要になります。

 リスト7-5-2の定義ファイルがdef.phpだとして、contextコンテキストのimagefileフィールドにパスが保存されていた場合のIMGタグの記述例はリスト7-5-3の通りです。例えば、あるレコードで、imagefileフィールドが「products/pencil.jpg」だったとします。すると、「def.php?media=products/pencil.jpg」というURLがsrc属性に設定され、その時に画像を取得するリクエストが定義ファイルに対してクライアントから送られてきます。media=とあるのでmedia-root-dirの値と合成され、サーバー上では「/var/www/files/products/pencil.jpg」という絶対パスのファイルを開き、ファイルの内容とともにMIMEタイプを適切設定されたレスポンスがクライアントに返されます。その結果、サーバー上にある画像ファイルの内容が、クライアントにも見えるということです。このリクエストを送る時、media-handlingキーが設定されているので、サーバーから得られたコードをクッキーに乗せてサーバーに送り込み、サーバー側では自分が発行したものかを確かめて、そうであるならファイルの内容を返します。関係ないコードの場合には何も返しません。

リスト7-5-3 画像ファイルを取り出すIMG要素の例
<img src="def.php?media=" data-im="context@imagefile@#src" />

 この章の最初に、ユーザー名やグループ名をフィールドに記述することで、そのレコードに対するアクセス権を指定したユーザーやグループにだけ許可する方法を『レコード単位のアクセス権』として説明しました。その特定のレコードに記録したパスのファイルを、特定のユーザーにだけ表示可能にすることもできます。なお、グループに対するアクセス権の設定はメディアデータではサポートしていません。

 まず、定義ファイルの例は、リスト7-5-4の通りです。オプション設定側には、メディアデータのルートを指定するmedia-root-dirに加えて、media-contextキーの値も指定します。このキーは、どのコンテキストで、ユーザー単位のアクセス権を適用するのかを指定します。ここでは、contextコンテキストに設定したいので、値は「context」となっています。コンテキスト定義にはauthenticationキーがあり、メディアデータに対する認証を行うので、media-handlingキーの指定があります。また、userフィールドに設定されたユーザーに対してレコードのアクセス権をCRUDのすべての操作に割り当てるようにしています。

リスト7-5-4 メディアデータの読み出しにレコードごとのアクセス権を適用する定義ファイルの例
IM_Entry(
    array(
        array(
            'name' => 'context',
            'key' => 'id',
            'authentication' => array(
                'media-handling' => true,
                'all' => array(
                    'target' => 'field-user',
                    'field' => 'user',
                ),
            ),
            'protect-writing' => array( 'user' ),
        ),
    ),
    array( // オプション設定
        'media-root-dir' => '/var/www/files',
        'media-context' => 'context',
        'authentication' => array(
        ),
    ),
    array('db-class' => 'PDO'),
    false
);

 そして、画像ファイルを取り出すIMG要素の例は、リスト7-5-5の通りです。レコード単位のアクセス権を設定するには、ファイル自体を規定されたディレクトリに入れる必要があります。例えば、ここでは、contextコンテキストのあるレコードについて、idフィールドは「120」、imagefileフィールドは「cutter.jpg」だったとします。以下のIMGタグの設定は、このファイルが「/var/www/files/context/id=120/cutter.jpg」である必要があります。つまり、media-root-dirと定義ファイルへのURLのmediaパラメーターの値を繋いだものがファイルのパスになるのは、単に認証を設定する場合と同じですが、そのファイルが、どのレコードに対するメディアデータなのかをパスで記録し、アクセス時には指定が必要です。つまり、「コンテキスト名/キーフィールド=その値」という相対パスを間に入れて、画像ファイル名を指定します。サーバー上でもこのようなディレクトリを用意して記録しておく必要があります。

リスト7-5-5 画像ファイルを取り出すIMG要素の例
<img src="defs.php?media=context/id=$/"
data-im="context@id@$src context@imagefile@#src" />

 ここでは、contextコンテキストは、すべて、フィールドuserにログインしているユーザーと同じユーザー名がある場合のみに、データベースに対する処理が許可されています。メディアデータの読み込みの場合も同様ですが、リクエスト自体からアクセス許可されているかどうかを判定します。前の例だと、id=120のcontextコンテキストのレコードのuserフィールドと、ログインユーザー名が同じかどうかを確認します。ここで、他のユーザーに対するアクセス権があるパスを指定しても、userフィールドの値とログインユーザーは異なるため、メディアデータへのアクセスは拒否され、メディアデータ自体がユーザーのアクセス権を適用されていることになります。

 このような「context/id=120/cutter.jpg」のようなパスを指定するのは厄介ではありますが、計算フィールド等を利用して、パスを生成してもいいでしょう。また、『5-4 JavaScriptコンポーネントの利用』で指定したファイルのアップロードコンポーネントは、ファイルを保存するときにコンテキスト名や主キーフィールドと値を絡めたパスを生成します。つまり、「自分でアップロードしたファイルは自分しか見えない」という動作を想定して機能を組み込んでいます。

演習レコード単位のアクセス権を設定する

 同一のテーブル内で、レコードごとに参照できるユーザーを切り替えたい場合があります。その時、ユーザー名やグループ名を入力する文字型フィールドを用意して、そのフィールドをログインしているユーザー名で自動的に絞り込むという動作で実現しています。レコードを新規作成するときに、そのフィールドに自動設定されることなども確認しましょう。

最初のページ用の定義ファイルに必要な設定を行う

1ここからの作業は、Webブラウザー上で行います。まず、INTER-MediatorがインストールされたVMを、VirtualBoxを使用して起動します(『1-2 演習を行うための準備』を参照)。続いて、ブラウザーで、「http://192.168.56.101」に接続します。「トライアル用のページファイルと定義ファイル」というタイトルの部分を特定します。
2「def25.phpを編集する」をクリックし、定義ファイルエディターでdef25.phpファイルを編集します。(もし、25番を他の用途に使ってしまっていれば、別の番号のファイルを利用してください。異なる番号のセットを利用した場合、ソースコードの記述が変わる部分がありますが、以下の手順では可能な限り注記します。)
3Contextsの中のQueryと書かれ背景がグレーの部分を特定します。そして、その次の行の右の方にある「削除」をクリックして、Queryの設定がある行を削除します。
4「レコードを本当に削除していいですか?」とたずねられるので、OKボタンをクリックします。
5同様に、Sortingの次の行にある「削除」ボタンを押し、確認にOKボタンをクリックして、こちらの設定も削除しておきます。
6Contextsでは、nameに「invoice-all」、table、viewに「invoice」、keyに「id」と指定します。Contextsにあるその他のテキストフィールドは空白にします。この設定によりinvoiceテーブルを表示します。単にすべてのレコードを一覧表示したいので、簡単な定義とします。
7Contextsの見出しの下にある「追加」ボタンをクリックして新たなコンテキスト定義を追加します。そして、name、table、viewに「invoice」、keyに「id」、repeat-controlに「delete insert」と指定します。Contextsにあるその他のテキストフィールドは空白にします。この設定によりinvoiceテーブルを表示し、レコードの追加や削除ができるようにします。
8定義ファイルエディターのページの冒頭にある「Show All」ボタンをクリックします。そして、nameが「invoice」の方のコンテキスト定義の中にある「Authentication, Authorizaton and Security」の見出しにあるall:targetを「field-user」、all:fieldを「authuser」とします。Tabキーを押すなどして、入力したフィールドから別のフィールドに移動して、確実に保存をしてください。
9Optionsの中にある「Authentication and Authorizaton」を探します。そして、storingに「session-storage」、realmに「Sample」と入力します。こちらも、Tabキーを押すなどして、入力したフィールドから別のフィールドに移動して、確実に保存をしてください。
10Database Settingsに設定を行います。
[MySQL]の場合
db-classは「PDO」のままでかまいません。dsnに「mysql:host=localhost;dbname=test_db;charset=utf8mb4」と入力します。そして、userに「web」、passwordに「password」と入力します。
[FileMaker]の場合
db-classを「FileMaker_FX」に書き換えます。databaseは「TestDB」、userに「web」、passwordに「password」、serverに「192.168.56.1」、portに「80」、protocolに「http」、datatypeに「FMPro12」と入力します。
11Debugについては、「2」のままにしてこの後の作業を行ってください。最後にデータベースとの通信結果を確認します。デバッグ情報を見ない場合には、ページの冒頭にある「clear」ボタンをクリックするなどして、適時画面から消しても構いません。

最初のページのページファイルの作成

1「http://192.168.56.101」で開いたページに戻り「page25.htmlを編集する」をクリックし、ページファイルのpage25.htmlを編集するページファイルエディターを開きます。HTMLでの記述内容を以下のように変更します。太字が追加する箇所を示します。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。番号にかかる部分は、SCRIPTタグのプログラムの内部にもあります。)
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title></title>
    <script type="text/javascript" src="def25.php"></script>
</head>
<body>
<div id="IM_NAVIGATOR"></div>
<table style="float: left">
  <thead>
    <tr><th>id</th><th>作成日</th><th>タイトル</th><th>ユーザー</th></tr>
  </thead>
  <tbody>
    <tr>
      <td data-im="invoice@id"></td>
      <td><input type="text" data-im="invoice@issued"/></td>
      <td><input type="text" data-im="invoice@title"/></td>
      <td data-im="invoice@authuser"></td>
    </tr>
  </tbody>
</table>

<table style="float: left; margin-left: 10px">
  <thead>
    <tr><th>id</th><th>作成日</th><th>タイトル</th><th>ユーザー</th></tr>
  </thead>
  <tbody>
    <tr>
      <td data-im="invoice-all@id"></td>
      <td data-im="invoice-all@issued"></td>
      <td data-im="invoice-all@title"></td>
      <td data-im="invoice-all@authuser"></td>
    </tr>
  </tbody>
</table>

  <br clear="all"/>
</body>
</html>
2「http://192.168.56.101」で開いたページに戻り「page25.htmlを表示する」をクリックします。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
3ログインパネルが表示されるので、ここではユーザー名、パスワードを「user2」としてログインを行います。
4ページが表示されました。authenticationのキーのある左側のinvoiceコンテキストはレコードが表示されていません。右側のinvoice-allのコンテキストは、invoiceテーブルのすべてのレコードを表示しています。ユーザー列は、authuserフィールドの値を示しており、おそらくすべてのレコードが空欄になっていると思われます。ページネーションコントロールでは、現在のログインユーザーを確認できます。

コンテキストにレコードを追加する

1左側のテーブルの下にある「追加」ボタンをクリックします。すると、左側にレコードが追加されます。作成日とタイトルを適当に入力します。作成日は、MySQLでは「2015-10-01」のように年月日、FileMakerでは「10/01/2015」のように、月日年でスラッシュ区切りとなるように入力します。
2ページネーションコントロールの「更新」ボタンをクリックして、ページ内容を更新します。左側には追加されたレコードが見えていますが、右側のテーブルにも見えるようになりました。
3左側で「追加」ボタンをクリックしてもうひとつレコードを追加します。そして、適当にフィールドに入力し、「更新」ボタンをクリックして、右側のテーブルも更新します。
右側のテーブルは、inoviceテーブルをすべて表示しています。これに対して、authenticationキーを設定した左側は、「ログインをしているユーザー名が、authuserテーブルに設定されているレコードのみ見えている」という状況になっています。
4ページネーションコントロールの「ログアウト」ボタンをクリックして、ログアウトします。
5ログインパネルが表示されるので、次は前と違うユーザーでログインをします。例えば、ユーザー名とパスワードに「user3」と入力して「ログイン」ボタンをクリックします。
6user3でログインをしました。まだ、authuserフィールドがuser3のレコードがないので、左側のテーブルにはレコードは表示されていません。
7「追加」ボタンをクリックします。左側の「ユーザー」列にはすでにuser3と見えており、ログインしているユーザーのユーザー名が自動的に設定されていることが分かります。
8フィールドに適当に入力し、ページを更新します。今度は、ログインしているuser3のレコードだけが左側で見えています。

データベースへのリクエスト内容を確認する

1ページを更新し、レコードが表示された状態で、デバッグログは消さずに、左側のテーブルの下にある「追加」ボタンをクリックした状態にして、データベースへ送信されたSQLステートメントあるいはリクエストのパラメーターを確認します。なお、以下の画面は一例です。異なるユーザーでログインしている場合にはその名前に入れ替わるなど状況によって異なるので、手元の結果を確認してください。
2まず、invoiceコンテキストのデータ取得部分を探します。ログの中に「クエリーアクセス: Accessing:/def25.php, Parameters:access=read&name=invoice…」と記載された部分を特定します。
3[MySQLの場合]その少し先に「SELECT * FROM invoice…」の部分をみてください。ここでは、ユーザー名がuser3であるユーザーのレコードに絞り込む検索条件が自動的に追加されています。
4[FileMakerの場合]その少し先にある「-db=TestDB…&-find」を特定します。ここでは、「authuser=user3」などとパラメーターが記述されており、authuserフィールドに対する検索条件が設定されています。
5まず、invoiceコンテキストにレコードを追加する部分を探します。ログの中に「新規レコードアクセス: Accessing:/def25.php, Parameters:access=create&name=invoice…」と記載された部分を特定します。(access=createの箇所は画面ショットではaccess=new)
6[MySQLの場合]少し先にINSERTステートメントがあり、初期値としてauthuserフィールドに対してログインしているユーザーのユーザー名が見えています。
7[FileMakerの場合]その少し先にある「…-db=TestDB…&-new」を特定します。ここでは、「authuser=user3」などと記述されており、ログインしているユーザーのユーザー名が、新規作成されたレコードのauthuserフィールドに自動的に設定されていることが分かります。

ユーザー名のフィールドの更新を阻止する

1def25.phpを定義ファイルエディターで編集します。すでにタブあるはウインドウで見えている場合はそれを呼び出します。閉じてしまった場合には、「http://192.168.56.101」で開いたページに戻り「def25.phpを編集する」をクリックします。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)「Show All」ボタンを押した状態でないのであれば、ページの冒頭にある「Show All」ボタンをクリックしてください。
2nameが「invoice」の方のコンテキストを特定します。「Authentication, Authorizaton and Security」の見出しにあるprotect-writingを「authuser」とします。Tabキーを押すなどして、入力したフィールドから別のフィールドに移動して、確実に保存をしてください。
3page25.htmlをページファイルエディターで編集します。すでにタブあるはウインドウで見えている場合はそれを呼び出します。閉じてしまった場合には、「http://192.168.56.101」で開いたページに戻り「page25.htmlを編集する」をクリックします。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
4以下のように太字の箇所を変更します。左側のテーブルの「ユーザー」列のフィールドを、ページ上で変更可能にします。
<body>
  <div id="IM_NAVIGATOR"></div>
<table style="float: left">
  <thead>
    <tr><th>id</th><th>作成日</th><th>タイトル</th><th>ユーザー</th></tr>
  </thead>
  <tbody>
    <tr>
      <td data-im="invoice@id"></td>
      <td><input type="text" data-im="invoice@issued"/></td>
      <td><input type="text" data-im="invoice@title"/></td>
      <td><input type="text" data-im="invoice@authuser"/></td>
    </tr>
  </tbody>
</table>
5page25.htmlがすでにタブあるはウインドウで見えている場合はそれを呼び出し、ブラウザーの更新ボタンをクリックして、ページを再度表示します。閉じてしまった場合には、「http://192.168.56.101」で開いたページに戻り「page25.htmlを表示する」をクリックします。(別の番号のセットで作業している場合には、該当する番号のリンクをクリックしてください。)
6左側のテーブルの「ユーザー」の列の適当なテキストフィールドの内容を書き換えてみます。
7Tabキーを押して更新処理をしますが、「No data to upate.」というエラーが表示されています。
8ページを更新してください。書き直したフィールドは、データベース上では更新されていません。
テキストフィールドでなければ書き直しはできませんが、それでも、定義ファイルへのアクセスを独自に記述して直接行うことで書き直してしまうことは、技術的には不可能ではありません(もちろん、プログラムを自分で作ることになります)。しかしながら、protect-writingの設定があれば、更新可能なコンテキストでも指定したフィールドは更新ができなくなります。
9「追加」ボタンをクリックして、レコードを追加してみます。レコードの追加はエラーなく行え、ユーザー列には現在ログインしているユーザーが見えています。

演習のまとめ

このセクションのまとめ

 ユーザー単位あるいはグループ単位にアクセス権を設定するテーブルの運用も可能です。ユーザー名やグループ名を記録するフィールドを用意し、定義ファイルのコンテキスト定義で、必要な設定を行います。メディアデータについても認証をかけることができます。なお、ユーザーによる制限のみがメディアデータに対しては可能です。