文章目錄
WSE(Web Services Enhancements)是微軟為了使開發人員通過.NET建立出更強大,更好用的Web Services而推出功能增強外掛程式。現在最新的版本是WSE2.0(SP2).本文描述了如何使用WSE2.0中的安全功能增強部分來實現安全的Web Services。WSE的安全功能增強實現的是WS-Security標準,此標準是WebService自己的安全性通訊協定,由IBM, BEA, Microsoft等聯合制定,所以在.NET和Java系統上都可實現。
我主要是根據WSE的文檔說明和自己使用體會(其實多半也是按照文檔畫瓢,呵呵)而寫,有錯誤之處請指出。另外,這是用的都是2.0,它與WSE1.0相比,變化很大,尤其在安全方面。
還有一點注意,其實通過WSE實現安全有兩種途徑:一種就是我們下面要介紹的通過編寫代碼的方法;另外一種是直接編寫策略檔案(XML文檔),這兩種方法本質上都是通過對傳遞的SOAP訊息進行設定,如增加使用者訊息,加解密,簽名驗證等,來實現安全功能的。但本人對第二種方法不太熟悉,也沒時間研究,就不寫了,呵呵。
一、使用使用者名稱和口令驗證Web Services調用者身份。
原理很簡單:用戶端通過SOAP擴充,在SOAP訊息中加入使用者名稱和口令(明文或加密),發送給Web Services端;服務端接到訊息後,同樣通過擴充從訊息上下文中得到使用者名稱和口令,再進行身分識別驗證和其他動作。下面是實現步驟:
用戶端:
1.添加Microsoft.Web.Services2和用戶端要訪問的Web Services引用,沒有什麼好說的。當然,這兩個引用是必須的,你可能還需要用戶端需要添加其他引用。
2.修改從Web Services引用產生的本地proxy類,這個類的代碼在引用產生Reference檔案中。從.NET開發環境右邊的方案總管裡開啟你要操作的Web Services引用檔案夾,開啟Reference.map節點,就可以看到Reference.cs或Reference.vb檔案。如果你沒有看到這些檔案,可能是沒有顯示所有檔案,你需要在方案總管或命令菜單“項目”裡設定“顯示所有檔案”。找到Reference檔案後,開啟它,在proxy類的聲明處將類的繼承父類改成Microsoft.Web.Services2.WebServicesClientProtocol.因為只有這樣,proxy類才能訪問WSE提供的SOAP擴充。注意,如果更新了Web服務的引用,則需要重新把繼承類修改。
3.通過UsernameToken類的執行個體添加使用者名稱與口令。UsernameToken屬於Microsoft.Web.Services2.Security.Tokens命名空間。假設Web Services的本地proxy類名稱為WebServer.WebService,使用者名稱為Username,使用者口令為Userpwd,則代碼可以如下所示(vb.net,下同):
'產生本地proxy類執行個體
Dim mywebserv as New WebServer.WebService
'產生UsernameToken類執行個體,將使用者名稱,使用者口令和口令發送方式寫在執行個體中
Dim untoken As UsernameToken = New UsernameToken(Username, Userpwd, PasswordOption.SendPlainText)
'設定SOAP訊息有效期間,以確減少訊息即使被其他使用者截獲後重新使用的可能性為,這裡設定為30秒,但要注意不同系統時鐘同步的問題。
mywebserv.RequestSoapContext.Security.Timestamp.TtlInSeconds = 30
'將UsernameToken執行個體加在SOAP訊息上下文中
mywebserv.RequestSoapContext.Security.Tokens.Add(untoken)
'調用Web服務的方法WebMethod
mywebserv.WebMethod
這裡需要說明口令發送方式的設定。口令發送方式為枚舉型資料:SendNone,SendHashed,SendPlainText.分別為不發送口令,發送口令雜湊值和發送口令明文,上面的例子是使用發送明文。如果選擇SendNone,表示服務端不要驗證口令,這個安全層級就很低,而且如果服務需要對傳遞的SOAP訊息簽名,則用戶端要單獨提供口令對其簽名;SendHashed是指發送的是口令的SHA-1雜湊值,這種情況下口令保護安全,這也是微軟推薦的最好方式。但它需要伺服器端通過編寫代碼和config檔案設定自己來驗證使用者身份,具體驗證方法在下面的伺服器端設定會講到;SendPlainText是口令以明文方式傳遞,如果採取這種方式,上述代碼中的UsernameToken最好加密,否則口令很容易被截獲。加密方法以後章節將詳細論述;另外,如果伺服器端選擇讓WSE自動根據Windows活動目錄域的使用者進行身分識別驗證,那這裡必須選擇發送明文。
服務端:
1.首先在Web Services的設定檔Web.config裡添加配置WSE的元素,這也是.NET開發的系統使用WSE最基本的一步。WSE文檔上說如果服務的調用端是ASP.NET系統時才需要這一步的配置,如果是普通的Client程式則不需要加這個標識。可我測試發現即使調用端是Client程式,還是需要加這個配置。下面是完整的配置文檔。
<configuration>
<system.web>
<webServices>
<soapExtensionTypes>
<add type="Microsoft.Web.Services2.WebServicesExtension, Microsoft.Web.Services2,Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" priority="1" group="0"/>
</soapExtensionTypes>
</webServices>
</system.web>
</configuration>
當然,一般情況下你只需要在Web.config的<system.web>...</system.web>之間添加<webServices>...</webServices>部分。另外,add元素的資料最好寫成一行,否則可能會出錯。
2.佈建服務端的調用身分識別驗證行為,這一步是可選的。
如前所述,調用方發過來的SOAP訊息中已加入了使用者資訊,服務端要做的工作是將這些資訊解析出來,再根據一定規則對比驗證,並返回結果。
上述處理過程也有兩種選擇:一是讓WSE自動驗證,我們自己的代碼和設定檔再也不用做什麼了。這種情況下,WSE在服務端收到調用方的SOAP訊息後,從裡面的UsernameToken中取出使用者名稱和口令,這也是為什麼前面提到過的自動驗證下使用者口令必須明文發送的原因,取出使用者名稱和口令後,WSE是基於系統所在Windows域的使用者進行判別和驗證的。也就是說,WSE從活動目錄裡的使用者列表遍曆,尋找是否存在和所接收到的使用者名稱/口令匹配的有效使用者使用者帳號,如果未找到匹配使用者,則返回調用者未被授權的錯誤。由此可見,這種方法下應用系統及使用者需要和Windows域緊密捆綁,缺乏靈活性並且不適合與已有業務系統對接。因此在實際應用中更多的應該是用下面第二種方法。
這種方法的核心是Web Services通過繼承UsernameTokenManager類,並重載AuthenticateToken方法實現的。
a.首先聲明一個從UsernameTokenManager繼承下來的類。
Public Class CustomUsernameTokenManager
Inherits UsernameTokenManager
這裡面有一個存取權限問題,為了使經過授權的程式集才能訪問這個類,你還需要給它在聲明時加上一些訪問限定。因為能夠訪問Unmanaged 程式碼(UnmanagedCode)的程式集信任層級都是比較高的,所以可以要求能訪問此類的程式集都是可以訪問UnmanagedCode的。這樣上面的聲明就變成如下形式:
<SecurityPermission(SecurityAction.Demand,Flags:= SecurityPermissionFlag.UnmanagedCode)> Public Class CustomUsernameTokenManager
Inherits UsernameTokenManager
當然你也可以配置成其他許可權要求,只要更改Flags的SecurityPermissionFlag枚舉值即可。
b.在代碼中重載AuthenticateToken方法。服務端接收到含有UsernameToken執行個體的SOAP訊息後,WSE將UsernameToken還原序列化,並調用VerifyToken方法,而VerifyToken方法在執行過程中又會調用AuthenticateToken方法,這個方法會返回一個口令值,WSE會拿它與UsernameToken中的口令進行對比。如果發送的口令為明文(UsernameToken.PasswordOption=SendPlainText),則直接對比;如果發送口令為雜湊值(UsernameToken.PasswordOption=SendHashed),則WSE會對這個傳回值做一個SHA-1雜湊運算,再將結果與UsernameToken中的口令進行對比。如果不一致,則返回使用者未被授權的錯誤。上述過程全部是自動完成的,因此我們要做的工作就是重載AuthenticateToken方法,在裡面實現返回正確的使用者口令,用於對比和驗證。這其實就相當於WSE1.0裡面的PasswordProvider.
重載AuthenticateToken方法實現的邏輯由系統根據具體要求來定,比如根據使用者名稱去資料庫裡尋找相應的使用者口令,或者從LDAP中等等。這裡就照找了WSE文檔上的例子,返回的口令和提交的使用者名稱相同。
Protected Overrides Function AuthenticateToken(ByVal userName As UsernameToken) As String
' Ensure that the SOAP message sender passed a UsernameToken.
If userName Is Nothing Then
Throw New ArgumentNullException
End If
' This is a very simple provider.
' In most production systems the following code
' typically consults an external database of (userName,hash)
' pairs. For this example, it is the UTF-8
' encoding of the user name.
Dim encodedUsername As Byte() = _
System.Text.Encoding.UTF8.GetBytes(userName.Username)
Return System.Text.Encoding.UTF8.GetString(encodedUsername)
End Function
c.配置web.config檔案,告知Web Services使用哪個類來驗證使用者身份。
首先在檔案中加入標明使用WSE2的元素:
<configuration>
<configSections>
<section name="microsoft.web.services2"
type="Microsoft.Web.Services2.Configuration.WebServicesConfiguration, Microsoft.Web.Services2, Version=2.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</configSections>
</configuration>
這裡要注意的是一定要把<configSections>...</configSections>寫到整個<configuration>...</configuration>裡的最前面,否則會出現“響應內容類型為 "text/html;charset=utf-8",但應該是"text/xml"”的錯誤。
其次配置使用UsernameTokenManager子類
<configuration>
<microsoft.web.services2>
<security>
<securityTokenManager xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"qname="wsse:UsernameToken" type="MyNamespace.CustomUsernameTokenProvider,MyAssemblyName,Version=2.0.0.0,Culture=neutral,PublicKeyToken=81f0828a1c0bb867" />
</security>
</microsoft.web.services2>
</configuration>
在這裡面type屬性的值要保持在一行,"MyNamespace.CustomUsernameTokenProvider”必須是衍生類別有效類層次名,MyAssemblyName為程式集名稱。另外Version,Culture和PublicKeyToken等屬性可以省略 。
這樣,我們就完成了在Web Service調用最基本的基於使用者名稱/口令的驗證。以後各項安全功能的實現都以它為基礎。
二、使用使用者名稱和口令對SOAP訊息簽名
上述過程雖然實現了Web Services使用者身份確認。但它不能保證Web Services接收到的SOAP訊息就是所聲明使用者所發送的,因此在實際的應用中是還需要調用方對SOAP訊息做一次簽名,並將簽名連同訊息一起發送;服務端接收到訊息後,除了驗證使用者身份外,還需要對其中的簽名做一次認證。以確定訊息在傳輸過程中沒有被更改過,而驗證的使用者就是對訊息簽名的使用者。
用戶端:
只需要在原有用戶端SOAP訊息中添加簽名即可,如下所示,黑體為新加代碼:
mywebserv.RequestSoapContext.Security.Tokens.Add(untoken)
mywebserv.RequestSoapContext.Security.Elements.Add(New _
MessageSignature(untoken))
mywebserv.WebMethod
上述代碼就是用戶端根據UsernameToken的執行個體產生一個簽名,然後把簽名加在SOAP訊息裡,具體是WS-Security擴充的SOAP訊息頭。
服務端:
其實服務端無需改動什麼。只要用戶端發送的SOAP訊息裡麵包含了簽名,服務端就會自動驗證簽名的有效性。服務端首先驗證使用者名稱/口令,然後使用用戶端傳遞的使用者名稱和自己獲得的口令(WSE自動從Windows活動目錄中獲得,或者通過重載的AuthenticateToken的方法)對簽名進行驗證。如果驗證失敗,說明訊息在傳遞過程中被更改過或者不是當前調用使用者所簽,返回相應錯誤。
WSE的協助文檔上這一塊加了一個判斷SOAP裡是否包含簽名的函數,通過它可以獲得簽名的一些相關資訊,並不是必須的。有興趣的朋友可以自己去看。
三、使用認證驗證身份並對SOAP訊息簽名
使用使用者名稱/口令存在固有缺點,因此在某些安全要求較嚴格的系統中我們還需要使用認證來完成對使用者身份的驗證。關於PKI/CA的基礎知識這裡就不介紹了,大家可以去看相關資料。
首先我們需要佈建服務端保證WSE可以訪問認證及其私密金鑰。(至於認證的申請,管理和使用屬於基礎知識,在這就不講了)這裡主要是在Web Services端設定,以保證服務端可以自動完成某些功能,還無需使用者幹預。在Web.config檔案添加如下<x509>元素(黑體部分):
<configuration>
<microsoft.web.services2>
<security>
<x509 storeLocation="CurrentUser" />
</security>
</microsoft.web.services2>
</configuration>
storeLocation屬性為WSE可訪問的憑證存放區。它可選的值有兩項:“LocalMachine”和“CurrentUser”,分別表示本機憑證存放區和目前使用者憑證存放區。這裡用的是CurrentUser,而storeLocation預設值是LocalMachine.另外幾個屬性的配置如下: verifyTrust,是否驗證所用認證的憑證鏈結有效性,預設為true;allowTestRoot:驗證憑證鏈結過程中是否認為測試根CA所簽發認證有效,這個參數在verifyTrust為true時才有效,預設為false;allowRevocationUrlRetrieval,是否線上驗證認證是否被吊銷,在verifyTrust為true時才有效,預設為true;allowUrlRetrieval,是否線上驗證憑證鏈結有效性,allowRevocationUrlRetrieval,是否線上驗證憑證撤銷狀態。
用戶端:
第1、2步與第一節使用使用者名稱/口令的步驟基本相同。但這裡也要多加一個引用:Microsoft.Web.Services2.Security.X509
3.編寫代碼取得要使用的認證。我這裡用了一個listview控制項顯示目前使用者憑證存放區裡的個人認證,以供使用者選擇。你當然也可以象WSE文檔上那樣利用認證的屬性去定位需要使用的認證。Listview控制項名稱為lv_certlist,它有兩列:認證持有人主體和頒發者名稱。
Dim cert As X509Certificate
Dim lvitem As ListViewItem
'從目前使用者憑證存放區中開啟個人認證庫
certstore = X509CertificateStore.CurrentUserStore(X509CertificateStore.MyStore)
certstore.Open()
lv_certlist.Items.Clear()
'遍曆認證,寫到listview控制項裡
For Each cert In certstore.Certificates
lvitem = lv_certlist.Items.Add(cert.GetName)
lvitem.SubItems.Add(cert.GetIssuerName)
lvitem.Tag = lvitem.Index
Next cert
4.選擇所選擇的認證產生X509SecurityToken的執行個體,它和前面的UsernameToken一樣是SecurityToken的子類。
Dim cert As X509Certificate
Dim certToken As X509SecurityToken
Dim result As String
cert = certstore.Certificates(lv_certlist.SelectedIndices(0))
’判斷所選擇認證是否支援簽名,並且私密金鑰是否存在
If cert.SupportsDigitalSignature And Not (cert.Key Is Nothing) Then
'遍曆認證,寫到listview控制項裡
certToken = New X509SecurityToken(cert)
Dim mywebserv As New WebServer.WebService
mywebserv.RequestSoapContext.Security.Timestamp.TtlInSeconds = 30
'添加包含認證資訊的X509SecurityToken
mywebserv.RequestSoapContext.Security.Tokens.Add(certToken)
'使用認證對SOAP訊息簽名,並將結果寫在訊息中
mywebserv.RequestSoapContext.Security.Elements.Add(New _
MessageSignature(certToken))
'調用Web服務的方法
mywebserv.WebMethod
End If
服務端:
和驗證使用使用者名稱/口令簽名的訊息類似,服務端也同樣自動驗證簽名有效性。同時,服務端會根據web.config檔案中<x509>元素裡的屬性設定來對認證的有效性進行驗證。如果驗證失敗,則返回相應錯誤。簽名所使用認證可以通過SoapContext.Security.Tokens得到
四、使用使用者名稱和口令對SOAP訊息加密
前面第一節講過,如果UsernameToken執行個體中的口令是明文,那最好將UsernameToken加密發送。在第一節用戶端代碼中做如下改動,黑體代碼為新加:
mywebserv.RequestSoapContext.Security.Tokens.Add(untoken)
mywebserv.RequestSoapContext.Security.Elements.Add(New _
Microsoft.Web.Services2.Security.EncryptedData (untoken))
mywebserv.WebMethod
上述代碼就是用戶端根據UsernameToken的對SOAP訊息加密,然後把密文加在SOAP訊息裡,具體是WS-Security擴充的SOAP訊息頭
服務端自動對訊息解密。服務端首先驗證使用者名稱/口令,然後使用用戶端傳遞的使用者名稱和自己獲得的口令(WSE自動從Windows活動目錄中獲得,或者通過重載的AuthenticateToken的方法)對解密資料。如果失敗,返回相應錯誤。
WSE的協助文檔上這一塊同樣加了一個判斷SOAP是否加密的函數。
五、使用認證對SOAP訊息加解密
這個略微複雜一些。根據PKI非對稱加解密原理,對訊息加密的用戶端需要事先得到作為接收方的服務端的認證公開金鑰,而服務端必須保證可以訪問其認證對應的私密金鑰。因此我們首先要在服務端選擇設定好它所使用的認證,原則就是使服務端能夠隨時並自動訪問其認證對應的私密金鑰。
對於基於ASP.NET開發的Web Services,需要給它運行時的預設使用者賦予訪問認證私密金鑰的許可權。運行在IIS 6.0上的Web Services預設使用者是Network Service,其他情況下使用者賬戶由machine.config檔案裡<processModel>元素的userName屬性決定,預設情況下是“machine”,它等同於ASPNET賬戶。添加步驟如下:運行WSE 2.0內建的X.509 Certificate tool(開始---程式---Microsoft WSE 2.0---X.509 Certificate tool),選擇並開啟WSE要訪問的認證。開啟後,“Private Key File Name”文字框裡會顯示認證所對應私密金鑰檔案的名稱,Private Key File Folder”文字框裡會顯示私密金鑰檔案的路徑。單擊“Private Key File Properties”開啟檔案屬性對話方塊,可以在安全屬性頁面裡添加被授權訪問的Network Service或ASPNET使用者賬戶。
如果開啟認證後發現私密金鑰檔案不存在或不可訪問,說明此認證有問題或者私密金鑰受保護,比如有口令保護或者儲存在安全外設中,不能被Web Services隨時自動訪問,因此也就不適用。
對於Web Services,如果使用的使用者是預設的ASPNET,那最好選擇本機儲存(LocalMachine Store)裡的認證。因為ASPNET這個使用者是安裝ASP.NET時自動產生的,它的口令是沒有辦法訪問的。如果認證包含在目前使用者儲存(CurrentUser Store)中,服務端很可能就訪問不到私密金鑰。
服務端設定好認證後,接下來就要使用戶端得到這個認證和公開金鑰。具體的方法和你的業務系統和應用需求有關係,你可以把它放在網頁上讓使用者事先下載安裝;如果應用在區域網路環境,可以讓用戶端去LDAP或CA裡取得。
用戶端做如下改動:
mywebserv.RequestSoapContext.Security.Tokens.Add(certToken)
’SOAP訊息中添加加密結果
mywebserv.RequestSoapContext.Security.Elements.Add(New _
Microsoft.Web.Services2.Security.EncryptedData(certToken))
mywebserv.WebMethod
服務端接收到加密後的SOAP訊息後,自動使用相應私密金鑰解密。如果失敗,返回相應錯誤。
可以看出,使用認證加解密訊息的痛點在於認證的選擇和配置,尤其是訊息接收端。
WSE除了支援上述使用者名稱/口令,X509認證兩種令牌外。還支援Kerberos Ticket,安全上下文令牌(Security Context Token)和使用者自訂令牌。Kerberos Ticket好象是WSE自己加的內容,標準的WS-Security裡沒有,而且只在Windows XP SP1/SP2,Windows2003上支援。安全上下文令牌需要一個上下文令牌服務產生。而使用者自訂令牌的核心是由SecurityToken派生一個使用者自己的令牌類,完成各種安全功能。總之WSE就是使用這種基於令牌和擴充SOAP訊息來實現Web Services安全的。