MSDN マガジン > Home > 発行物 > 2008 > September >  Cutting Edge: 安全な AJAX サービス層を構築する
Cutting Edge
安全な AJAX サービス層を構築する
Dino Esposito
AJAX および Silverlight™ アプリケーションの主な利点には、バックエンド サービスとの透過的で連続的な対話が挙げられます。問題は、セキュリティを考慮して設計されていない HTTP 上でそれらが実行されることです。承認されないアクセスからバックエンド サービスを保護することは、依然として重要です。では、どうすればよいでしょうか。
図 1 に、一般的な AJAX または Silverlight アプリケーションのアークテクチャを示します。これは、フロントエンドとバックエンドがインターネットで分離された本質的な 2 層アークテクチャです。このアーキテクチャの結果として、プレゼンテーション層とバックエンドの間に承認されないアクセスが入り込む余地があります。
図 1 リッチ Web クライアント アプリケーションの一般的なアーキテクチャ (クリックすると拡大画像が表示されます)
バックエンドは、パブリック URL の集合を介して API を公開します。これらの URL は、技術的にアプリケーションの一部でありパブリック サービスとして公開されているホストされるサービスを参照します。ファイアウォールなどの高度なセキュリティ バリアを配置した場合は、承認されない呼び出しだけでなく承認された呼び出しも停止されます。不要なアクセスを完全に取り除くことは非常に困難になります。当然ながら、イントラネットまたはエクストラネットのシナリオでは、この処理ははるかに簡単になります。
このような Web ベースの分散アーキテクチャを利用することを選択した場合は、部外者によるサービスの検索と悪用の可能性を考慮する必要があります。

サービス層
新しい分散システムには、プレゼンテーション層とビジネス層の間に、サービス層と呼ばれる追加のコード層があります。Martin Fowler の著書『エンタープライズ アプリケーション アーキテクチャ パターン』(翔泳社、2005 年) で明らかにされたサービス層は、分散アプリケーションの他の 2 つの層 (この場合は、プレゼンテーション層とビジネス層) の間の境界を構築する追加のコード層を表します。
プレゼンテーション層のコードは、一般にユース ケースを実装します。一般的なユース ケースはユーザーが実行する一連の操作であり、1 つ以上のビジネス オブジェクト、ワークフロー、およびサービスの間で対話が開始されます。サービス層では、きめの粗いサービスを通じて公開される中間 API を使用して、それらの細かい対話を抽象化できます。プレゼンテーション層では、この層の 1 つのサービスに対して 1 回の呼び出しを行います。呼び出されたサービス層のメソッドは、必要な動作を実装するためにビジネス層のオブジェクトとワークフローを調整します。図 2 に、サービス層の利点を示します。
図 2 サービス層がプレゼンテーション層とビジネス層を分離する (クリックすると拡大画像が表示されます)
サービス層の実装では、通常は呼び出し元が事前に認証および承認されているものと見なします。また、通常はファイアウォールの内側などのネットワークの分離された場所にサービス層を配置します。クライアントが ASP.NET アプリケーションの場合、サービス層はページ コード内部から呼び出されます。ただし、このコードは Web サーバー上で実行するため、サービスへの呼び出しは本質的にサーバーからサーバーへの呼び出しになり、保護と制御を備えた環境で実行されます。このシナリオでは、セキュリティの新たな考慮事項は発生しません。
ASP.NET AJAX または Silverlight シナリオでのサービス層を考えてみます。AJAX または Silverlight クライアントによるサービス層への呼び出しを可能にするため、サービス層のエンドポイントのアドレスをパブリックに公開する必要があります。これは、中間に配置するファイアウォールを外すことを意味します。
パブリックに接続するリッチ Web クライアントからは、パブリックに公開された機能のみを呼び出すことができます。バックエンド サービス層を保護するために、ASP.NET AJAX または Silverlight クライアントで目的の機能の簡易版を実装できます。これにより、たとえば、Silverlight クライアントで保留中の注文の表示 (重要でない操作) はできますが、新規注文の発注 (重要な操作) は許可されません。しかし、最終的に完全な機能が必要な場合は、セキュリティ対策を行う必要があります。

AJAX サービス層を実装する
筆者が AJAX サービス層と呼んでいる追加のサービス層では、前述のいくつかの承認されないアクセスを防止できます。この AJAX サービス層は従来のサービス層の前面に位置し、AJAX または Silverlight のプレゼンテーション層と残りのバックエンド システムとの間の通信を仲介します。AJAX サービス層ではサービスの URL がパブリックになり、見つかったサービスはだれもが使用できます。アプリケーションとこの層の間の通信は簡単で、ブロックされることはありません。
筆者がお勧めする AJAX サービス層ではアプリケーション ロジックを実装しません。この層はバックエンド システムに対する AJAX 固有のゲートウェイとして動作し、その唯一の目的はセキュリティの実装です。ただし、それと同時に、AJAX 固有のセキュリティ コードをビジネス サービスとアプリケーション サービスから保護します。
AJAX サービス層は、ユーザー インターフェイスから呼び出される ASMX Web サービス、WCF (Windows® Communication Foundation)、または REST (representational state transfer) サービスから構成されます。これらのサービスは 2 つの操作を実行します。まず、操作の要求元であるユーザーが操作の実行に十分なアクセス許可を持っているかどうかを検証します。一般に、メンバシップとロールがチェックされます。すべてのセキュリティ チェックに合格すると、AJAX サービス層のサービスは実際のアプリケーション サービス層に呼び出しを転送します。図 3 に、AJAX サービス層とアプリケーション サービス層への接続を示します。
図 3 AJAX サービス層が承認されない呼び出しからサービス層を分離する (クリックすると拡大画像が表示されます)
AJAX サービス層を通過するすべての呼び出しは安全と見なされ、コア システムへのアクセスが許可されます。AJAX サービス層とサービス層の間の通信では、ファイアウォール、IPsec、証明書などの考えられる限りの手段を使ってセキュリティをさらに強化することができます。
AJAX サービス層には、ASP.NET AJAX または Silverlight 2 アプリケーション内部から直接呼び出されるサービスが含まれています。そのため、直接 URL (Silverlight 2 の WebClient クラスなど) または JavaScript プロキシを通じて呼び出されるローカルの、ドメインが同じサービスから構成されています。ユーザー インターフェイスが一連のサードパーティ製のコントロールに基づいている場合は、(ページングや埋め込み先編集などの目的で) これらのコントロールのメンバにバインドするサービスを AJAX サービス層の一部として保護する必要があります。
しかし、セキュリティの実装にはコストがかかることを忘れないでください。特定のサービスでセキュリティ保護を必要とする操作が実際に実行されない場合は、保護しないでおく必要がある場合があります。これは許容できる選択肢であり、多数の Web サイトで採用されています。たとえば、オートコンプリート Web サービスには隠すものがほとんどありません。オートコンプリート機能を使用する Web サイトは、修正候補一覧を取得するために Web サービスに接続します。これはサーバーのデータを変更したり企業秘密を開示したりすることのない読み取り専用の操作です。セキュリティを保護しなくても、リスクはほとんどありません。
当然ながら、外部者によるシステムへの侵入を許せば、潜在的な未知のセキュリティ ホールを悪用される可能性が生じます。AJAX ではすべての種類のクライアントからの呼び出しを取得することが必要ですが、リスクのレベルを認識することも不可欠です。常に、さまざまなサービスを異なるサブドメインに配置することができます。

サービス呼び出しのための組み込みの保護機能
ASP.NET Web サービスと WCF サービスのエンドポイントでは、組み込みのセキュリティ バリアを自動的に利用できます。既定では、ASMX Web サービスの WebMethod を HTTP GET 呼び出しから呼び出すことはできません。さらに、POST 要求はコンテンツ タイプが application/json という特定の文字列に設定されている場合にのみ動作します。この単純なメカニズムによって、スクリプト インジェクション攻撃で生成された呼び出しが防止されます。ページに悪意のあるスクリプトが挿入されると、URL は <script> タグを通じてのみ呼び出せるようになります。<script> タグはクロスドメイン URL にもアクセスできますが、HTTP GET を使用する必要があり、ヘッダーまたはコンテンツ タイプを設定することはできません。同時に、ローカル ブラウザの JavaScript エンジン内部から実行を試みるダウンロードされたスクリプト コードはサンドボックス化され、ローカルまたはリモートのいずれの URL にもアクセスできません。
既定では、ASP.NET AJAX または Silverlight クライアントは HTTP POST を使用して WCF サービスを呼び出します。HTTP GET を有効にする (および URL の形式を変更する) には、サービスを適切に構成する必要があります。この場合の既定のコンテンツ タイプも application/json に設定されます。
これらを考慮しても、まだ心配でしょうか。もちろんです。
ターゲット サービスの URL または静的 IP アドレスが検出されたら、ハッカーにとって呼び出しを再生するのは比較的たやすいことです。前述のバリアではブラウザ (常に GET を使用するブラウザ) を使用してサービスへのアクセスを防止できますが、方法は他にもあります。どのような方法でしょうか。きわめて単純ですが、彼らはブラウザからサービスへの通信をかぎつけます。そして、すべての詳細が揃ったところで、.NET スマート クライアントを使用して呼び出しを準備します。図 4 は、この点を示す呼び出しです。
void Button1_Click(object sender, EventArgs e)
{
    string url = "http://contoso.com/services/service.asmx/GetCompletionList";
    WebRequest request = WebRequest.Create(url);
    request.Method = "POST";
    request.ContentType = "application/json";
    Stream stmReq = request.GetRequestStream();
    StreamWriter writer = new StreamWriter(stmReq);
    writer.Write(@"{""prefixText"":""Lon"",""count"":5}");
    writer.Close();

    WebResponse response = request.GetResponse();
    Stream stmResp = response.GetResponseStream();
    StreamReader reader = new StreamReader(stmResp);
    string text = reader.ReadToEnd();

    reader.Close();
    stmResp.Close();
}
コードでは、Windows フォーム アプリケーションの System.Net 名前空間の WebRequest および WebResponse クラスを使用して、直接呼び出しています。アプリケーションが完全な信頼で実行するため、制限は適用されず、URL と呼び出しの詳細があればだれでもリモートでサービスを呼び出すことができます。

AJAX サービス層のセキュリティを実装する
AJAX サービス層を効率的に保護する方法を理解するために、図 3 をもう一度見てください。正規ユーザーおよび部外者という 2 つのユーザー グループが、AJAX サービス層のサービスを呼び出すことができます。正規ユーザーは、ASP.NET AJAX であれ Silverlight 2 であれ標準の Web フロントエンドを介して接続します。部外者は利用できる任意のプラットフォームを使って URL にアクセスしますが、通常はカスタムの完全信頼アプリケーションです。
正規ユーザーからのアクセスを許可し、悪意のあるユーザーを拒否するには、正規ユーザーのみが簡単に指定できる情報を特定する必要があります。図 5 に示すように、その特殊な情報とは認証 Cookie です。
図 5 正規ユーザーはログイン インターフェイスの通過後に接続する (クリックすると拡大画像が表示されます)
重要なのは、機密性を持つサービス メソッドを呼び出す必要のあるすべての機能を、保護されたユーザー インターフェイス要素 (この場合は ASP.NET ページ) に分離することです。この機能を呼び出す必要があるすべてのユーザーが最初にログイン ページを通過するように要求することで、そのユーザーの後続のすべての要求がアプリケーションのメンバシップ システムで発行される認証 Cookie を必ず保持するようにします。
ログイン ページはユーザーから資格情報を取得し、ページの閲覧を承認されたユーザーであるかどうかを検証します。すべてに問題がなければ、要求が承認され、認証 Cookie が生成されて応答にアタッチされます。それ以降は、サービス要求を含め、ユーザーがアプリケーションに対して行う要求で Cookie が保持されます。その結果、AJAX サービス層で呼び出されたサービスは認証 Cookie をチェックして、その要求が正規ユーザーによって行われたものであることを確認します。
ASP.NET AJAX または Silverlight 2 アプリケーションのセキュリティに関する操作を実行するページは、Web サイトの保護領域に配置する必要があります。特殊な領域へのアクセスをフィルタリングするには、web.config ファイルに以下の構成を追加します。
<location path="MembersOnly">
    <system.web>
        <authorization>
            <deny users="?" />
        </authorization>
    </system.web>
</location>
ここでは、MembersOnly フォルダのコンテンツが認証済みのユーザーに制限されています。最後に、認証を有効にする必要があります。この例では、以下のように ASP.NET フォーム認証を有効にしています。
<authentication mode="Forms">
    <forms loginUrl="MembersOnly/login.aspx">
    </forms>
</authentication>
確かに、これは格別に難しいことではありません。
ユーザーがログイン ページを正常に通過すると、有効な認証 Cookie が応答にアタッチされます。この Cookie は期限切れになるまで後続の要求に使用されます。
この方法が ASP.NET AJAX および Silverlight 2 の両方で動作するのは興味深いことです。Silverlight プラグインは、実際には、ブラウザの基となるインフラストラクチャ (これは、プラグインにのみ見えます) に依存してリモート呼び出しを実行します。これにより、認証 Cookie (およびその他の Cookie) が要求に必ず追加されます。

サービスからユーザー情報を取得する
アプリケーションが発行した認証 Cookie は Web サービスまたは WCF サービスに渡されます。しかし、サービスではどのようにしてこの情報にアクセスできるでしょうか。まずは、ASMX Web サービスから見ていきます。
ASMX Web サービスは常に ASP.NET ワーカー プロセスでホストされ、処理中の ASP.NET 要求の HTTP コンテキストにアクセスすることができます。System.Web.Services 名前空間に定義された WebService クラスから Web サービス クラスを派生している場合、クラスは Context プロパティを継承し、Session、Request、Response、そして特に User などの、すべてのプロパティにアクセスすることができます。WebService からのクラスの派生を選択しなかった場合は、HttpContext.Current プロパティを使用して、同じ ASP.NET 組み込みオブジェクトに引き続きアクセスすることができます。いずれの場合も、ASMX Web サービスでは常に ASP.NET 要求コンテキストが使用可能です。
WCF サービスの場合は、状況が異なります。WCF サービスは IIS でのホスティングが可能であり、ASP.NET との side-by-side または互換モードのどちらかになります。既定の構成は ASP.NET との side-by-side です。
WCF サービスの要求を受信する ASP.NET ランタイムは、これらの要求の処理に関与しません。ワーカー プロセス内で WCF ランタイムが受信要求を検出し、WCF スタックを介してこれらの要求を処理します。
ASP.NET と WCF の side-by-side 実行では、いくつかの副作用が生じます。たとえば、SVC リソースに対するアクセス制御リストを定義できません。ここでさらに重要なのは、WCF サービス内からアクセスした場合に HttpContext.Current プロパティで常に null が返される点です。そのため、WCF サービスが ASP.NET ログイン ユーザーに関する情報を取得する手段がありません。
通常は、WCF モデルはホスティング環境や転送メカニズムにかかわらず動作するように設計されています。しかし、AJAX サービス層に対するセキュリティでは、WCF とホスト ASP.NET 環境の間でより厳密な共同作業が要求されます。WCF の ASP.NET 互換モードは、この問題に対処します。
互換モードでの実行中、WCF サービスは ASP.NET 要求のライフサイクルに完全に関与しています。実質的な効果として、WCF サービスは ASMX Web サービスと同じ情報にアクセスします。特に、ファイルベースの承認がサポートされ、さらに重要なことには、HttpContext.Current が正しく設定されます。
互換モードは、アプリケーション レベルで有効にされるグローバル設定です。ただし、個々のサービスでこのモードを拒否または許可できます。互換モードを有効にするには、構成ファイルに以下の行を入力する必要があります。
<system.serviceModel>  
  <serviceHostingEnvironment 
     aspNetCompatibilityEnabled="true" />    
</system.serviceModel>
個々のサービスでは、AspNetCompatibilityRequirements 属性の RequirementsMode プロパティで互換モードのサポートを宣言します。これはコントラクトではなくサービス クラスで設定されます。プロパティの値は Required、Allowed、および NotAllowed です。既定値は NotAllowed であるため、AJAX サービス層のそれぞれの WCF サービスで RequirementsMode プロパティを Allowed または Required のどちらかに変更する必要があります。
[AspNetCompatibilityRequirements(
    RequirementsMode = AspNetCompatibilityRequirementsMode.Required)] 
public class TimeService : ITimeService
{
   :
}
この情報に基づいて、AJAX サービス層内部のサービスの重要なメソッドごとに、ログインしたユーザーの ID をチェックするコードを実行する必要があります。その例を次に示します。
public class TimeService : ITimeService
{
   public DateTime GetTime()
   {
      CheckIdentity();
      ...
   }
}
CheckIdentity 関数は、認証 Cookie によって伝達され、ASP.NET パイプラインによって解決されたユーザーの ID を読み取る任意の関数です。
private void CheckIdentity()
{
  string user = HttpContext.Current.User.Identity.Name;
  if (AuthenticationHelper.VerifyUser(user))
      return;

  throw new InvalidOperationException("Invalid credentials");
}
ここでは、使用可能なユーザー名がない場合、またはユーザー名が承認済みユーザーの一覧に含まれていない場合に、CheckIdentity が例外をスローするように設計されています。メソッドがブール値を返し、サービス メソッドがその後の動作を決定するように実装を変更することも可能です。
このように設計すると、ログイン ページを通過せずにサービスを呼び出す部外者は、認証 Cookie が欠けている HTTP パケットを送信するようになります。結果として、HTTP コンテキストの User オブジェクトが null になり、CheckIdentity が例外をスローします。この方法はどのくらい安全でしょうか。これは、ASP.NET プラットフォームの柱の 1 つであるフォーム認証自体と同様に安全です。部外者に残された唯一の方法は、認証 Cookie を盗んで期限が切れる前に使うことです。
ASMX および WCF サービスは、ASP.NET HTTP コンテキストに格納されている情報を利用します。AJAX サービス層の REST サービスについてはどうでしょうか。Microsoft® .NET Framework 3.5 の WCF Web プログラミング モデルを使って REST サービスを実装した場合は、WCF 互換モードでカバーされます。しかし、単に URL を呼び出し、従来のプレーン XML または JSON (JavaScript Object Notation) データを返してカスタム サービスを作成する場合は、サービス エンドポイントは単に ASP.NET エンドポイントであり、HTTP コンテキストに完全にアクセスできます。

まとめ
AJAX サービス層では、クライアントのプラットフォームにかかわらず、ASP.NET AJAX であれ Silverlight 2 であれ、この記事に示す方法でサービスのセキュリティを保護できます。AJAX サービス層には、リッチ Web クライアントがタスクを完了するために呼び出す必要のあるすべてのパブリック エンドポイントが含まれています。これらのサービスは、アプリケーションが有効な間に不可欠な役割を果たしますが、パブリック エンドポイントです。このため、部外者がこれらのサービスを呼び出すことも可能であり、本質的なセキュリティの危機が生じます。
これらのサービスを保護するため、ユーザーがログイン ページを通過するように要求し、ASP.NET の認証 Cookie を要求にアタッチして後続の要求で使用できるようにすることが可能です。Cookie を使用すると正規ユーザーと部外者を区別できます。パブリック エンドポイントを回避することはできませんが、承認済みユーザーを認識し、それ以外のユーザーからサイトを守る方法が必要です。

ご意見やご感想は、cutting@microsoft.com まででお送りください。

Dino Esposito は、IDesign 社のアーキテクトであり、『プログラミング Microsof ASP.NET 3.5 Core Reference』(日経 BP ソフトプレス、2008 年) の著者です。Dino はイタリアに在住し、世界各国で開催される業界のイベントで頻繁に講演しています。ブログは weblogs.asp.net/despos で読むことができます。

Page view tracker