Microsoft .NET Framework 與 Microsoft ASP.NET 支援多種適用於程式碼的安全性功能。因此如果您只需要使用與 HttpContext.Current.User.IsInRole() 類似的結構,就能兼顧 WSE 架構 Web 服務方法的存取,是不是會很棒?在本篇文章中,我將示範如何結合 WSE 2.0 的功能與 .NET Framework 的角色架構權限系統,來簽署和驗證訊息。
在傳統的 Web 應用程式或 Web 服務中,您只能依賴 IIS (SSL) 的驗證和加密方法。在此情況下,您可以設定目錄,要求使用者透過 HTTP 通訊協定 (藉由使用 HTTP 基本或 Windows 整合式安全性) 來傳送登入憑證。
起初您可能認為使用 HTTP 來驗證 Web 服務請求是很好的構想,但是一旦 WS-Routing 出場,情勢將會徹底改觀:訊息的傳送者和最終接收者之間將不會有直接的 HTTP 連接,取而代之的是許多不同的通訊協定將可用於路由路徑。如此一來,即可造成所有傳輸層級的安全性方法 (例如純粹的選購類附加元件) 失靈,因為它無法保證訊息的端對端完整性和安全性。
提供這些 Web 服務的端對端服務的方法之一,就是根據 WS-Security 規格,使用 X.509 憑證來簽署傳出的訊息。
簽章訊息的捷徑
您可以利用 Verisign 等知名的憑證授權單位 (CA),或是使用 Windows Certificate Services 直接建立自己的 CA,來取得 X.509 憑證。安裝此服務之後 -- 此服務是 Windows 2000 Server 或 Windows Server 2003 的選購類元件 -- 您可以將瀏覽器指向 http://<servername>/certsrv,以便要求建立新憑證。系統管理員獲准您的要求之後,您就可以使用相同的 Web 應用程式,透過 http://<servername>/certsrv 將新建立的憑證加入您的私用憑證存放區。此外在匯入憑證時,請記得手動選取存放區位置 (包括「實體位置」),以便將憑證授權單位的根憑證加入 Web 伺服器的「本機電腦」存放區。
建立並取得 X.509 憑證之後,您就可以開始用它來簽署 Web 服務要求。但請先稍安勿躁 -- 首先您必須要讓專案支援 Web Services Enhancements 2.0。在「.NET Developer」模式中安裝 WSE 2.0 之後,您就可以在 Visual Studio® .NET 中的任何專案上按一下滑鼠右鍵,並選擇 WSE Settings 2.0,來開啟如 [圖 1] 所示的對話方塊。
[圖 1] 啟用 Web Services Enhancements 2.0
一旦您選取此核取方塊,便會產生一些變動。首先,Microsoft.Web.Services.dll 的參考將會自動加入專案中。其次,且更為重要的是,[加入 Web 參考 ...] 與 [更新 Web 參考] 命令的行為將會改變,使得您以後能夠存取其他內容屬性。舉例而言,您將可以改變安全性語彙基元。改變命令行為的方法就是為每個 Web 參考都建立第二個 Proxy。新產生的 Proxy 名稱將會附加「Wse」的後置字元 (譬如說,Proxy 將會命名為 「MyServiceWse」而非「MyService」),而 Framework 將為其選取不同的基底類別:WSE Proxy 不像非 WSE Proxy 一樣從 System.Web.Services.Protocols.SoapHttpClientProtocol 繼承,而是擴充 Microsoft.Web.Services.WebServicesClientProtocol,其中包含各種附加的屬性。
有了此新的基底類別,您就能夠存取 WSE SoapContext,也能將 WS-Security 語彙基元和簽章加入傳出的訊息中。我建立了示範服務來進一步說明角色架構安全性擴充功能,如下所示:
using System; using System.Web.Services; using System.Security.Permissions; [WebService(Namespace="http://schemas.ingorammer.com/wse/role-based-security-extensions/2003-08-10")] public class DemoService: System.Web.Services.WebService { [WebMethod] public string HelloWorld() { return "Hello World"; } }接著我將 Web 參考加入專案中,將其「URL Behavior」指定為「dynamic」,並建立下列 app.config 檔案,以便指定伺服器位置和用戶端應使用的憑證。如果您在自己的電腦上根據此範例進行練習,請記得要指定您的私用憑證存放區中的憑證名稱!
<configuration> <appSettings> <add key="RoleBasedSecurityClient.demosvc.DemoService" value="http://server/WSEDemo/DemoService.asmx"/><add key="CertificateName" value="[email protected]"/>
</appSettings> </configuration>如此一來,就可以根據 X.509 憑證,開始使用 Web Services Enhancements 2.0 來建立二進位安全性語彙基元。傳出的訊息將會附加上此語彙基元,而且使用它來進行密碼編譯簽名。在以下的用戶端程式碼中,我所加入的功能可用來存取使用者的私用憑證存放區,並尋找在組態檔 (如上所示) 中指定的憑證。
public static void Main(string[] args) { String sto = X509CertificateStore.MyStore; // 開啟憑證存放區X509CertificateStore store = X509CertificateStore.CurrentUserStore(sto);
store.OpenRead(); // 找出您要使用的憑證String certname = System.Configuration.ConfigurationSettings.AppSettings["CertificateName"];
X509CertificateCollection certcoll = store.FindCertificateBySubjectString(certname);
if (certcoll.Count == 0) { Console.WriteLine("Certificate not found"); } else { X509Certificate cert = certcoll[0]; DemoServiceWse svc = new DemoServiceWse(); SoapContext ctx = svc.RequestSoapContext; // 使用憑證來簽署訊息SecurityToken tok = new X509SecurityToken(cert);
ctx.Security.Tokens.Add(tok);
ctx.Security.Elements.Add(new Signature(tok));
// 呼叫 Web Service String res = svc.HelloWorld(); } Console.WriteLine("Done"); Console.ReadLine(); }就是這麼簡單!這個部分到此已說明完畢,您應該已經學會簽章訊息的捷徑。如果您想瞭解更詳盡的方法以及其精彩內容,請參閱 Matt Powell 的文章《使用 Web Services Enhancements 進行 WS-Security 驗證和數位簽章 (英文)》,以取得詳細資訊。
使用宣告式與強制式角色架構安全性
太好了 -- 訊息已經簽署完畢。接下來呢?一方面,您可以直接從伺服器端 [WebMethod] 存取安全性語彙基元與其憑證,並檢查它是否為叫用服務所需的安全性語彙基元。雖然在剛開始的時候,這種作法似乎很實際,但您必須考慮到您的應用程式將要支援多位使用者,而每位使用者都將擁有自己的憑證。而且如果您的 Web 服務基礎結構比上述的簡易範例碼更為複雜,那麼您也就極可能會需要面臨具多種不同角色的使用者。每種角色的權限都不一樣,因此只有屬於特定角色的使用者才能呼叫某些方法。
實作典型的 HTTP 架構 Web 應用程式時,您可以直接存取
HttpContext
,藉此根據 Windows 使用者群組或 Active Directory 群組來檢查成員資格,如下所示:[WebMethod] public void AuthorizeOrder(long orderID) { if (! HttpContext.Current.User.IsInRole(@"DOMAIN\Accounting")
) throw new Exception("Only members of 'Accounting' may call this method."); // ... 更新資料庫 }您也可以使用此語法來檢查進一步細分的使用權限集,例如使用某些參數值來進行細分。在下列範例中,角色為「HR」的使用者可以呼叫方法,但是只有角色為 「PointyHairedBoss」的使用者才可以將薪資調高至特定金額以上:
public void SetMonthlySalary(long employeeID, double salary) { if (! HttpContext.Current.User.IsInRole(@"DOMAIN\HR")
) throw new Exception("Only members of 'HR' may call this method."); if (salary > 2000) { if (! HttpContext.Current.User.IsInRole(@"DOMAIN\PointyHairedBoss")
) throw new Exception("Only the pointy haired boss might set salaries larger than 2K"); } // ... 更新資料庫 }上述程式碼是使用一項標準的 ASP.NET 功能 -- 儘管此功能會對我們的案例造成一個有點嚴重的缺陷:實際上,此功能是依靠 HTTP 驗證,而非使用端對端驗證配置,因此並未解決我們的需求。另一個缺點在於,使用者的角色成員的認定,是查看其 Windows 或 Active Directory 群組成員資格。因此如果您的應用程式需要細分的權限層級,便會產生大量的 Windows 群組 -- 如此會使您的系統管理員抓狂。
自訂安全性語彙基元管理員為您解除煩惱
幸好 WSE 2.0 提供必要的擴充性攔截程序,可供您自行實作角色架構安全性。聽來複雜,但作法簡單:您只需要提供必要的資訊給 Runtime,以便確定使用者的角色成員。畢竟,要不然 Framework 怎麼曉得由「[email protected]」簽章的訊息,是由 PointyHairedBoss 角色的成員所傳送。
若要在此環境中啟用角色架構安全性,您需要撰寫從 Microsoft.Web.Services.Security.Tokens.X509SecurityTokenManager 衍生的自訂安全性語彙基元管理員。稍後我會討論此新的語彙基元管理員,此管理員將會讀取組態檔 (如下所示),以便確定憑證與角色的對應:
<?xml version="1.0" encoding="utf-8" ?> <CertificateMapping> <Certificates> <!-- [email protected] --> <CertificateMap Hash="f5 06 ba 1d 76 3b 59 1f ac 0c 3d ff e8 52 a3 41 44 b5 ed b1"> <Roles> <Role>PointyHairedBoss</Role> <Role>Accounting</Role> <Role>HR</Role> </Roles> </CertificateMap> <!-- [email protected] --> <CertificateMap Hash="d7 fd 06 0d 43 7f 8f bb df a2 ee 9a 55 e4 c4 49 93 65 99 e4"> <Roles> <Role>Accounting</Role> <Role>HR</Role> </Roles> </CertificateMap> </Certificates> </CertificateMapping>在此組態檔中,每個您要指派角色的憑證都必須具有 <CertificateMap> 項目。憑證可根據其指紋雜湊碼來加以識別,舉例而言,您可以使用憑證授權單位 (CA) 所提供的 Windows Certificate Services 管理工具來取得指紋雜湊碼,方法是按一下 [開始] > [系統管理工具] > [憑證授權單位]。執行此工具並選取之前發出的憑證時,您將會看到如 [圖 2] 所示的視窗,讓您將雜湊碼複製到剪貼簿。
只要開啟 Internet Explorer 並按一下 [工具] > [網際網路選項] > [內容] > [憑證],您也可以存取目前使用者憑證存放區中所有憑證的相同資訊。
[圖 2] 憑證詳細資訊
接著您可以使用此雜湊碼,在組態檔中建立新的 <CertificateMap> 項目,以便指定與特定憑證相關聯的角色。稍後 XmlSerializer 將會讀取此檔案,而且此檔案將會根據以下兩個類別,進行物件的還原序列化:
public class CertificateMapping { [XmlArrayItem(typeof(CertificateMap))] public ArrayList Certificates = new ArrayList(); public CertificateMap this[String hash] { get { foreach (CertificateMap cert in Certificates) { if (cert.CertificateHash.Replace(" ","").ToUpper() == hash.ToUpper()) return cert; } return null; } } } public class CertificateMap { [XmlAttribute("Hash")] public String CertificateHash; [XmlArrayItem("Role", typeof(String))] public ArrayList Roles = new ArrayList(); }介紹 X509SecurityTokenManager
語彙基元管理員本身必須繼承 Microsoft.Web.Services.Security.Tokens.X509SecurityTokenManager
。
在 Framework 中註冊此類別之後,要求訊息中所包含的每個 X509 二進位安全性語彙基元都將經由此新類別的 AuthenticateToken() 方法來傳遞,再到達 [WebMethod]。因此,這個方法可讓您針對每個語彙基元建立相符的 IPrincipal 物件,且您最終將可以往回攔截至 .NET 安全性模型。若要實作此安全性語彙基元管理員,首先您得要將參考加入至必要的命名空間:
using System; using System.Collections; using Microsoft.Web.Services.Security; using Microsoft.Web.Services; using Microsoft.Web.Services.Security.Tokens; using System.Security.Principal; using System.Threading; using System.Xml.Serialization; using System.IO; using System.Web; using System.Security.Permissions;然後您可以覆寫 AuthenticateToken() 方法,以便存取內送的安全性語彙基元,並附加上相符的 IPrincipal。
namespace RoleBasedSecurityExtension { [SecurityPermission(SecurityAction.Demand, Flags=SecurityPermissionFlag.UnmanagedCode)] public class X509RoleBasedSecurityTokenManager: X509SecurityTokenManager { protected override voidAuthenticateToken
(X509SecurityToken token) { base.AuthenticateToken(token);token.Principal = new CertificatePrincipal(token.Certificate);
} } }介紹 Identity 與 Principal
.NET Framework 的角色架構安全性功能,是根據指派給要求內容的 Principal 與 Identity 物件而建置。Principal 物件包含目前使用者所屬的角色,而 Identity 物件則是儲存使用者本身的相關資訊,以及使用者取得授權的方式。
您可以在 System.Security.Principal 中找到這兩個介面,其內容如下:
public interface IPrincipal { IIdentity Identity { get; } bool IsInRole(string role); } public interface IIdentity { string AuthenticationType { get; } bool IsAuthenticated { get; } string Name { get; } }為了完成角色架構安全性擴充功能,我實作了自訂 Principal (CertificatePrincipal) 和自訂 Identity (CertificateIdentity),以便存取目前使用者的角色成員與驗證所用的憑證。
每當內送的訊息傳到您的伺服器,並使用 X509Certificate 物件作為參數時,程式便會從安全性語彙基元管理員叫用此類別的公用建構函式。公用建構函式可能會從 XML 檔載入角色對應 (如有變更的話),或使用儲存在靜態變數中的快取版本。接著,篩選器會根據從 XML 檔載入的資訊,尋找指定的憑證。它會檢查角色成員資訊是否包含指定憑證的相關記錄。如果不是,便會傳回例外 -- 或是建立相對應的 Identity 物件。
若要實作此 Principal,首先您必須包含必要的命名空間:
using System; using System.Xml.Serialization; using System.IO; using System.Security.Principal; using Microsoft.Web.Services.Security.X509;必要時,公用建構函式會讀取 XML 檔,並檢查傳送者的憑證是否已經在 certmap.config 中設定。如果未設定,這時公用建構函式便會建立新的 CertificateIdentity 物件,並傳回例外。
public class CertificatePrincipal: IPrincipal { private static CertificateMapping _map; private static DateTime _certmapDateTime = DateTime.MinValue; private CertificateMap _certmap; private CertificateIdentity _ident; public CertificatePrincipal(X509Certificate cert) { String file = System.Web.HttpContext.Current.Server.MapPath("certmap.config"); // 比較檔案和快取版本的日期 FileInfo f = new FileInfo(file); DateTime fileDate = f.LastWriteTime; // 視需要重新載入 if (fileDate > _certmapDateTime) { XmlSerializer ser = new XmlSerializer(typeof (CertificateMapping)); using (FileStream fs = new FileStream(file,FileMode.Open,FileAccess.Read)) { _map = (CertificateMapping) ser.Deserialize(fs); } _certmapDateTime = fileDate; } _certmap = _map[cert.GetCertHashString()]; if (_certmap == null) { throw new ApplicationException("The certificate " + cert.GetCertHashString() + " has not been configured."); }_ident = new CertificateIdentity(cert);
}Principal 實作的其他部分將提供方法,讓您存取 XML 檔中已設定的角色成員,並提供屬性以擷取 Identity 物件:
public boolIsInRole
(string role) { return _certmap.Roles.Contains(role); } public System.Security.Principal.IIdentity Identity { get { return _ident; } } }CertificateIdentity 類別包含 IIdentity 的模版實作以及一個額外的屬性,好讓您存取用來簽署訊息的 X509Certificate 物件。
public class CertificateIdentity: IIdentity { private X509Certificate _x509cert; internal CertificateIdentity(X509Certificate cert) { _x509cert = cert; } public bool IsAuthenticated { get { return true; } } public string Name { get { return _x509cert.GetName(); } } public string AuthenticationType { get { return "X.509"; } } public X509Certificate Certificate { get { return _x509cert; } } }開始匯總
要完成所需功能的最後一個步驟,就是使用伺服器應用程式註冊新建立的語彙基元管理員。安裝 Web Services Enhancements 2.0 之後,您可以使用兩種方法來進行註冊。您可以直接編輯 web.config 檔案,或是使用我在 [圖 1] 中所介紹的 WSE 2.0 設定工具。
若要在 Visual Studio .NET 中使用 WSE 2.0 設定工具,請在 Web 服務專案上按一下滑鼠右鍵,並選擇 WSE Settings 2.0。此時會出現 [圖 3] 所示的對話方塊。請務必選取其中的兩個核取方塊。
[圖 3] 為 Web 服務專案啟用 WSE 和擴充的處理管線
接著您可以切換至 [安全性] 標籤,並在此指定語彙基元管理員的名稱,如 [圖 4] 所示。
[圖 4] 新增語彙基元管理員和檢查憑證設定 (按一下即可放大影像)。
若要新增語彙基元管理員,請按一下 [新增...] 並填入資料,如 [圖 5] 所示。請注意,您必須將 [型別] 項目的格式指定為:
<namespace>.<classname>, <assemblyname>
因此,本範例的完整型別名稱為「RoleBasedSecurityExtension.X509RoleBasedSecurityTokenManager, RoleBasedSecurityExtension」。
[圖 5] 新增語彙基元管理員
最後一個步驟是在 [原則] 標籤上設定原則檔案,如 [圖 6] 所示。我們將約略介紹此原則的內容。
[圖 6] 設定原則檔案名稱 (按一下即可放大影像)。
將安全性語彙基元管理員、原則檔案、自訂 Principal 及自訂 Identity 物件結合在一起之後,您就可以利用 WS-Policy,依完全描述式的方式來使用角色架構安全性。
只要一行程式碼
在本文的開頭,我已經示範兩種不同的方法,讓您在典型的 ASP.NET 應用程式中使用角色架構安全性:第一個方法是根據明示程式碼來檢查角色,另一個方法則是使用宣告式安全性與 .NET 屬性,來指定方法的安全性需求。
在使用 WSE 2.0 的 Web 服務應用程式中會有兩種相似的可能性。其主要差別在於我們不會使用 .NET 屬性來指派方法的安全性需求,而是使用 WS-Policy 功能。選擇 WS-Policy 功能的原因在於它能提供一項有力的優勢:這是 XML 檔案,而您可以將此檔案傳送給正在實作用戶端的開發人員,他就能直接決定每個 Web 服務所需完成的需求。
但是,我們先來看一下使用程式碼進行的角色成員檢查。很不幸地,我們無法再輕鬆地使用 SomeContext.Current.User.IsInRole() 的相似結構,因為內送的 SOAP 訊息中可能不只一個安全性語彙基元。我所選擇的作法是提供兩個協助程式方法,第一個是檢查目前要求的安全性內容,並且尋找用來簽署完整的內送訊息本文的 X509SecurityToken。第二個協助程式是小型包裝函式,可提供與 SomeContext.Current.User.IsInRole() 一樣容易使用的程式設計介面。
public X509SecurityTokenGetBodySigningToken
(Security sec) { X509SecurityToken token = null; foreach (ISecurityElement securityElement in sec.Elements) { if (securityElement is Signature) { Signature sig = (Signature)securityElement; if ((sig.SignatureOptions & SignatureOptions.IncludeSoapBody) != 0) { SecurityToken sigToken = sig.SecurityToken; if (sigToken is X509SecurityToken) { token = (X509SecurityToken)sigToken; } } } } return token; } private boolCurrentCertificatePrincipalIsInRole
(String role) { X509SecurityToken tok = GetBodySigningToken(RequestSoapContext.Current.Security); if (tok == null) return false; if (tok.Principal == null) return false; return tok.Principal.IsInRole(role); }如此一來,您只需要一行程式碼,即可在 [WebMethod] 中使用這些協助程式,來檢查傳送者的 X.509 憑證的角色成員:
[WebMethod] public void SetMonthlySalary(long employeeID, double salary) { if (! CurrentCertificatePrincipalIsInRole ("HR")
) throw new Exception("Only members of 'HR' may call this method."); if (salary > 2000) { if (! CurrentCertificatePrincipalIsInRole("PointyHairedBoss")
) throw new Exception("Only the pointy haired boss might set " + "salaries larger than 2K"); } // ... 實際工作已經移除 ... }沒有精靈嗎?
最後還有一點尚未提到,就是使用原則檔案來宣告性指定 Web 服務的安全性需求。無庸置疑地,在此 WSE 2.0 為當前 Web 服務領域中所有可用工具提供最佳的支援!您可以再次使用 WSE Settings 2.0 對話方塊,而無須根據 WS-Policy 來手動撰寫原則檔案。在此對話方塊中,請重新切換至 [原則] 標籤,按一下 [建立/編輯],再按 [新增原則] 以開啟對話方塊,如 [圖 7] 所示。
[圖 7] 新增原則更容易了!
如您所見,在此對話方塊中若要根據 WS-Policy 來使用宣告性安全性,那麼您只需指定服務位置、選取 [需要簽章] 核取方塊、選取簽章類型、及輸入存取服務所需的角色即可。
這些設定將使 Framework 檢查所有內送的訊息,以符合原則檔案中描述的安全性需求。舉例而言,如果內送的訊息沒有取得簽章,或是憑證尚未指派給角色「Accounting」,此時程式便會將 SOAP 錯誤傳回給用戶端,告知用戶端內送的訊息不符合原則。請注意:如果出現「無法檢查憑證的信任鏈結」的例外,就表示「本機電腦」存放區中尚未安裝您的自訂憑證授權單位的根憑證。若要安裝根憑證,請前往 http://<your_certificate_server>/certsrv,在「匯入憑證」精靈中手動選取匯入位置和實體匯入位置,即可匯入根憑證。
總結
透過本文,您已瞭解如何利用 Web Services Enhancements 2.0 for Microsoft .NET,來建立和使用自訂安全性語彙基元管理員,並藉此檢查 X.509 憑證,對應憑證與角色,以及使用自訂 Principal 與 Identity 物件來填入內容資訊。您也知道在 Visual Studio .NET 中,使用 WS-Policy 將宣告性的角色成員檢查加入應用程式,是一件非常容易的事。相較於典型的 HTTP 架構安全性,使用 WS-Security 架構的方法,其優點在於不用依賴傳輸層級完整性或安全性,而是完全使用 SOAP 訊息來作業。如此可透過多重躍點和通訊協定來提供端對端安全性功能。