IIS 7.0 統合パイプラインを利用する方法
作成者: Mike Volodarsky
IIS 6.0 以前のバージョンでは、ASP.NET プラットフォームを介して .NET アプリケーション コンポーネントを開発することができました。 ASP.NET が ISAPI 拡張を介して IIS と統合され、独自のアプリケーションと要求処理モデルを公開しました。 これにより、2 つの個別のサーバー パイプライン (1 つはネイティブ ISAPI フィルターと拡張コンポーネント用、もう一つはマネージド アプリケーション コンポーネント用) が効率良く公開されました。 ASP.NET コンポーネントは、ASP.NET ISAPI 拡張バブル内で完全に実行され、かつ IIS スクリプト マップ構成の ASP.NET にマップされた要求に対してのみ実行されます。
IIS 7.0 以降では、ASP.NET ランタイムがコア Web サーバーと統合され、モジュールと呼ばれるネイティブ コンポーネントとマネージド コンポーネントの両方に公開される統合された要求処理パイプラインが提供されます。 統合によって得られる多くの利点の例を、次に挙げます。
- ハンドラーに関係なく、ネイティブ モジュールとマネージド モジュールの両方で提供されるサービスをすべての要求に適用可能です。 たとえば、マネージド フォーム認証は、ASP ページ、CGI、静的ファイルなど、すべてのコンテンツに使用できます。
- ASP.NET コンポーネントが、サーバー パイプライン内での配置が原因で以前は使用できなかった機能を提供できるように強化されています。 たとえば、要求書き換え機能を提供するマネージド モジュールは、認証を含むサーバー処理が行われる前に要求を書き換えることができます。
- 単一モジュールとハンドラーのマッピング構成、単一カスタム エラー構成、単一 URL 承認構成などのサーバー機能を実装、構成、監視、およびサポートするための単一の場所。
この記事では、ASP.NET アプリケーションで IIS 7.0 以降の統合モードを利用する方法について説明し、次のタスクを示します。
- 各アプリケーションレベルでのモジュールの有効化/無効化。
- マネージド アプリケーション モジュールのサーバーへの追加と、すべての要求の種類に対する適用の有効化。
- マネージド ハンドラーの追加。
IIS 7.0 以降のモジュールのビルドに関する詳細については、「.NET Framework を使用した IIS 7.0 以降のモジュールとハンドラーの開発」を参照してください。
統合モードの利点と IIS 7.0 以降の ASP.NET 統合を活用する IIS モジュールの開発に関する詳細なヒントについては、ブログの http://www.mvolo.com/ も参照してください。 ここでは、HttpRedirection モジュールを使用したアプリケーションへのリダイレクト要求、DirectoryListingModule を使用した IIS Web サイトの見栄えのよいディレクトリ一覧の取得、IconHandler を使用した ASP.NET アプリケーションの見栄えのよいファイル アイコンの表示など、多くのモジュールをダウンロードできます。
前提条件
このドキュメントの手順に従うには、次の IIS 7.0 以上の機能をインストールする必要があります。
ASP.NET
Windows Vista コントロール パネルを使用して ASP.NET をインストールします。 [プログラムと機能] - [Windows の機能の有効化または無効化] を選択します。 次に、[インターネット インフォメーション サービス] - [World Wide Web サービス] - [アプリケーション開発機能] を開き、[ASP.NET] をオンにします。
Windows Server® 2008 ビルドがある場合は、[サーバー マネージャー] - [役割] を開き、[Web サーバー (IIS)] を選択します。 [役割サービスの追加] をクリックします。 [アプリケーション開発] で [ASP.NET] をオンにします。
Classic ASP
ASP.NET モジュールが ASP.NET ページだけでなく、すべてのコンテンツで動作するようになりました。したがって、ここでは Windows Vista コントロール パネルを使用して Classic ASP をインストールする方法について説明します。 [プログラム] - [Windows の機能の有効化または無効化] を選択します。 次に、[インターネット インフォメーション サービス] - [World Wide Web サービス] - [アプリケーション開発機能] を開き、[ASP] をオンにします。
Windows Server 2008 ビルドがある場合は、[サーバー マネージャー] - [役割] を開き、[Web サーバー (IIS)] を選択します。 [役割サービスの追加] をクリックします。 [アプリケーション開発] で [ASP] をオンにします。
フォーム認証のアプリケーションへの追加
このタスクの一環として、アプリケーションの ASP.NET フォームベース認証を有効にします。 次のタスクでは、コンテンツの種類にかかわらず、アプリケーションに対するすべての要求に対してフォーム認証モジュールを実行できるようにします。
最初に、通常の ASP.NET アプリケーションの場合と同様にフォーム認証を構成します。
サンプル ページの作成
この機能を説明するために、Web ルート ディレクトリに default.aspx ページを追加します。 メモ帳を開き (以下の wwwroot ディレクトリにアクセスできることを確認するには、Programs\Accessories\Notepad アイコンを右クリックし、[管理者として実行] をクリックして管理者として実行する必要があります)、次のファイルを作成します: %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.0 以降の詳細な統合を示すために、独自の 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 として保存する必要があります。 ディレクトリが存在しない場合は、作成してください。
Note
メモ帳を使用している場合は、[名前を付けて保存: すべてのファイル] を設定して、ファイルがXmlMembershipProvider.cs.txt として保存されないようにします。
手順 3: 最後は、実際の資格情報ストアです。 以下の xml スニペットを %systemdrive%\inetpub\wwwroot\App_Data
ディレクトリにMembershipUsers.xml ファイルとして保存します。
Note
メモ帳を使用している場合は、[名前を付けて保存: すべてのファイル] を設定して、ファイルが MembershipUsers.xml.txt として保存されないようにします。
<Users>
<User>
<UserName>Bob</UserName>
<Password>contoso!</Password>
<Email>bob@contoso.com</Email>
</User>
<User>
<UserName>Alice</UserName>
<Password>contoso!</Password>
<Email>alice@contoso.com</Email>
</User>
</Users>
App_Data ディレクトリが存在しない場合は、作成してください。
Note
Windows Server 2003 および Windows Vista SP1 のセキュリティが変更されたため、IIS 管理ツールを使用して GACed 以外のメンバーシップ プロバイダーのメンバーシップ ユーザー アカウントを作成することはできなくなりました。
このタスクを完了したら、IIS 管理ツールに移動してアプリケーションのユーザーを追加または削除します。 [実行…] メニューから [INETMGR] を起動します。 [既定の Web サイト] が表示されるまで、左側のツリー ビューで [+] 記号を開きます。 [既定の Web サイト] を選択し、右側に移動して [セキュリティ] カテゴリをクリックします。 残りの機能は [.NET ユーザー] と表示されます。 [.NET ユーザー] をクリックし、任意のユーザー アカウントを 1 つ以上追加します。
MembershipUsers.xml で、新しく作成されたユーザーを検索します。
ログイン ページの作成
フォーム認証を使用するには、ログイン ページを作成する必要があります。 メモ帳を開き (以下の wwwroot ディレクトリにアクセスできることを確認するには、Programs\Accessories\Notepad アイコンを右クリックし、[管理者として実行] をクリックして管理者として実行する必要があります)、%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 ウィンドウを開き、http://localhost/default.aspx
を要求します。 最初は認証されていなかったため、login.aspx にリダイレクトされていることがわかります。Microsoft では、以前に認証されなかったユーザーへのアクセスが保留されます。 MembershipUsers.xml で指定されたユーザー名とパスワードのペアのいずれかを使用して正常にログインすると、最初に要求された default.aspx ページにリダイレクトされます。 このページには、現在の時刻と、認証されたユーザー ID が表示されます。
この時点で、フォーム認証、ログイン コントロール、およびメンバーシップを使用してカスタム認証ソリューションが正常にデプロイされました。 この機能は IIS 7.0 以降の新機能ではなく、以前の IIS リリースの ASP.NET 2.0 以降から使用できるようになっています。
ただし、問題は、ASP.NET によって処理されるコンテンツのみが保護されていることです。
ブラウザー ウィンドウを閉じて再度開き、http://localhost/iisstart.htm
を要求する場合、資格情報の入力を求められることはありません。 ASP.NET は、iisstart.htm などの静的ファイルの要求には関与しません。 そのため、フォーム認証では保護できません。 Classic ASP のページ、CGI プログラム、PHP または Perl スクリプトでも同じ動作が見られます。 フォーム認証は ASP.NET の機能であり、これらのリソースへの要求時には使用できません。
アプリケーション全体のフォーム認証の有効化
このタスクでは、以前のリリースでの ASP.NET の制限を排除し、アプリケーション全体に対する ASP.NET フォーム認証と URL 承認機能を有効にします。
ASP.NET 統合を利用するには、統合モードで実行するようにアプリケーションを構成する必要があります。 ASP.NET 統合モードはアプリケーション プールごとに構成でき、異なるモードの ASP.NET アプリケーションを同じサーバー上で横並びにホストできます。 アプリケーションが存在する既定のアプリケーション プールでは、既定で統合モードが既に使用されているため、ここでは何もする必要はありません。
では、前に静的ページにアクセスしようとしたときに統合モードの利点を体験できなかったのはなぜでしょうか。、 その答えは、IIS 7.0 以降に付属するすべての ASP.NET モジュールの既定の設定にあります。
統合パイプラインの活用
フォーム認証モジュールや URL 承認モジュールなど、IIS 7.0 以降に付属するすべてのマネージド モジュールの既定の構成で前提条件が使用されるため、これらのモジュールは (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 でアプリケーションに対する要求を実行します。
http://localhost/iisstart.htm
login.aspx ページにリダイレクトされ、ログインします。
以前に使用したユーザー名とパスワードでログインします。 正常にログインすると、元のリソースにリダイレクトされ、IIS のウェルカム ページが表示されます。
Note
静的ファイルを要求した場合でも、マネージド フォーム認証モジュールと URL 承認モジュールは、リソースを保護するためにサービスを提供します。
これをさらに説明するために、従来の ASP ページを追加し、フォーム認証で保護します。
メモ帳を開き (以下の wwwroot ディレクトリにアクセスできることを確認するには、Programs\Accessories\Notepad アイコンを右クリックし、[管理者として実行] をクリックして管理者として実行する必要があります)、%systemdrive%\inetpub\wwwroot
ディレクトリで page.asp ファイルを作成します。
Note
メモ帳を使用している場合は、[名前を付けて保存: すべてのファイル] を設定して、ファイルが page.asp.txt として保存されないようにします。 以下の行を貼り付けます。
<%
for each s in Request.ServerVariables
Response.Write s & ": "&Request.ServerVariables(s) & VbCrLf
next
%>
すべての Internet Explorer インスタンスをもう一度閉じます。閉じないと、資格情報が引き続きキャッシュされ、http://localhost/page.asp
が要求されます。 もう一度ログイン ページにリダイレクトされ、認証が成功すると ASP ページが表示されます。
お疲れ様でした。これで、マネージド サービスがサーバーに正常に追加され、ハンドラーに関係なく、サーバーへのすべての要求に対してマネージド サービスが有効になります。
まとめ
このチュートリアルでは、ASP.NET 統合モードを利用して、ASP.NET ページだけでなく、アプリケーション全体で強力な ASP.NET 機能を使用できるようにする方法を示しました。
さらに重要なのは、すべてのアプリケーション コンテンツに対して実行できる使い慣れた ASP.NET 2.0 API を使用して新しいマネージド モジュールを構築し、アプリケーションに一連の強化された要求処理サービスが提供されるようになったことです。
統合モードの利点と IIS 7 以降の ASP.NET 統合を活用する IIS モジュールの開発に関する詳細なヒントについては、ブログの https://www.mvolo.com/ を参照してください。 ここでは、HttpRedirection モジュールを使用したアプリケーションへのリダイレクト要求、DirectoryListingModule を使用した IIS Web サイトの見栄えのよいディレクトリ一覧の取得、IconHandler を使用した ASP.NET アプリケーションの見栄えのよいファイル アイコンの表示など、多くのモジュールをダウンロードすることもできます。
付録
このメンバーシップ プロバイダーは、このメンバーシップ プロバイダーで見つかったサンプル XML メンバーシップ プロバイダーに基づいています。
このメンバーシップ プロバイダーを使用するには、コードを XmlMembershipProvider.cs として %systemdrive%\inetpub\wwwroot\App\_Code
ディレクトリに保存します。 このディレクトリが存在しない場合は、作成する必要があります。 注: メモ帳を使用している場合は、[名前を付けて保存: すべてのファイル] を設定して、ファイルがXmlMembershipProvider.cs.txt として保存されないようにします。
Note
このメンバーシップ プロバイダーのサンプルは、このデモのみでの使用を目的としています。 パスワードの安全な保存やユーザー アクションの監査など、本番環境のメンバーシップ プロバイダーのベスト プラクティスとセキュリティ要件には準拠していません。 アプリケーションでこのメンバーシップ プロバイダーを使用しないでください。
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);
}
}
}
}
}