IIS 7 統合パイプラインの利点の活用方法
公開日: 2007 年 12 月 5 日 (作業者: IIS チーム (英語))
更新日: 2008 年 2 月 29 日 (作業者: IIS チーム (英語))
はじめに
IIS 6 以前のバージョンでは、ASP.NET プラットフォームで .NET アプリケーション コンポーネントを開発できます。ASP.NET は ISAPI 拡張によって IIS と統合され、独自のアプリケーションおよび要求処理モデルを利用できます。例として 2 つのサーバー パイプラインがあります。1 つはネイティブ ISAPI フィルターおよび拡張機能コンポーネント用、もう 1 つはマネージ アプリケーション コンポーネント用のパイプラインです。ASP.NET コンポーネントは、完全に ASP.NET ISAPI 拡張機能バブル内で実行され、IIS スクリプト マップ構成内の ASP.NET にマップされた要求に対してのみ実行されます。
IIS 7 では、コア サーバーに ASP.NET ランタイムが統合され、モジュールとして認識されるネイティブ コンポーネントとマネージ コンポーネントから利用できる統合要求処理パイプラインを利用できます。この統合には、以下をはじめとする多くの利点があります。
- ネイティブおよびマネージの両方のモジュールから提供されるサービスを、ハンドラーに関係なく、すべての要求に適用できます。たとえば、マネージ フォーム認証を、ASP ページ、CGI、静的ファイルなどのすべてのコンテンツに使用できます。
- サーバー パイプラインでの実行位置の制限によって以前は ASP.NET コンポーネントで提供できなかった機能を使用できます。たとえば、要求を書き換える機能を提供するマネージ モジュールを使用して、認証などのサーバー処理に渡される前に要求を書き換えることができます。
- モジュールとハンドラー マッピングの単一の構成、カスタム エラーの単一の構成、URL 認証の単一の構成など、サーバー機能の実装、構成、監視、サポートを 1 か所で管理できます。
この記事では、ASP.NET アプリケーションで IIS 7 の統合モードの利点を活用する方法を検討し、以下の作業について解説します。
- アプリケーションごとにモジュールの有効化と無効化を行う。
- サーバーにマネージ アプリケーション モジュールを追加し、すべての要求の種類に適用されるようにする。
- マネージ ハンドラーを追加する。
IIS 7 モジュールの構築の詳細については、「.NET Framework による IIS 7 のモジュールおよびハンドラーの開発」を参照してください。
また、統合モードの利点の活用、および IIS 7 での ASP.NET 統合を活用する IIS 7 モジュールの開発のヒントについては、ブログ (http://www.mvolo.com/、英語) も参照してください。このブログでは、「HttpRedirection モジュールを使用して要求をアプリケーションにリダイレクトする (英語)」、「DirectoryListingModule を使用して IIS Web サイトの見やすいディレクトリ一覧を表示する (英語)」、「IconHandler を使用して ASP.NET アプリケーションで見栄えのよいファイル アイコンを表示する (英語)」、および「IIS と ASP.NET によるホット リンクの阻止 (英語)」などのページからさまざまな IIS 7 モジュールをダウンロードできます。
必要条件
このドキュメントのステップを実行するには、以下の IIS 7 の機能をインストールする必要があります。
ASP.NET
Windows Vista のコントロール パネルから ASP.NETをインストールします。[プログラムと機能] の [Windowsの機能の有効化または無効化] をクリックします。次に、[インターネット インフォメーション サービス]、[World Wide Web サービス]、[アプリケーション開発機能] の順に展開し、[ASP.NET] をオンにします。
Windows Server® 2008 ビルドの場合は、[サーバー マネージャ]、[役割] の順に展開し、[Web サーバー (IIS)] をオンにします。[役割サービスの追加] をクリックします。[アプリケーション開発] の [ASP.NET] をオンにします。
クラシック ASP
ASP.NET ページだけではなく、すべてのコンテンツに対して ASP.NET モジュールが動作することを示す必要があるので、Windows Vista のコントロール パネルからクラシック ASP をインストールします。[プログラム] をクリックし、[Windowsの機能の有効化または無効化] をクリックします。次に、[インターネット インフォメーション サービス]、[World Wide Web サービス]、[アプリケーション開発機能] の順に展開し、[ASP] をオンにします。
Windows Server 2008 ビルドの場合は、[サーバー マネージャ]、[役割] の順に展開し、[Web サーバー (IIS)] をオンにします。[役割サービスの追加] をクリックします。[アプリケーション開発] の [ASP] をオンにします。
アプリケーションへのフォーム認証の追加
この作業の一環として、アプリケーションで ASP.NET のフォームに基づく認証を有効にします。次の作業では、コンテンツ タイプに関係なく、フォーム認証モジュールがアプリケーションへのすべての要求に対して実行されるようにします。
最初に、普通の ASP.NET アプリケーション向けの場合と同様にフォーム認証を構成します。
サンプル ページの作成
この機能を例示するために、default.aspx ページを Web のルート ディレクトリに追加します。メモ帳を開きます。wwwroot ディレクトリに確実にアクセスするには、管理者として実行する必要があります ([すべてのプログラム]、[アクセサリ] の順にクリックし、[メモ帳] アイコンを右クリックして [管理者として実行] をクリックします)。%systemdrive%\inetpub\wwwroot\default.aspx というファイルを作成し、以下のコードを追加します。
<%=Datetime.Now%>
<BR>
Login Name: <asp:LoginName runat="server"/>
default.aspx は、現在時刻およびログインしたユーザーの名前を表示するだけのものです。このページを後で使用して、動作中のフォーム認証を示します。
フォーム認証およびアクセス制御規則の構成
ここで、default.aspx をフォーム認証で保護します。%systemdrive%\inetpub\wwwroot ディレクトリに web.config ファイルを作成し、次の構成を追加します。
<configuration>
<system.web>
<!--membership provider entry goes here-->
<authorization>
<deny users="?"/>
<allow users="*"/>
</authorization>
<authentication mode="Forms"/>
</system.web>
</configuration>
この構成によって、ASP.NET 認証モードでフォームに基づく認証が使用されるようになり、アプリケーションへのアクセスを制御するための承認設定が追加されます。これらの設定では、匿名ユーザー (?) のアクセスは拒否され、認証されたユーザー (*) のみが許可されます。
メンバーシップ プロバイダーの作成
ステップ 1: ユーザー資格情報を照合するための認証ストアを提供する必要があります。ASP.NET と IIS 7 の密接な統合を例示するために、独自の XML に基づくメンバーシップ プロバイダーを使用します (SQL Server がインストールされている場合は、既定の SQL Server メンバーシップ プロバイダーも使用できます)。
Web.config ファイル内の最初の <configuration>/<system.web> 構成要素の直後に、次のエントリを追加します。
<membership defaultProvider="AspNetReadOnlyXmlMembershipProvider">
<providers>
<add name="AspNetReadOnlyXmlMembershipProvider" type="AspNetReadOnlyXmlMembershipProvider" description="Read-only XML membership provider" xmlFileName="~/App_Data/MembershipUsers.xml"/>
</providers>
</membership>
ステップ 2: 構成エントリを追加した後で、「付録」に記載のメンバーシップ プロバイダー コードを、%systemdrive%\inetpub\wwwroot\App_Code ディレクトリに XmlMembershipProvider.cs として保存する必要があります。このディレクトリが存在していない場合は、作成する必要があります。
メモ: メモ帳を使用する場合は、XmlMembershipProvider.cs.txt として保存されないようにするために、[名前を付けて保存] をクリックし、[ファイルの種類] の一覧の [すべてのファイル] を選択してください。
ステップ 3: 最後は、実際の資格情報ストアです。次の xml スニペットを、%systemdrive%\inetpub\wwwroot\App_Data ディレクトリに MembershipUsers.xml として保存します。
メモ: メモ帳を使用する場合は、MembershipUsers.xml.txt として保存されないようにするために、[名前を付けて保存] をクリックし、[ファイルの種類] の一覧の [すべてのファイル] を選択してください。
<Users>
<User>
<UserName>Bob</UserName>
<Email>bob@contoso.com</Email>
</User>
<User>
<UserName>Alice</UserName>
<Password>contoso!</Password>
<Email>alice@contoso.com</Email>
</User>
</Users>
App_Data ディレクトリが存在していない場合は、作成する必要があります。
メモ: Windows Server 2003 および Windows Vista SP1 ではセキュリティ関係の変更があったので、GAC にキャッシュされていないメンバーシップ プロバイダー用のメンバーシップ ユーザー アカウントは、IIS 7 管理ツールで作成できなくなりました。
上記の作業を完了したら、IIS 7 管理ツールを使用してアプリケーション用のユーザーの追加または削除を行います。[ファイル名を指定して実行] メニューから「INETMGR」と入力して開始します。[Default Web Site] が表示されるまで、左側のツリー ビューの [+] 記号を展開します。[Default Web Site] を選択してから、その右のウィンドウの [セキュリティ] カテゴリをクリックします。そこに [.NET ユーザー] 機能が表示されています。[.NET ユーザー] をクリックし、任意のユーザー アカウントを 1 つ以上追加します。
MembershipUsers.xml の中身を確認し、この新しく作成したユーザーを探します。
ログイン ページの作成
フォーム認証を使用するには、ログイン ページを作成する必要があります。メモ帳を開きます。wwwroot ディレクトリに確実にアクセスするには、管理者として実行する必要があります ([すべてのプログラム]、[アクセサリ] の順にクリックし、[メモ帳] アイコンを右クリックして [管理者として実行] をクリックします)。%systemdrive%\inetpub\wwwroot ディレクトリに login.aspx ファイルを作成します。メモ - login.aspx.txt として保存されないようにするために、[名前を付けて保存] をクリックし、[ファイルの種類] の一覧の [すべてのファイル] を選択してください。このファイル内に、次のコードを貼り付けます。
<%@ Page language="c#" %>
<form id="Form1" runat="server">
<asp:LoginStatus runat="server" />
<asp:Login runat="server" />
</form>
これはログイン ページであり、承認規則によって特定のリソースへのアクセスが拒否されたときにこのページにリダイレクトされます。
テスト
Internet Explorer ウィンドウを開いて https://localhost/default.aspx を要求すると login.aspx にリダイレクトされます。これは、最初は認証されておらず、また前に未認証のユーザーのアクセスを保留するようにしたからです。MembershipUsers.xml に指定されているユーザー名とパスワードの組み合わせの 1 つを使用して正しくログインすると、リダイレクトされ、最初に要求した default.aspx に戻されます。このページには、現在時刻および認証に使用したユーザー ID が表示されます。
この時点で、フォーム認証、ログイン コントロール、メンバーシップを使用したカスタム認証ソリューションが正しく展開されています。これは IIS 7 で初めて登場した機能ではありません。以前のリリースの IIS の ASP.NET 2.0 から提供されています。
しかし、問題は ASP.NET で処理されるコンテンツのみが保護されるということです。
ブラウザー ウィンドウを閉じ、開き直して https://localhost/iisstart.htm を要求すると、資格情報を求めるプロンプトは表示されません。ASP.NET は、iisstart.htm のような静的ファイルに対する要求に関与しないので、フォーム認証でこのページを保護できません。クラシック ASP ページ、CGI プログラム、PHP、Perl スクリプトの場合も同じ動作になります。フォーム認証は ASP.NET 機能なので、単純にこれらのリソースに対する要求の最中は利用できません。
アプリケーション全体に対するフォーム認証の有効化
この作業では、過去のリリース上の ASP.NET の制限をなくし、アプリケーション全体に対して ASP.NET のフォーム認証および URL 認証機能を有効化します。
ASP.NET 統合の利点を活用するには、統合モードで実行されるようにアプリケーションを構成する必要があります。ASP.NET 統合モードは、アプリケーション プールごとに構成可能なので、異なるモードの ASP.NET アプリケーションを同一サーバー上で一緒にホストできます。アプリケーションを格納する既定のアプリケーション プールは既定で統合モードを使用するようになっているので、ここでの作業は必要ありません。
前に静的ページにアクセスしようとしたときに、統合モードの利点を体験できなかった理由をここで検討します。原因は、IIS 7 に付属のすべての ASP.NET モジュールに対する既定の設定にあります。
統合パイプラインの利点の活用
IIS 7 に付属のすべてのマネージ モジュール (フォーム認証、URL 認証モジュールなど) の既定の設定では、(ASP.NET) ハンドラーが管理するコンテンツのみにこれらのモジュールを適用するという前提条件が使用されます。これは、後方互換性のためです。
この前提条件を削除すると、コンテンツに関係なく、アプリケーションに対するすべての要求に対して目的のマネージ モジュールを実行できるようになります。静的ファイルをはじめとするすべてのアプリケーション コンテンツを、フォームに基づく認証を使用して保護するには、この作業が必要です。
そのためには、%systemdrive%\inetpub\wwwroot ディレクトリに存在するアプリケーションの web.config ファイルを開き、最初の <configuration> 要素の直後に次のコードを貼り付ける必要があります。
<system.webServer>
<modules>
<remove name="FormsAuthenticationModule" />
<add name="FormsAuthenticationModule" type="System.Web.Security.FormsAuthenticationModule" />
<remove name="UrlAuthorization" />
<add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" />
<remove name="DefaultAuthentication" />
<add name="DefaultAuthentication" type="System.Web.Security.DefaultAuthenticationModule" />
</modules>
</system.webServer>
この構成によって、モジュール要素は前提条件なしで再度追加され、アプリケーションへのすべての要求に対してこれらのモジュールが実行されるようになります。
テスト
以前に入力した資格情報のキャッシュを消去するために、Internet Explorer のすべてのインスタンスを閉じます。Internet Explorer を開き、次の URL にあるアプリケーションに要求を送信します。
https://localhost/iisstart.htm
ログインするための login.aspx ページにリダイレクトされます。
前に使用したユーザー名とパスワードの組み合わせでログインします。正しくログインすると、リダイレクトされて元のリソースに戻され、IIS 7 の [ようこそ] ページが表示されます。
メモ: 要求したのは静的ファイルですが、リソースの保護のために、マネージ フォーム認証モジュールおよび URL 認証モジュールによってサービスが提供されます。
さらに例証するために、クラシック ASP ページを追加し、フォーム認証でそのページを保護します。
メモ帳を開きます。wwwroot ディレクトリに確実にアクセスするには、管理者として実行する必要があります ([すべてのプログラム]、[アクセサリ] の順にポイントし、[メモ帳] アイコンを右クリックして [管理者として実行] をクリックします)。%systemdrive%\inetpub\wwwroot ディレクトリに page.asp ファイルを作成します。
メモ: メモ帳を使用する場合は、page.asp.txt として保存されないようにするために、[名前を付けて保存] をクリックし、[ファイルの種類] の一覧の [すべてのファイル] を選択してください。このファイル内に次のコードを貼り付けます。
<%
for each s in Request.ServerVariables
Response.Write s & ": "&Request.ServerVariables(s) & VbCrLf
next
%>
再度、すべての Internet Explorer インスタンスを閉じます (この操作を行わないと、資格情報がキャッシュされたままになります)。https://localhost/page.asp を要求すると、今度もログイン ページにリダイレクトされます。正しく認証された後で、この ASP ページが表示されます。
以上で完了です。マネージ サービスをサーバーに正しく追加し、ハンドラーに関係なくマネージ サービスがサーバーへのすべての要求に対して実行されるようになりました。
まとめ
このチュートリアルでは、ASP.NET 統合モードを活用し、強力な ASP.NET 機能を ASP.NET ページだけでなくアプリケーション全体で利用できることを実証しました。
重要な点は、使い慣れた ASP.NET 2.0 API を使用して、アプリケーションのすべてのコンテンツに対して実行できる新しいマネージ モジュールを構築できるようになり、強化された要求処理サービスのセットをアプリケーションで使用できるようにしたことです。
また、統合モードの利点の活用、および IIS 7 での ASP.NET 統合を活用する IIS 7 モジュールの開発のヒントについては、ブログ (http://www.mvolo.com/、英語) も参考になります。このブログでは、「HttpRedirection モジュールを使用して要求をアプリケーションにリダイレクトする (英語)」、「DirectoryListingModule を使用して IIS Web サイトの見やすいディレクトリ一覧を表示する (英語)」、「IconHandler を使用して ASP.NET アプリケーションで見栄えのよいファイル アイコンを表示する (英語)」、および「IIS と ASP.NET によるホット リンクの阻止 (英語)」などのページから、さまざまな IIS 7 モジュールをダウンロードできます。
付録
このメンバーシップ プロバイダーは、このMSDN の記事(英語)にあるサンプル XML メンバーシップ プロバイダーに基づいています。
このメンバーシップ プロバイダーを使用するには、コードを %systemdrive%\inetpub\wwwroot\App_Code ディレクトリ内に XmlMembershipProvider.cs として保存します。このディレクトリが存在していない場合は、作成する必要があります。メモ - メモ帳を使用する場合は、XmlMembershipProvider.cs.txt として保存されないようにするために、[名前を付けて保存] をクリックし、[ファイルの種類] の一覧の [すべてのファイル] を選択してください。
メモ: このメンバーシップ プロバイダーのサンプルは、例示のみを目的としています。これは実稼働用のメンバーシップ プロバイダーとしてのベスト プラクティスではなく、パスワードの安全な格納、ユーザー操作の監査などのセキュリティ要件を満たしていません。実際のアプリケーションには、このメンバーシップ プロバイダーを使用しないでください。
using System;
using System.Xml;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration.Provider;
using System.Web.Security;
using System.Web.Hosting;
using System.Web.Management;
using System.Security.Permissions;
using System.Web;
public class AspNetReadOnlyXmlMembershipProvider : MembershipProvider
{
private Dictionary<string, MembershipUser> _Users;
private string _XmlFileName;
// MembershipProvider Properties
public override string ApplicationName
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
public override bool EnablePasswordRetrieval
{
get { return false; }
}
public override bool EnablePasswordReset
{
get { return false; }
}
public override int MaxInvalidPasswordAttempts
{
get { throw new NotSupportedException(); }
}
public override int MinRequiredNonAlphanumericCharacters
{
get { throw new NotSupportedException(); }
}
public override int MinRequiredPasswordLength
{
get { throw new NotSupportedException(); }
}
public override int PasswordAttemptWindow
{
get { throw new NotSupportedException(); }
}
public override MembershipPasswordFormat PasswordFormat
{
get { throw new NotSupportedException(); }
}
public override string PasswordStrengthRegularExpression
{
get { throw new NotSupportedException(); }
}
public override bool RequiresQuestionAndAnswer
{
get { return false; }
}
public override bool RequiresUniqueEmail
{
get { throw new NotSupportedException(); }
}
// MembershipProvider Methods
public override void Initialize(string name,
NameValueCollection config)
{
// Verify that config isn't null
if (config == null)
throw new ArgumentNullException("config");
// Assign the provider a default name if it doesn't have one
if (String.IsNullOrEmpty(name))
name = "ReadOnlyXmlMembershipProvider";
// Add a default "description" attribute to config if the
// attribute doesn't exist or is empty
if (string.IsNullOrEmpty(config["description"]))
{
config.Remove("description");
config.Add("description",
"Read-only XML membership provider");
}
// Call the base class's Initialize method
base.Initialize(name, config);
// Initialize _XmlFileName and make sure the path
// is app-relative
string path = config["xmlFileName"];
if (String.IsNullOrEmpty(path))
path = "~/App_Data/MembershipUsers.xml";
if (!VirtualPathUtility.IsAppRelative(path))
throw new ArgumentException
("xmlFileName must be app-relative");
string fullyQualifiedPath = VirtualPathUtility.Combine
(VirtualPathUtility.AppendTrailingSlash
(HttpRuntime.AppDomainAppVirtualPath), path);
_XmlFileName = HostingEnvironment.MapPath(fullyQualifiedPath);
config.Remove("xmlFileName");
// Make sure we have permission to read the XML data source and
// throw an exception if we don't
FileIOPermission permission =
new FileIOPermission(FileIOPermissionAccess.Read,
_XmlFileName);
permission.Demand();
// Throw an exception if unrecognized attributes remain
if (config.Count > 0)
{
string attr = config.GetKey(0);
if (!String.IsNullOrEmpty(attr))
throw new ProviderException
("Unrecognized attribute: " + attr);
}
}
public override bool ValidateUser(string username, string password)
{
// Validate input parameters
if (String.IsNullOrEmpty(username) ||
String.IsNullOrEmpty(password))
return false;
// Make sure the data source has been loaded
ReadMembershipDataStore();
// Validate the user name and password
MembershipUser user;
if (_Users.TryGetValue(username, out user))
{
if (user.Comment == password) // Case-sensitive
{
return true;
}
}
return false;
}
public override MembershipUser GetUser(string username,
bool userIsOnline)
{
// Note: This implementation ignores userIsOnline
// Validate input parameters
if (String.IsNullOrEmpty(username))
return null;
// Make sure the data source has been loaded
ReadMembershipDataStore();
// Retrieve the user from the data source
MembershipUser user;
if (_Users.TryGetValue(username, out user))
return user;
return null;
}
public override MembershipUserCollection GetAllUsers(int pageIndex,
int pageSize, out int totalRecords)
{
// Note: This implementation ignores pageIndex and pageSize,
// and it doesn't sort the MembershipUser objects returned
// Make sure the data source has been loaded
ReadMembershipDataStore();
MembershipUserCollection users =
new MembershipUserCollection();
foreach (KeyValuePair<string, MembershipUser> pair in _Users)
users.Add(pair.Value);
totalRecords = users.Count;
return users;
}
public override int GetNumberOfUsersOnline()
{
throw new NotSupportedException();
}
public override bool ChangePassword(string username,
string oldPassword, string newPassword)
{
throw new NotSupportedException();
}
public override bool
ChangePasswordQuestionAndAnswer(string username,
string password, string newPasswordQuestion,
string newPasswordAnswer)
{
throw new NotSupportedException();
}
public override MembershipUser CreateUser(string username,
string password, string email, string passwordQuestion,
string passwordAnswer, bool isApproved, object providerUserKey,
out MembershipCreateStatus status)
{
throw new NotSupportedException();
}
public override bool DeleteUser(string username,
bool deleteAllRelatedData)
{
throw new NotSupportedException();
}
public override MembershipUserCollection
FindUsersByEmail(string emailToMatch, int pageIndex,
int pageSize, out int totalRecords)
{
throw new NotSupportedException();
}
public override MembershipUserCollection
FindUsersByName(string usernameToMatch, int pageIndex,
int pageSize, out int totalRecords)
{
throw new NotSupportedException();
}
public override string GetPassword(string username, string answer)
{
throw new NotSupportedException();
}
public override MembershipUser GetUser(object providerUserKey,
bool userIsOnline)
{
throw new NotSupportedException();
}
public override string GetUserNameByEmail(string email)
{
throw new NotSupportedException();
}
public override string ResetPassword(string username,
string answer)
{
throw new NotSupportedException();
}
public override bool UnlockUser(string userName)
{
throw new NotSupportedException();
}
public override void UpdateUser(MembershipUser user)
{
throw new NotSupportedException();
}
// Helper method
private void ReadMembershipDataStore()
{
lock (this)
{
if (_Users == null)
{
_Users = new Dictionary<string, MembershipUser>
(16, StringComparer.InvariantCultureIgnoreCase);
XmlDocument doc = new XmlDocument();
doc.Load(_XmlFileName);
XmlNodeList nodes = doc.GetElementsByTagName("User");
foreach (XmlNode node in nodes)
{
MembershipUser user = new MembershipUser(
Name, // Provider name
node["UserName"].InnerText, // Username
null, // providerUserKey
node["Email"].InnerText, // Email
String.Empty, // passwordQuestion
node["Password"].InnerText, // Comment
true, // isApproved
false, // isLockedOut
DateTime.Now, // creationDate
DateTime.Now, // lastLoginDate
DateTime.Now, // lastActivityDate
DateTime.Now, // lastPasswordChangedDate
new DateTime(1980, 1, 1) // lastLockoutDate
);
_Users.Add(user.UserName, user);
}
}
}
}
}