概述
Windows Vista 在平台集成方面為開發人員提供了許多新的機會。新的憑據提供程式模型是變動最大的方面之一,由於它的出現,實現作業系統支持的新用戶身份驗證方案變得容易了許多。它已取代了 GINA(圖形標識與身份驗證)模型,而直言不諱地說,後者一向因為開發人員難以理解和實現以及昂貴的 Microsoft支持費用而廣為詬病。那么 Windows® 登錄外掛程式接口的一個變化竟會如此令人興奮,其原因何在?用戶打開計算機時首先看到的是登錄螢幕。由於登錄體驗是由憑據提供程式來控制和管理的,這使得自定義登錄體驗以及集成最符合組織需要的身份驗證方法變得容易了許多。簡而言之,憑據提供程式為開發和實現更好、更可靠的安全性提供了一種更容易的方式。
兩種體系結構比較
在 Windows Vista™ 之前的環境中,每個會話都有一個 winlogon 實例,它負責控制該會話的互動式登錄序列。(圖 1 顯示了 Windows XP 和 Windows Server® 2003 舊的登錄體系結構。)在剛啟動的系統中,控制台位置的互動式登錄始終在會話 0 中執行。會話 0 承載運行系統服務以及其他關鍵進程,包括“本地安全機構”(Local Security Authority) 進程。(換句話說,在會話 0 中運行的許多進程都沒有在圖 1 中顯示出來。)圖 1 GINA 登錄體系結構
計算機上已註冊的 GINA 載入到 winlogon 進程空間中。(還可能載入一個稱作“GINA 連結”的配置,但測試和支持這樣的複雜配置很困難。)最後,GINA 調用 LogonUser 以及相關的身份驗證 API。
在 Windows Vista 中,會話 0 不再用於互動式登錄(請參見圖 2)。這有利於提高安全性,因為已有一個會話邊界將所有的計算機進程與各個用戶的進程分隔開來。此外,對核心全局命名空間的控制也更加嚴格,因為默認情況下由用戶應用程式創建的對象已不在核心全局命名空間之內。
圖 2 新的登錄體系結構
除會話0 之外的每一個會話仍會有一個 winlogon 實例。圖 2 顯示,系統中已註冊了幾個憑據提供程式,並已通過新的 LogonUI 進程載入。
在由哪個組件負責顯示登錄圖形界面方面也有一個重要的改動。以前,這是由 GINA 來處理的,因此,顯示界面的工作可能一直由第三方組件來完成。在新的體系結構中,這是由作業系統的一個內置組件 LogonUI 來負責完成的。
那么每個提供程式的用戶提示行為在新的模型中是如何實現的呢?“憑據提供程式”體系結構要求每個提供程式都要列舉說明它所需要的 UI 元素。例如,在某個指定的方案中,提供程式可能會向 LogonUI 表明它需要兩個編輯框、兩個標題、一個複選框和一個點陣圖。然後,LogonUI 為憑據提供程式顯示這些控制項。這對實現以前討論的目標大有幫助,即用一致的外觀和方法來廣泛支持不斷修改完善的用戶驗證方案。
負責“憑據提供程式”開發的 Microsoft 開發團隊原以為外部開發人員會更願意基於 COM 來開發外掛程式模型。然而,在 Windows Vista 開發周期的早期階段,新接口最初的內部設計(類似於 GINA)完全基於LoadLibrary 和函式指針。之後,基於 COM 的重新設計吸取了第一次的教訓,使得設計出來的界面更加簡潔和易用。
混合憑據提供程式
此新外掛程式模型的計時功能臻於完美(當然,或許早就應該有這樣的功能了)。開發人員可以更輕鬆地滿足多因素身份驗證方案需求,同時提供與 Microsoft 原有的相一致的登錄體驗。儘管如此,新的接口仍顯得相當抽象。有關它的描述說明也同樣令人費解,讓人感到乏味!要想了解它,一個讓人能提起興趣的方式就是體驗一下新憑據提供程式的設計、開發和測試過程。而且,這可以很好地彌補 Microsoft 提供的現有文檔的不足。
混合憑據的要求
混合憑據提供程式應達到以下這些要求:使之基於智慧卡運行
最大限度地增加代碼的重用
最大限度地減少額外配置和基礎結構需求
設計
ICredentialProviderCredential 接口
ICredentialProviderCredential : public IUnknown{
HRESULT STDMETHODCALLTYPE Advise(
/* [in] */ ICredentialProviderCredentialEvents *pcpce);
HRESULT STDMETHODCALLTYPE UnAdvise( void);
HRESULT STDMETHODCALLTYPE SetSelected(
/* [out] */ BOOL *pbAutoLogon);
HRESULTSTDMETHODCALLTYPE SetDeselected( void);
HRESULT STDMETHODCALLTYPE GetFieldState(
/* [in] */ DWORD dwFieldID,
/* [out] */ CREDENTIAL_PROVIDER_FIELD_STATE *pcpfs,
/* [out] */ CREDENTIAL_PROVIDER_FIELD_INTERACTIVE_STATE *pcpfis);
HRESULT STDMETHODCALLTYPE GetStringValue(
/* [in] */ DWORD dwFieldID,
/* [string][out] */ LPWSTR *ppsz);
HRESULT STDMETHODCALLTYPE GetBitmapValue(
/* [in] */ DWORD dwFieldID,
/* [out] */ HBITMAP *phbmp);
HRESULT STDMETHODCALLTYPE GetCheckboxValue(
/* [in] */ DWORD dwFieldID,
/* [out] */ BOOL *pbChecked,
/* [string][out] */ LPWSTR *ppszLabel);
HRESULT STDMETHODCALLTYPE GetSubmitButtonValue(
/* [in] */ DWORD dwFieldID,
/* [out] */ DWORD *pdwAdjacentTo);
HRESULT STDMETHODCALLTYPE GetComboBoxValueCount(
/* [in] */ DWORD dwFieldID,
/* [out] */ DWORD *pcItems,
/* [out] */ DWORD *pdwSelectedItem);
HRESULT STDMETHODCALLTYPE GetComboBoxValueAt(
/* [in] */ DWORD dwFieldID,
DWORD dwItem,
/* [string][out] */ LPWSTR *ppszItem);
HRESULT STDMETHODCALLTYPE SetStringValue(
/* [in] */ DWORD dwFieldID,
/* [string][in] */ LPCWSTR psz);
HRESULT STDMETHODCALLTYPE SetCheckboxValue(
/* [in] */ DWORD dwFieldID,
/* [in] */ BOOL bChecked);
HRESULT STDMETHODCALLTYPE SetComboBoxSelectedValue(
/* [in] */ DWORD dwFieldID,
/* [in] */ DWORD dwSelectedItem);
HRESULT STDMETHODCALLTYPE CommandLinkClicked(
/* [in] */ DWORD dwFieldID);
HRESULT STDMETHODCALLTYPE GetSerialization(
/* [out] */ CREDENTIAL_PROVIDER_GET_SERIALIZATION_RESPONSE
*pcpgsr,
/* [out] */ CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION *pcpcs,
/* [out] */ LPWSTR *ppszOptionalStatusText,
/* [out] */ CREDENTIAL_PROVIDER_STATUS_ICON
*pcpsiOptionalStatusIcon);
HRESULT STDMETHODCALLTYPE ReportResult(
/* [in] */ NTSTATUS ntsStatus,
/* [in] */ NTSTATUS ntsSubstatus,
/* [out] */ LPWSTR *ppszOptionalStatusText,
/* [out] */ CREDENTIAL_PROVIDER_STATUS_ICON *pcpsiOptionalStatusIcon);
};
ICredentialProvider接口
ICredentialProvider :public IUnknown{
HRESULT STDMETHODCALLTYPE SetUsageScenario(
/* [in] */ CREDENTIAL_PROVIDER_USAGE_SCENARIO cpus,
/* [in] */ DWORD dwFlags);
HRESULT STDMETHODCALLTYPE SetSerialization(
/* [in] */ const CREDENTIAL_PROVIDER_CREDENTIAL_SERIALIZATION
*pcpcs);
HRESULTSTDMETHODCALLTYPE Advise(
/* [in] */ ICredentialProviderEvents *pcpe,
/* [in] */ UINT_PTR upAdviseContext);
HRESULTSTDMETHODCALLTYPE UnAdvise( void);
HRESULTSTDMETHODCALLTYPE GetFieldDescriptorCount(
/* [out] */ DWORD *pdwCount);
HRESULTSTDMETHODCALLTYPE GetFieldDescriptorAt(
/* [in] */ DWORD dwIndex,
/* [out] */ CREDENTIAL_PROVIDER_FIELD_DESCRIPTOR **ppcpfd);
HRESULT STDMETHODCALLTYPE GetCredentialCount(
/* [out] */ DWORD *pdwCount,
/* [out] */ DWORD *pdwDefault,
/* [out] */ BOOL *pbAutoLogonWithDefault);
HRESULT STDMETHODCALLTYPE GetCredentialAt(
/* [in] */ DWORD dwIndex,
/* [out] */ ICredentialProviderCredential **ppcpc);
};
ICredentialProviderEvents接口
ICredentialProviderCredentialEvents: public IUnknown{
HRESULT STDMETHODCALLTYPE SetFieldState(
/* [in] */ ICredentialProviderCredential *pcpc,
/* [in] */ DWORD dwFieldID,
/* [in] */ CREDENTIAL_PROVIDER_FIELD_STATE cpfs);
HRESULT STDMETHODCALLTYPE SetFieldInteractiveState(
/* [in] */ ICredentialProviderCredential *pcpc,
/* [in] */ DWORD dwFieldID,
/* [in] */ CREDENTIAL_PROVIDER_FIELD_INTERACTIVE_STATE cpfis);
HRESULT STDMETHODCALLTYPE SetFieldString(
/* [in] */ ICredentialProviderCredential *pcpc,
/* [in] */ DWORD dwFieldID,
/* [unique][string][in] */ LPCWSTR psz);
HRESULT STDMETHODCALLTYPE SetFieldCheckbox(
/* [in] */ ICredentialProviderCredential *pcpc,
/* [in] */ DWORD dwFieldID,
/* [in] */ BOOL bChecked,
/* [in] */ LPCWSTR pszLabel);
HRESULT STDMETHODCALLTYPE SetFieldBitmap(
/* [in] */ ICredentialProviderCredential *pcpc,
/* [in] */ DWORD dwFieldID,
/* [in] */ HBITMAP hbmp);
HRESULT STDMETHODCALLTYPE SetFieldComboBoxSelectedItem(
/* [in] */ ICredentialProviderCredential *pcpc,
/* [in] */ DWORD dwFieldID,
/* [in] */ DWORD dwSelectedItem);
HRESULT STDMETHODCALLTYPE DeleteFieldComboBoxItem(
/* [in] */ ICredentialProviderCredential *pcpc,
/* [in] */ DWORD dwFieldID,
/* [in] */ DWORD dwItem);
HRESULT STDMETHODCALLTYPE AppendFieldComboBoxItem(
/* [in] */ ICredentialProviderCredential *pcpc,
/* [in] */ DWORD dwFieldID,
/* [string][in] */ LPCWSTR pszItem);
HRESULT STDMETHODCALLTYPE SetFieldSubmitButton(
/* [in] */ ICredentialProviderCredential *pcpc,
/* [in] */ DWORD dwFieldID,
/* [in] */ DWORD dwAdjacentTo);
HRESULT STDMETHODCALLTYPE OnCreatingWindow(
/* [out] */ HWND *phwndOwner);
};
混合憑據提供程式調用序列
1. [The system boots]2. [LogonUI.exe process is created]
3. [Credential provider DLLs are loaded]
4. Provider::CreateInstance
5. [User presses Ctrl+Alt+Del]
6. Provider::SetUsageScenario (CPUS_LOGON)
7. Credential::Initialize
8. Provider::Advise
9. Provider::GetCredentialCount
10. Provider::GetCredentialAt (dwIndex = 0)
11. Provider::GetFieldDescriptorCount
12. Provider::GetFieldDescriptorAt (dwIndex =0)
13. Provider::GetFieldDescriptorAt (dwIndex =1)
14. Provider::GetFieldDescriptorAt (dwIndex =2)
15. Provider::GetFieldDescriptorAt (dwIndex =3)
16. Provider::GetFieldDescriptorAt (dwIndex =4)
17. Credential::GetBitmapValue (dwFieldID =0; tile image)
18. Credential::GetStringValue (dwFieldID =1; user name field)
19. Credential::GetFieldState (dwFieldID = 1;user name field)
20. Credential::GetStringValue (dwFieldID =2; password field)
21. Credential::GetFieldState (dwFieldID = 2;password field)
22. Credential::GetSubmitButtonValue(dwFieldID = 3; submit button)
23. Credential::GetFieldState (dwFieldID = 3;submit button)
24. Credential::GetStringValue (dwFieldID =4; domain name field)
25. Credential::GetFieldState (dwFieldID = 4;domain name field)
26. Credential::Advise
27. Credential::GetSerialization
28. Credential::UnAdvise
29. Provider::UnAdvise
30. [The WinLogon process calls LogonUser]
31. Credential::Advise
32. Credential::ReportResult (ntsStatus = 0)
33. Credential::UnAdvise
首先,winlogon啟動控制台會話 LogonUI 進程。創建後,LogonUI 枚舉在HKLM\Software\Microsoft\Windows\CurrentVersion\Auntication\Credential Providers下註冊的所有憑據提供程式。每個提供程式 DLL 會被載入,並接收到一個Provider::CreateInstance 調用。對於混合憑據提供程式,這將創建一個CHybridProvider。(請參見步驟 1 到4。)
用戶將看到登錄螢幕。假定用戶按了 Ctrl+Alt+Delete 並且每個提供程式都收到了Provider::SetUsageScenario CPUS_LOGON 通知。這向提供程式表明,用戶想進行互動式登錄。該混合憑據提供程式將嘗試從所插入的任何智慧卡中讀取憑據。如果找到了一個可讀的智慧卡,會將一個CHybridCredential 實例化並將其與當前的CHybridProvider 關聯。然後將有一個對Credential::Initialize 的調用。(請參見步驟5 到 7。)
LogonUI 隨後為每個載入的提供程式調用Provider::Advise。Advise 的目的是為提供程式提供一種機制,將對可見的 UI 元素(當前還未創建)所做的任何預期的更改通知給 LogonUI。內置的智慧卡提供程式給出了關於如何使用該機制的一個很好的例子。初始化之後,無論何時插入卡都會增大可用憑據數,而取出卡則會減小該數字。發生此類情況時,將通過這種機制通知 LogonUI:
複製代碼
ICredentialProviderEvents : public IUnknown
{
HRESULT STDMETHODCALLTYPE CredentialsChanged(
/* [in] */ UINT_PTR upAdviseContext);
};
為簡單起見,混合憑據提供程式不對卡的插入和取出進行動態處理。因此,它不跟蹤通過 Advise 傳遞給它的 ICredentialProviderEvents 接口。
LogonUI 執行的下一個接口調用是調用 Provider::GetCredentialCount,即步驟 9。如果創建了混合憑據(由於插入的智慧卡),混合憑據提供程式將執行一些操作。它首先將 GetCredentialCount *pdwCount 輸出參數設定為 1。該值指的是提供程式要枚舉的憑據圖塊數。(混合憑據提供程式只能處理 1 個。)首次安裝 Windows Vista 並加入域時,可以根據顯示的圖塊數推斷 Microsoft 密碼憑據提供程式將什麼 pdwCount 值返回到 LogonUI。
混合憑據提供程式然後將GetCredentialCount *pdwDefault 輸出參數設定為 0。該值是一個從 0 開始的索引值,用於對每個提供程式假定要維護的憑據數組進行索引。如何實現提供程式跟蹤其憑據由實施人員來完成,而在一組給定憑據對象的生存期內會一直對索引進行維護。
多個提供程式枚舉一個默認憑據是完全可能的。例如,在當前的方案中,可以預期內置的密碼憑據提供程式將枚舉它自己的一個默認憑據。LogonUI 如何提示用戶從多個默認和非默認憑據中作出選擇而不會使用戶無從下手?一般來講,對於每一個憑據,都會向用戶顯示一個圖塊,並且會將焦點設定到代表默認憑據的那個圖塊。在存在多個默認憑據的情況下,實際的默認憑據是在枚舉各個默認憑據時通過一系列優先規則選出的。對於各個憑據而言,如果已有一個沒有自動登錄的默認憑據,並且此憑據將要執行自動登錄,則它將成為默認憑據。如果此憑據來自最後登錄 (LLO) 提供程式並且尚還沒有自動登錄的默認憑據,則此憑據將成為默認憑據。最後,如果還沒有默認憑據,則此憑據將成為默認憑據。儘管說了這么多,我的混合憑據提供程式的自動登錄語義使得該討論沒有什麼實際意義。只要枚舉的混合憑據包含有效的登錄信息,用戶就永遠都看不到任何圖塊。
我已提到了與優先規則有關的最後登錄提供程式,但應指出,LLO 的意義會根據用戶是否正在登錄或者它是否是登錄後的情況(如桌面鎖定或密碼更改)而變化。登錄時,LLO 提供程式是用於最後的控制台登錄的最後一個提供程式。登錄後,LLO 提供程式只是用於登錄到那個會話的提供程式。其原則就是如果始終用智慧卡登錄,則智慧卡憑據提供程式默認圖塊將在重新啟動後成為默認憑據。但如果因智慧卡丟失而使用密碼登錄,則解鎖時密碼憑據提供程式的圖塊將成為該會話的默認憑據。
混合憑據提供程式始終都會將*pbAutoLogonWithDefault 輸出參數設定為 TRUE。這用於向 LogonUI 傳送通知,指示它應立即查詢此提供程式的默認憑據以獲得登錄信息,而無需先向用戶傳送提示。請注意,通過使用可以存儲在註冊表中的密碼自動登錄信息(可選),內置的密碼憑據提供程式也具有相同的功能。實際上,如果 Windows Vista 檢測到該計算機上只有一個用戶並且沒有密碼,則這就是默認行為。對於有多個憑據提供程式將 *pbAutoLogonWithDefault 設定為 TRUE 的情況,LogonUI 的行為尚不明確。
執行了 GetCredentialCount 調用之後,LogonUI 將調用 Provider::GetCredentialAt。對於混合憑據提供程式,此例程最多調用一次,它反映此提供程式的最大憑據計數。作為回響,提供程式會返回該憑據實例的一個與請求的索引對應的 ICredentialProviderCredential 指針。
接下來,LogonUI 調用 Provider::GetFieldDescriptorCount,提供程式通過此調用返回在其憑據中可以找到的 UI 元素的最大數目。例如,我的密碼憑據提供程式示例有五個域:一個點陣圖、一個用戶名輸入域、一個密碼輸入域、一個提交按鈕和一個域名輸入域。即使實際上這些元素從不顯示,您仍可以看到混合憑據提供程式中保存了這些元素。這將完成圖 5 中的步驟 11。
LogonUI 然後將為每個 UI 元素分別調用一次 Provider::GetFieldDescriptorAt,以便檢索其類型。例如,執行了對應於點陣圖索引的調用之後,該示例將返回 CREDENTIAL_PROVIDER_FIELD_TYPE CPFT_TILE_IMAGE。混合憑據提供程式中未使用的一個功能是與唯讀文本域相對的可寫文本域。如果修改了混合憑據提供程式來提示用戶輸入智慧卡 PIN,則此功能將通過CPFT_PASSWORD_TEXT 來完成。可以顯示從智慧卡讀取的用戶名,以便提供某些上下文來提示用戶輸入信息。但就技術而言,用戶名應為唯讀,因為它已綁定到同樣存儲在卡上的密碼。因此,可能會使用 CPFT_LARGE_TEXT 欄位類型(與 CPFT_EDIT_TEXT 相對)。(有關選項的完整列表,請參見 credentialprovider.h。)
完成欄位描述符的枚舉之後,LogonUI將根據每個憑據欄位的類型對憑據提供程式執行一系列調用。例如,對於 CPFT_TILE_IMAGE 欄位類型,LogonUI 接下來將調用 Credential::GetBitmapValue。對於 CPFT_LARGE_TEXT 之類的用於用戶名編輯框的文本值,隨後會調用 Credential::GetStringValue 和 Credential::GetFieldState。
由於已從智慧卡讀取了我的混合憑據提供程式所需的所有登錄信息(用戶名、密碼和域名),此時可以獲得對應於每個文本欄位的字元串,這些字元串通過 GetStringValue 的 ppwz 輸出參數返回。此時,作為對 GetStringValue 的回響,其他提供程式則可能會返回 NULL 字元串值,因為用戶還沒來得及鍵入任何內容。請注意可能造成混淆的一點:文本欄位名是通過 GetFieldDescriptorAt 檢索的,而欄位中當前的文本值則是通過 GetStringValue 檢索的。(欄位名或欄位標籤將顯示為空編輯控制項中的提示文本。)
在完全描述了各種 UI元素之後,LogonUI 將調用 Credential::Advise。(請參見步驟 26。)它與前面調用的 Provider::Advise 接口的作用類似;每個憑據都可以將影響 LogonUI 的 UI 元素狀態的相關更改異步通知給 LogonUI。例如,在取消選擇示例密碼憑據提供程式的某一憑據圖塊時,提供程式便使用這一機制。在這種情況下,憑據對象將使用 ICredentialProviderCredentialEventsSetFieldString來清除密碼域。這與在 Windows XP 登錄螢幕中只鍵入部分密碼然後便暫停時所發生的情況類似。最終,登錄對話框將逾時,文本將被清除。