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

作成者: Mike Volodarsky

はじめに

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

  • マネージド コードと ASP.NET サーバー拡張機能 API の使用
  • ネイティブ コードと IIS ネイティブ サーバー拡張機能 API の使用

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

IIS では、マネージド モジュールは、統合パイプライン アーキテクチャを使用したネイティブ モジュールと実質的に同程度に強力になります。 最も重要なのは、マネージド モジュールが提供するサービスが、ASPX ページなどのコンテンツを ASP.NET する要求に対してだけでなく、サーバーへのすべての要求に適用できるようになったことです。 マネージド モジュールは、ネイティブ モジュールと一貫した方法で構成および管理され、ネイティブ モジュールと同じ処理ステージと順序で実行できます。 最後に、マネージド モジュールは、複数の追加および強化された ASP.NET API を介して要求処理を操作するために、より広範な操作セットを実行できます。

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

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

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

  • ASP.NET API を使用したマネージド モジュールの開発
  • サーバーへのマネージド モジュールのデプロイ

IIS モジュールとハンドラーの開発の基本の詳細については、「.NET Framework を使用した IIS7 モジュールとハンドラーの開発」を参照してください。

また、http://www.mvolo.com/ ブログで IIS モジュールの作成に関する多くのリソースとヒントを見つけ、自分のアプリケーション用に既存の IIS モジュールをダウンロードすることもできます。 例については、HttpRedirection モジュールを使用したアプリケーションへのリダイレクト要求DirectoryListingModule を使用した IIS Web サイトの見栄えのよいディレクトリ一覧の取得IconHandler を使用した ASP.NET アプリケーションの見栄えのよいファイル アイコンの表示に関する記事を参照してください。

Note

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

前提条件

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

ASP.NET

Windows Vista コントロール パネルを使用して ASP.NET をインストールします。 [プログラム] - [Windows の機能をオンまたはオフにする] を選択します。 次に、[インターネット インフォメーション サービス] - [World Wide Web サービス] - [アプリケーション開発機能] を開き、[ASP.NET] をオンにします。

Windows Server® 2008 ビルドがある場合は、[サーバー マネージャー] - [役割] を開き、[Web サーバー (IIS)] を選択します。 [役割サービスの追加] を選択します。 [アプリケーション開発] で [ASP.NET] をオンにします。

基本認証に関する背景情報

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

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

Note

この認証プロトコルの詳細については、この記事の範囲外ですが、基本認証スキームでは、プレーン テキストでユーザー名/パスワードを送信するため、SSL をセキュリティで保護する必要があることを説明する価値があります。

IIS には、ローカル アカウント ストアに格納されている Windows アカウントまたはドメイン アカウントの Active Directory に対する基本認証のサポートが含まれています。 ここでは、ユーザーが基本認証を使用して認証できるようにしますが、資格情報を検証するために、代わりに ASP.NET 2.0 メンバーシップ サービスを使用できるようにします。 これにより、Windows アカウントに関連付けることなく、SQL Server などのさまざまな既存のメンバーシップ プロバイダーにユーザー情報を自由に格納できます。

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

このタスクでは、HTTP.1 基本認証スキームをサポートする認証モジュールの開発について説明します。 このモジュールは、ASP.NET v1.0 以降で使用可能な標準 ASP.NET モジュール パターンを使用して開発されました。 この同じパターンを使用して、IIS サーバー ASP.NET 拡張するモジュールをビルドします。 実際、IIS の以前のバージョン用に記述された既存の ASP.NET モジュールは IIS で使用でき、より優れた ASP.NET 統合を利用して、それを使用する Web アプリケーションにより多くの機能を提供できます。

Note

モジュールの完全なコードは付録 A で提供されています。

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

"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。 このメソッドは、メンバーシップを使用してユーザー資格情報の検証を試みます。 Membership API は、基になる資格情報ストアを抽象化し、構成を通じてメンバーシップ プロバイダーを追加または削除することで、資格情報ストアの実装を構成できるようにします。

Note

このサンプルでは、メンバーシップの検証がコメント アウトされ、代わりに、モジュールは単にユーザー名とパスワードが両方とも文字列 "test" と等しいかどうかを確認します。 これはわかりやすくするための架空の実行内容であり、運用環境でのデプロイを目的としたものではありません。 ValidateCredentials 内のメンバーシップ コードのコメントを解除し、アプリケーションのメンバーシップ プロバイダーを構成するだけで、メンバーシップ ベースの資格情報の検証を有効にするよう招待されます。 詳細については、付録 C をご覧ください。

タスク 2: モジュールのアプリケーションへのデプロイ

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

アプリケーションへのデプロイ

まず、モジュールをアプリケーションにデプロイします。 ここにはいくつかのオプションがあります。

  • モジュールが格納されたソース ファイルを、アプリケーションの /App_Code ディレクトリにコピーします。 これにはモジュールのコンパイルは必要ありません。ASP.NET アプリケーションの起動時に、モジュールの型が自動的にコンパイルされて読み込まれます。 このソース コードをアプリケーションの /App_Code ディレクトリ内の BasicAuthenticationModule.cs として保存するだけです。 他にやりやすい手順がなければ、この方法を実行します。

  • モジュールをアセンブリにコンパイルし、このアセンブリをアプリケーションの /BIN ディレクトリにドロップします。 これは、このモジュールのみをこのアプリケーションで使用できるようにし、モジュールのソースをアプリケーションに配布しない場合に最も一般的なオプションです。 コマンド ライン プロンプトから次のコマンドを実行して、モジュール ソース ファイルをコンパイルします。

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

    <PATH_TO_FX_SDK> に、CSC.EXE コンパイラが格納された .NET Framework SDK へのパスを指定します。

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

アプリケーションの 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 認証モジュールを無効にします。 既定では、Windows 認証と匿名認証のみが有効になっています。 ブラウザーが Windows 資格情報を使用して認証を試みるか、匿名ユーザーを許可しないようにするため、Windows 認証モジュールと匿名認証モジュールの両方が無効になります。

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

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

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

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

これで、カスタム基本認証モジュールの構成が完了しました。

試してみましょう。 Internet Explorer を開き、次の URL でアプリケーションに要求を行います。

http://localhost/

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

まとめ

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

また、マネージド コードでサーバー コンポーネントを開発する能力を目の当たりにしました。 これにより、Windows 資格情報ストレージから切り離された基本認証サービスの開発が可能になります。

上級者向けの手順としては、ASP.NET 2.0 メンバーシップ アプリケーション サービスの機能を利用して、プラグ可能な資格情報ストアをサポートするようにこのモジュールを構成します。 詳細については、付録 C をご覧ください。

http://www.mvolo.com/ ブログで IIS モジュールの作成に関する多くのリソースとヒントを見つけ、自分のアプリケーション用に既存の IIS モジュールをダウンロードすることもできます。 例については、HttpRedirection モジュールを使用したアプリケーションへのリダイレクト要求DirectoryListingModule を使用した IIS Web サイトの見栄えのよいディレクトリ一覧の取得IconHandler を使用した ASP.NET アプリケーションの見栄えのよいファイル アイコンの表示に関する記事を参照してください。

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

このソース コードを /App_Code ディレクトリ内の BasicAuthenticationModule.cs として保存して、アプリケーションにすばやくデプロイします。

Note

メモ帳を使用している場合は、[名前を付けて保存: すべてのファイル] を設定して、ファイルを 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 の呼び出しをコメント解除し、アプリケーションのメンバーシップ プロバイダーを構成します。 メンバーシップの構成の詳細については、「メンバーシップを使用する ASP.NET アプリケーションの構成」を参照してください。