.NET を使用したモジュールの開発

公開日: 2007 年 11 月 24 日 (作業者: saad (英語))

更新日: 2008 年 3 月 7 日 (作業者: saad (英語))

はじめに

IIS 7 では 2 つの方法で開発されたモジュールによってサーバーを拡張できます。

  • マネージ コードと ASP.NET サーバー拡張 API を使用する方法。
  • ネイティブ コードと IIS 7 ネイティブ サーバー拡張 API を使用する方法。

以前は、ASP.NET 要求処理パイプラインがメインのサーバー要求パイプラインから独立していたため、ASP.NET モジュールの機能が制限されていました。

IIS 7 では、統合パイプライン アーキテクチャによって、マネージ モジュールはネイティブ モジュールとほぼ同等の機能性が備わっています。最も重要な点は、マネージ モジュールが提供するサービスを、ASPX ページのような ASP.NET コンテンツに対する要求だけではなく、サーバーに対する要求すべてに適用できるようになったことです。マネージ モジュールの構成方法や管理方法は、ネイティブ モジュールの場合と同様です。処理段階やその順序についてもネイティブ モジュールと同様に実行できます。また、マネージ モジュールは、追加および強化されたいくつかの ASP.NET API を使用して、要求処理のさまざま操作を行うことができます。

この記事では、任意の資格情報ストア (ASP.NET 2.0 メンバーシップ システムのプロバイダー ベースの資格情報インフラストラクチャなど) で基本認証を実行する機能を追加するために、マネージ モジュールを使用してサーバーを拡張する方法を紹介します。

これによって、Windows 資格情報ストアに関連付けられている、IIS 7 の組み込みの基本認証のサポートが、任意の資格情報ストアまたは ASP.NET に付属する既存のメンバーシップ プロバイダーのいずれか (SQL Server、SQL Express、Active Directory など) をサポートするものに置き換えられます。

この記事では、次のようなタスクについて説明します。

  • ASP.NET API を使用したマネージ モジュールの開発
  • サーバーでのマネージ モジュールの展開

IIS 7 のモジュールおよびハンドラーの開発の基礎については、「.NET Framework による IIS 7 のモジュールおよびハンドラーの開発 (英語)」を参照してください。

ブログ http://www.mvolo.com/ (英語) には、IIS 7 モジュールの作成に関するリソースやヒントが多数用意されているほか、アプリケーションで使用できる既存の IIS 7 モジュールをダウンロードすることもできます。サンプルについては、「HttpRedirection モジュールを使用して要求をアプリケーションにリダイレクトする (英語)」、「DirectoryListingModule を使用して IIS Web サイトの見やすいディレクトリ一覧を表示する (英語)」、「IconHandler を使用して ASP.NET アプリケーションで見栄えのよいファイル アイコンを表示する (英語)」、および「IIS と ASP.NET によるホット リンクの阻止 (英語)」を参照してください。

: この記事で紹介するコードは C# で記述されています。

必要条件

このドキュメントの手順を実行するには、次の IIS の機能がインストールされている必要があります。

ASP.NET

Windows Vista のコントロール パネルから ASP.NET をインストールします。[プログラムと機能]、[Windows の機能の有効化または無効化] の順にクリックします。[Internet Information Services]、[World Wide Web サービス]、[アプリケーション開発機能] の順に開き、[ASP.NET] チェック ボックスをオンにします。

Windows Server® 2008 の場合は、[サーバー マネージャー]、[役割] の順に開き、[Web サーバー (IIS)] をクリックします。[役割サービスの追加] をクリックします。[アプリケーション開発] の [ASP.NET] チェック ボックスをオンにします。

基本認証の背景情報

基本認証は、HTTP 1.1 プロトコル (RFC 2617 (英語)) で定義されている認証スキームです。基本認証では、大まかには次のように動作する、標準的なチャレンジベースのメカニズムを使用します。

  • ブラウザーは資格情報を使用せずに URL に対して要求を行います。
  • サーバーでその URL の認証が必要である場合、サーバーは 401 アクセス拒否メッセージで応答し、基本認証スキームがサポートされていることを示すヘッダーを含めます。
  • ブラウザーは応答を受信し、ユーザー名/パスワードの入力をユーザーに求めます (そのように構成されている場合)。入力されたユーザー名/パスワードは、その URL の次回要求時に、プレーン テキストで要求ヘッダーに含められます。
  • サーバーは受信したヘッダーに含まれているユーザー名/パスワードを使用して、認証を行います。

: この記事では、この認証プロトコルについては詳しく説明しません。基本認証スキームではユーザー名とパスワードがプレーン テキストで送信されるので、SSL を使用してセキュリティで保護する必要があることに注意してください。

IIS 7 および IIS 7 以前のリリースでは、ローカル アカウント ストアまたは Active Directory (ドメイン アカウントの場合) に保存されている Windows アカウントの基本認証がサポートされています。ここでは、基本認証を使用してユーザーが認証を実行できるようにしますが、資格情報の検証には、ASP.NET 2.0 メンバーシップ(英語) サービスを使用します。これによって、Windows アカウントに関連付けることなく、SQL Server など、既存のさまざまなメンバーシップ プロバイダーにユーザー情報を格納することができます。

タスク 1: .NET を使用したモジュールの開発

このタスクでは、HTTP 1.1 基本認証スキームをサポートする認証モジュールの開発について説明します。このモジュールは、ASP.NET Version 1.0 以降に提供されるようになった標準的な ASP.NET モジュール パターンを使用して開発されました。この同じパターンを使用して、IIS 7 サーバーを拡張する ASP.NET モジュールを構築します。実際、以前のバージョンの IIS 用に作成された既存の ASP.NET モジュールは IIS 7 でも使用できます。ASP.NET の統合が進んだことによって、ASP.NET モジュールを使って Web アプリケーションの機能をさらに向上させることができます。

: モジュールの完全なコードについては、付録 A を参照してください。

マネージ モジュールは、System.Web.IHttpModule インターフェイスを実装する .NET クラスです。このクラスの主な機能は、IIS 7 要求処理パイプラインで発生する 1 つまたは複数のイベント用に登録し、IIS 7 がそのイベント用のイベント ハンドラーを呼び出したときに何らかの処理を実行することです。

ここでは、"BasicAuthenticationModule.cs" という名前の新しいソース ファイルを作成し、モジュール クラスを作成します (完全なソース コードについては、付録 A を参照してください)。

public class BasicAuthenticationModule : System.Web.IHttpModule
{
    void Init(HttpApplication context)
    {
    }
    void Dispose()
    {
    }
}

Init メソッドの主な機能は、モジュールのイベント ハンドラー メソッドを適切な要求パイプラインのイベントに関連付けることです。モジュールのクラスはイベント処理メソッドを提供します。イベント処理メソッドはモジュールが要求する機能を実装します。これについては、後で詳しく説明します。

Dispose メソッドは、モジュール インスタンスが破棄されたときに、モジュールの状態をクリーンアップするために使用します。通常、解放が必要な特定のリソースをモジュールで使用しない場合は、実装しません。

Init()

クラスを作成したら、次に Init メソッドを実装します。ここでは、1 つまたは複数の要求パイプラインのイベントについてモジュールを登録するだけです。System.EventHandler デリゲート シグネチャを使用したモジュール メソッドを、System.Web.HttpApplication インスタンスの目的のパイプライン イベントに関連付けます。

public void Init(HttpApplication context)            
{
   //          
   // Subscribe to the authenticate event to perform the 
   // authentication. 
   // 
   context.AuthenticateRequest += new        
              EventHandler(this.AuthenticateUser);

   // 
   // Subscribe to the EndRequest event to issue the 
   // challenge if necessary. 
   // 
   context.EndRequest += new 
              EventHandler(this.IssueAuthenticationChallenge);
}

AuthenticateUser メソッドは、すべての要求において、AuthenticateRequest イベントの発生時に呼び出されます。このメソッドを使用して、要求で指定された資格情報を基にユーザー認証を行います。

IssueAuthenticationChallenge メソッドは、すべての要求において、EndRequest イベントの発生時に呼び出されます。このメソッドは、承認モジュールが要求を拒否し、認証が必要になった場合に、クライアントに対して基本認証チャレンジを発行します。

AuthenticateUser()

AuthenticateUser メソッドを実装します。このメソッドは次の処理を実行します。

  • 受信した要求ヘッダーから基本資格情報 (存在する場合) を抽出します。この実装については、ExtractBasicAuthenticationCredentials ユーティリティ メソッドを参照してください。
  • メンバーシップによって (構成済みの既定のメンバーシップ プロバイダーを使用して)、提供された資格情報の検証を試行します。この実装については、ValidateCredentials ユーティリティ メソッドを参照してください。
  • 認証が成功した場合は、ユーザーを識別するユーザー プリンシパルを作成し、要求に関連付けます。

ユーザー資格情報の取得および検証に成功した場合、後から他のモジュールやアプリケーション コードでアクセス制御に使用できるように、モジュールによって認証されたユーザー プリンシパルが生成されます。たとえば、URL 承認モジュールは、アプリケーションで構成されている承認規則を適用するために、次のパイプライン イベントでこのユーザーを調べます。

IssueAuthenticationChallenge()

IssueAuthenticationChallenge メソッドを実装します。このメソッドは次の処理を実行します。

  • 応答ステータス コードを調べて、この要求が拒否されたかどうかを判別します。
  • 拒否された場合は、応答に対して基本認証チャレンジ ヘッダーを発行して、クライアントの認証をトリガーします。

ユーティリティ メソッド

モジュールで使用するユーティリティ メソッドを実装します。

  • ExtractBasicAuthenticationCredentials。このメソッドは、基本認証スキームに従って、Authorize 要求ヘッダーから基本認証資格情報を抽出します。
  • ValidateCredentials。このメソッドは、メンバーシップを使用してユーザー資格情報の検証を試行します。メンバーシップ API は、基になる資格情報ストアを抽出し、メンバーシップ プロバイダーの追加、削除によって、資格情報ストアを構成できるようにします。

: このサンプルでは、メンバーシップの検証はコメントアウトされています。モジュールは、ユーザー名とパスワードが "test" であるかどうかのみ検証します。これは説明をわかりやすくするためです。運用環境への展開では使用しないでください。ValidateCredentials に含まれるメンバーシップ コードのコメントを解除し、アプリケーション用のメンバーシップ プロバイダーを構成するだけで、メンバーシップ ベースの資格情報の検証を有効にすることができます。詳細については、付録 C を参照してください。

タスク 2: アプリケーションへのモジュールの展開

最初のタスクでモジュールを作成したので、次にモジュールをアプリケーションに追加します。

アプリケーションの展開

最初に、モジュールをアプリケーションに展開します。ここで、いくつかの選択肢があります。

  • モジュールを含むソース ファイルを、アプリケーションの /App_Code ディレクトリにコピーします。この場合、モジュールをコンパイルする必要はありません。アプリケーションの起動時に、ASP.NET によって自動的にモジュール型がコンパイルされて読み込まれます。単純に、このソース コードを、BasicAuthenticationModule.cs という名前でアプリケーションの /App_Code ディレクトリに保存します。他の手順を省略したい場合は、この方法で作業してください。

  • モジュールをコンパイルしてアセンブリを作成し、このアセンブリをアプリケーションの /BIN ディレクトリに配置します。このモジュールをこのアプリケーションでのみ使用できるようにする場合や、モジュールのソースをアプリケーションといっしょに提供しない場合は、この方法が最も一般的な選択肢です。コマンド ライン プロンプトから次のコマンドを実行することによって、モジュールのソース ファイルをコンパイルします。

    <PATH_TO_FX_SDK>csc.exe /out:BasicAuthenticationModule.dll /target:library BasicAuthenticationModule.cs

    <PATH_TO_FX_SDK> は、CSC.EXE コンパイラーを含む .NET Framework SDK のパスです。

  • モジュールを厳密な名前付きアセンブリとしてコンパイルし、このアセンブリを GAC に登録します。これは、コンピューター上の複数のアプリケーションでこのモジュールを使用する場合に適切なオプションです。厳密な名前付きアセンブリの構築の詳細については、この MSDN の記事を参照してください。

アプリケーションの web.config ファイルで構成を変更する前に、既定でサーバー レベルでロックされている一部の構成セクションのロックを解除する必要があります。昇格した権限でコマンド プロンプトを開き ([スタート] ボタンをクリックし、Cmd.exe を右クリックして [管理者として実行] をクリック)、次のコマンドを実行します。

%windir%\system32\inetsrv\APPCMD.EXE unlock config /section:windowsAuthentication
%windir%\system32\inetsrv\APPCMD.EXE unlock config /section:anonymousAuthentication

これらのコマンドを実行すると、アプリケーションの web.config ファイルでこれらの構成セクションを定義できるようになります。

モジュールをアプリケーションで実行されるように構成します。まず、新しいモジュールを有効にして使用できるようにするために必要な構成情報を格納する、新しい web.config ファイルを作成します。以下のテキストを追加して、アプリケーションのルート (既定の Web サイトのルート アプリケーションを使用している場合は %systemdrive%\inetpub\wwwroot\web.config) に保存します。

<configuration> 
    <system.webServer> 
        <modules> 
        </modules> 
        <security> 
            <authentication> 
                <windowsAuthentication enabled="false"/> 
                <anonymousAuthentication enabled="false"/> 
            </authentication> 
        </security> 
    </system.webServer> 
</configuration> 

新しい基本認証モジュールを有効にする前に、他の IIS 7 認証モジュールをすべて無効にします。既定では、Windows 認証と匿名認証だけが有効になっています。ブラウザーで認証に Windows 資格情報を使用したり、匿名ユーザーを許可したりしないように、Windows 認証モジュールと匿名認証モジュールの両方を無効にします。

次に、アプリケーションに読み込まれるモジュールの一覧にモジュールを追加します。これによって、モジュールが有効になります。web.config をもう一度開いて、<modules> タグにエントリを追加します。

<add name="MyBasicAuthenticationModule" type="IIS7Demos.BasicAuthenticationModule" /> 

IIS 7 管理ツールまたは APPCMD.EXE コマンド ライン ツールを使用して、モジュールを展開することもできます。

これらの変更を行った後の、アプリケーションの web.config ファイルの最終的な内容については、付録 B を参照してください。

これで、カスタム基本認証モジュールの構成は終わりです。

結果を確認してみましょう。Internet Explorer を開き、次の URL を要求します。

https://localhost/

基本認証ログイン ダイアログ ボックスが表示されます。アクセスするには、[ユーザー名] フィールドに「test」、[パスワード] フィールドに「test」と入力します。アプリケーションに HTML、JPG、またはその他のコンテンツをコピーした場合は、これらも新しい BasicAuthenticationModule によって保護されます。

まとめ

この記事では、アプリケーション用のカスタム マネージ モジュールを開発および展開する方法、およびそのモジュールでアプリケーションに対するすべての要求についてサービスを提供できるようにする方法について説明しました。

また、マネージ コードでのサーバー コンポーネントの開発の利点についても確認しました。これによって、Windows 資格情報のストレージから独立した基本認証サービスを開発することができました。

もっと試してみたい方は、ASP.NET 2.0 メンバーシップ アプリケーション サービス機能を利用して資格情報ストアのプラグインをサポートするように、このモジュールを構成してみてください。詳細については、付録 C を参照してください。

ブログ http://www.mvolo.com/ (英語) には、IIS 7 モジュールの作成に関するリソースやヒントが多数用意されているほか、アプリケーションで使用できる既存の IIS 7 モジュールをダウンロードすることもできます。サンプルについては、「HttpRedirection モジュールを使用して要求をアプリケーションにリダイレクトする (英語)」、「DirectoryListingModule を使用して IIS Web サイトの見やすいディレクトリ一覧を表示する (英語)」、「IconHandler を使用して ASP.NET アプリケーションで見栄えのよいファイル アイコンを表示する (英語)」、および「IIS と ASP.NET によるホット リンクの阻止 (英語)」を参照してください。

付録 A: 基本認証モジュールのソース コード

このソース コードをすばやくアプリケーションに展開するには、BasicAuthenticationModule.cs という名前で、アプリケーションの /App_Code ディレクトリに保存してください。

注: メモ帳を使用している場合は、ファイルが BasicAuthenticationModule.cs.txt という名前で保存されないように、[名前を付けて保存] ダイアログ ボックスで [ファイルの種類] を [すべてのファイル] に設定します。

#region Using directives
using System;
using System.Collections;
using System.Text;
using System.Web;
using System.Web.Security;
using System.Security.Principal;
using System.IO;
#endregion
 
namespace IIS7Demos
{
    /// 
    /// This module performs basic authentication. 
    /// For details on basic authentication see RFC 2617. 
    /// 
    /// The basic operational flow is: 
    /// 
    ///     On AuthenticateRequest: 
    ///         extract the basic authentication credentials 
    ///         verify the credentials 
    ///         if succesfull, create the user principal with these credentials 
    /// 
    ///     On SendResponseHeaders: 
    ///         if the request is being rejected with an unauthorized status code (401), 
    ///         add the basic authentication challenge to trigger basic authentication. 
    ///       
    /// 

    public class BasicAuthenticationModule : IHttpModule
    {
        #region member declarations
        public const String     HttpAuthorizationHeader = "Authorization";  // HTTP1.1 Authorization header 
        public const String     HttpBasicSchemeName = "Basic"; // HTTP1.1 Basic Challenge Scheme Name 
        public const Char       HttpCredentialSeparator = ':'; // HTTP1.1 Credential username and password separator 
        public const int        HttpNotAuthorizedStatusCode = 401; // HTTP1.1 Not authorized response status code 
        public const String     HttpWWWAuthenticateHeader = "WWW-Authenticate"; // HTTP1.1 Basic Challenge Scheme Name 
        public const String     Realm = "demo"; // HTTP.1.1 Basic Challenge Realm 
        #endregion

        #region Main Event Processing Callbacks
        public void AuthenticateUser(Object source, EventArgs e)
        {
            HttpApplication application = (HttpApplication)source;
            HttpContext context = application.Context;
            String userName = null;
            String password = null;
            String realm = null;
            String authorizationHeader = context.Request.Headers[HttpAuthorizationHeader];

            // 
            //  Extract the basic authentication credentials from the request 
            // 
            if (!ExtractBasicCredentials(authorizationHeader, ref userName, ref password))
                return;
            // 
            // Validate the user credentials 
            // 
            if (!ValidateCredentials(userName, password, realm))
               return;

            // 
            // Create the user principal and associate it with the request 
            // 
            context.User = new GenericPrincipal(new GenericIdentity(userName), null);
        }

        public void IssueAuthenticationChallenge(Object source, EventArgs e)
        {
            HttpApplication application = (HttpApplication)source;
            HttpContext context = application.Context;

            // 
            // Issue a basic challenge if necessary 
            // 

            if (context.Response.StatusCode == HttpNotAuthorizedStatusCode)
            {
                context.Response.AddHeader(HttpWWWAuthenticateHeader, "Basic realm =\"" + Realm + "\"");
            }
        }
        #endregion

        #region Utility Methods
        protected virtual bool ValidateCredentials(String userName, String password, String realm)
        {
            // 
            //  Validate the credentials using Membership (refault provider) 
            // 
            // NOTE: Membership is commented out for clarity reasons.   
            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 
            // WARNING: DO NOT USE THE CODE BELOW IN PRODUCTION 
            // !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! 
            // return Membership.ValidateUser(userName, password); 
            if (userName.Equals("test") && password.Equals("test"))
            {
                return true;
            }
            else 
            {
                return false;
            }    
        }
      
        protected virtual bool ExtractBasicCredentials(String authorizationHeader, ref String username, ref String password)
        {
            if ((authorizationHeader == null) || (authorizationHeader.Equals(String.Empty)))
               return false;
            String verifiedAuthorizationHeader = authorizationHeader.Trim();
            if (verifiedAuthorizationHeader.IndexOf(HttpBasicSchemeName) != 0)     
                return false;

            // get the credential payload 
            verifiedAuthorizationHeader = verifiedAuthorizationHeader.Substring(HttpBasicSchemeName.Length, verifiedAuthorizationHeader.Length - HttpBasicSchemeName.Length).Trim();
           // decode the base 64 encoded credential payload 
            byte[] credentialBase64DecodedArray = Convert.FromBase64String(verifiedAuthorizationHeader);
            UTF8Encoding encoding = new UTF8Encoding();
            String decodedAuthorizationHeader = encoding.GetString(credentialBase64DecodedArray, 0, credentialBase64DecodedArray.Length);

            // get the username, password, and realm 
            int separatorPosition = decodedAuthorizationHeader.IndexOf(HttpCredentialSeparator);

           if (separatorPosition <= 0)
              return false;
            username = decodedAuthorizationHeader.Substring(0, separatorPosition).Trim();
           password = decodedAuthorizationHeader.Substring(separatorPosition + 1, (decodedAuthorizationHeader.Length - separatorPosition - 1)).Trim();

            if (username.Equals(String.Empty) || password.Equals(String.Empty))
               return false;

           return true;
        }
        #endregion

        #region IHttpModule Members
        public void Init(HttpApplication context)
        {
            // 
            // Subscribe to the authenticate event to perform the 
            // authentication. 
            // 
            context.AuthenticateRequest += new 
                               EventHandler(this.AuthenticateUser);
            // 
            // Subscribe to the EndRequest event to issue the 
            // challenge if necessary. 
            // 
            context.EndRequest += new 
                               EventHandler(this.IssueAuthenticationChallenge);
        }
        public void Dispose()
        {
            // 
            // Do nothing here 
            // 
        }
        #endregion

    }
}

付録 B: 基本認証モジュールの Web.config

この構成を、web.config ファイルとしてアプリケーションのルートに保存します。

<configuration> 
    <system.webServer> 
      <modules> 
           <add name="MyBasicAuthenticationModule" type="IIS7Demos.BasicAuthenticationModule" /> 
      </modules> 
      <security> 
         <authentication> 
          <windowsAuthentication enabled="false"/> 
             <anonymousAuthentication enabled="false"/> 
         </authentication> 
      </security> 
</system.webServer> 
</configuration> 

付録 C: メンバーシップの構成

ASP.NET 2.0 メンバーシップ サービスを使うと、多くの認証スキームおよびアクセス制御スキームで必須の資格情報の検証やユーザーの管理を簡単にアプリケーションに実装できます。メンバーシップによって、アプリケーション コードが資格情報ストアの実装から分離されます。これにより、既存の資格情報ストアとの統合の選択肢が広がります。

このモジュール サンプルのメンバーシップを利用するには、ValidateCredentials メソッド内の Membership.ValidateUser の呼び出しのコメントを解除し、アプリケーション用のメンバーシップ プロバイダーを構成します。メンバーシップの構成の詳細については、この MSDN の記事を参照してください。