如何利用 IIS 7.0 整合式管線
IIS 6.0 和舊版允許透過 ASP.NET 平臺開發 .NET 應用程式元件。 ASP.NET 透過 ISAPI 擴充功能與 IIS 整合,並公開自己的應用程式和要求處理模型。 這會有效地公開兩個不同的伺服器管線,一個用於原生 ISAPI 篩選和擴充元件,另一個用於受控應用程式元件。 ASP.NET 元件會在 ASP.NET ISAPI 擴充功能泡泡內完全執行,而且僅適用于對應至 IIS 腳本對應組態中 ASP.NET 的要求。
IIS 7.0 和更新版本會將 ASP.NET 執行時間與核心 Web 服務器整合,提供一個統一的要求處理管線,這同時公開至原生和受控元件,稱為模組。 整合的許多優點包括:
- 允許原生和 Managed 模組所提供的服務套用至所有要求,而不論處理程式為何。 例如,受控表單驗證可用於所有內容,包括 ASP 頁面、CCI 和靜態檔案。
- 讓 ASP.NET 元件能夠提供先前因伺服器管線中放置而無法使用的功能。 例如,提供要求重寫功能的 Managed 模組可以在任何伺服器處理之前重寫要求,包括驗證。
- 實作、設定、監視及支援伺服器功能的單一位置,例如單一模組和處理常式對應組態、單一自訂錯誤組態、單一 URL 授權設定。
本文探討 ASP.NET 應用程式如何利用 IIS 7.0 和更新版本中的整合模式,並說明下列工作:
- 在個別應用層級上啟用/停用模組。
- 將受控應用程式模組新增至伺服器,並啟用它們以套用至所有要求類型。
- 新增 Managed 處理常式。
若要深入瞭解如何使用.NET Framework開發 IIS 7.0 和更新版本模組和處理常式,以建置 IIS 7.0 和更新版本模組。
另請參閱部落格, http://www.mvolo.com/ 以取得利用整合模式及開發利用 IIS 7.0 和更新版本中 ASP.NET 整合的 IIS 模組的更多秘訣。 在那裡,下載許多這類別模組,包括 使用 HttpRedirection 模組將要求重新導向至您的應用程式、 具有 DirectoryListingModule 的 IIS 網站的美觀目錄清單,以及 使用 IconHandler 在 ASP.NET 應用程式中顯示美觀的檔案圖示。
必要條件
若要遵循本檔中的步驟,必須安裝下列 IIS 7.0 和更新版本功能。
ASP.NET
透過 Windows Vista 主控台安裝 ASP.NET。 選取 [程式和功能] - [開啟或關閉 Windows 功能]。 然後開啟 「Internet Information Services」 - 「World Wide Web Services」 - 「Application Development Features」,並檢查 「ASP.NET」。
如果您有 Windows Server® 2008 組建,請開啟 [伺服器管理員] - [角色],然後選取 [Web Server (IIS) ]。 按一下 [新增角色服務]。 在 [應用程式開發] 底下,檢查 [ASP.NET]。
傳統 ASP
我們想要示範 ASP.NET 模組現在如何使用所有內容,而不只是 ASP.NET 網頁,因此請透過 Windows Vista 主控台安裝傳統 ASP。 選取 [程式] - [開啟或關閉 Windows 功能]。 然後開啟 「Internet Information Services」 - 「World Wide Web Services」 - 「Application Development Features」 並檢查 「ASP」。
如果您有 Windows Server 2008 組建,請開啟 [伺服器管理員] - [角色],然後選取 [Web Server (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.0 和更新版本之間的深入整合,我們會使用自己的 XML 型成員資格提供者, (您也可以在) 安裝SQL Server時使用預設SQL Server成員資格提供者。
在web.config檔案中的初始 < 組態/ < 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 程式碼片段儲存為目錄中的MembershipUsers.xml檔案 %systemdrive%\inetpub\wwwroot\App_Data
。
注意
如果使用 [記事本],請務必設定 [另存新檔:所有檔案] 以防止檔案儲存為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目錄不存在,您必須建立它。
注意
由於 Windows Server 2003 和 Windows Vista SP1 的安全性變更,您無法再使用 IIS 管理工具來建立非 GACed 成員資格提供者的成員資格使用者帳戶。
完成這項工作之後,請移至 IIS 管理工具,並為您的應用程式新增或刪除使用者。 從 「Run...」 啟動 「INETMGR」功能表。 開啟左側樹狀檢視的 「+」 登入,直到 [預設網站] 顯示為止。 選取 [預設網站],然後移至右側,然後按一下 [安全性] 類別。 其餘功能會顯示 「.NET Users」。 按一下 [.NET 使用者],然後新增您選擇的一或多個使用者帳戶。
查看MembershipUsers.xml尋找新建立的使用者。
建立登入頁面
若要使用表單驗證,我們必須建立登入頁面。 開啟記事本 (若要確定您可以存取下列 wwwroot 目錄,您必須以系統管理員身分執行,方法是以滑鼠右鍵按一下 Programs\Accessories\Notepad 圖示,然後按一下 [以系統管理員身分執行] ) ,然後在目錄中建立 login.aspx 檔案 %systemdrive%\inetpub\wwwroot
。 注意 - 請務必設定 [另存新檔:所有檔案] 以防止檔案儲存為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,因為您最初未經過驗證,而且我們稍早會協助存取未經驗證的使用者。 如果您已成功使用MembershipUsers.xml中指定的其中一個使用者名稱/密碼組登入,您就會重新導向回原始要求的 default.aspx 頁面。 此頁面接著會顯示您驗證的目前時間和使用者身分識別。
此時,我們已使用表單驗證、登入控制項和成員資格成功部署自訂驗證解決方案。 此功能不是 IIS 7.0 或更新版本中的新功能, 自舊版 IIS 上 ASP.NET 2.0 起已提供此功能。
不過,問題在於只有 ASP.NET 處理的內容受到保護。
如果您關閉並重新開啟瀏覽器視窗,並要求 http://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.0 和更新版本隨附之所有 ASP.NET 模組的預設設定中。
利用整合式管線
IIS 7.0 和更新版本隨附之所有 Managed 模組的預設組態,包括表單驗證和 URL 授權模組,都會使用前置條件,讓這些模組僅適用于 (ASP.NET) 處理常式所管理的內容。 這是為了回溯相容性而完成。
藉由移除前置條件,我們會針對應用程式的所有要求執行所需的受控模組,而不論內容為何。 這是保護靜態檔案,以及使用表單型驗證的任何其他應用程式內容的必要專案。
若要這樣做,請開啟位於 %systemdrive%\inetpub\wwwroot
目錄中的應用程式web.config檔案,然後將下列幾行貼到第一個 < 組態 > 專案下方:
<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 歡迎頁面。
注意
即使您要求靜態檔案,受控表單驗證模組和 URL 授權模組也提供了其服務,以保護您的資源。
為了進一步說明,我們會新增傳統 ASP 頁面,並使用表單驗證加以保護。
開啟記事本 (以確定您可以存取下方的 wwwroot 目錄,您必須以系統管理員身分執行--以滑鼠右鍵按一下 Programs\Accessories\Notepad 圖示,然後按一下 [以系統管理員身分執行] ) ,然後在您的 %systemdrive%\inetpub\wwwroot
目錄中建立page.asp檔案。
注意
如果使用 [記事本],請務必設定 [另存新檔:所有檔案] 以防止檔案儲存為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 來建置新的受控模組,這些 API 能夠針對所有應用程式內容執行,並將一組增強的要求處理服務提供給您的應用程式。
如需利用整合模式和開發利用 IIS 7 和更新版本中 ASP.NET 整合的 IIS 模組的更多秘訣,請隨意查看 https://www.mvolo.com/ 部落格。 您也可以在該處下載一些這類別模組,包括 使用 HttpRedirection 模組將要求重新導向至您的應用程式、 使用 DirectoryListingModule 為您的 IIS 網站尋找良好的目錄清單,以及 使用 IconHandler 在 ASP.NET 應用程式中顯示美觀的檔案圖示。
附錄
此成員資格提供者是以此 成員資格提供者中找到的 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);
}
}
}
}
}