Duncan Mackenzie
Microsoft Developer Network
摘要:说明如何在 Microsoft Windows XP 和 Windows Server 2003 中使用 DPAPI 函数 CredUIPromptForCredentials 来检索用户凭据,从而以安全且标准的方式获取身份验证信息。
适用于:
Microsoft?.NET
Microsoft Windows XP
Microsoft Windows Server 2003
![]() |
简介 |
![]() |
存储的用户名和密码 |
![]() |
在 .NET 中创建凭据 API 类 |
![]() |
请求用户凭据 |
![]() |
使用自己的图形 |
![]() |
小结 |
有时,应用程序要求有用户提供的凭据以便访问受保护的资源,如数据库或 FTP 网站。然而,获取并存储用户的 ID 和密码可能会给您的系统带来安全风险。如有可能,您根本不应该让用户提供凭据(例如,通过对数据库使用集成身份验证),但有时这无法避免。如果您确实要求用户提供凭据,并且您的应用程序将运行在 Microsoft?Windows?XP 或 Microsoft?Windows Server 2003 上,则相应的操作系统会提供函数以使此任务变得容易一些。
Windows XP 和 Windows Server 2003 使用一种称为“存储的用户名和密码”的功能(参见图 1)将一组凭据与单个 Windows 用户帐户相关联,并使用数据保护 API (DPAPI) 来存储这些凭据。
图 1. Windows XP 中的 Credential Management 对话框
如果您的应用程序运行在 Windows XP 或 Windows .NET 上,则可以使用凭据管理 API 函数来提示用户输入凭据。这些 API 可以为您提供一致的用户界面(参见图 2),并使您可以自动支持由操作系统来缓存这些凭据。
图 2. 标准 Windows XP 凭据对话框
有关在应用程序中请求、存储和使用用户凭据所涉及问题的详细讨论,请参阅 Michael Howard 与 David LeBlanc 合著的 Writing Secure Code 一书。建议您阅读该书,以获取更多信息。在本文中,我将只是向您说明如何在 Microsoft?Visual Basic?.NET 和 C# 应用程序中使用凭据管理 API。
因为这些凭据管理函数是 Win32 API 调用,您将需要创建 extern (C#) 或 Declare (Visual Basic .NET) 定义以便访问它们。除了这些函数本身以外,还需要使用一些常数和结构来访问它们。这些常数被组织为预定义的常数组,所以我已经选择在 .NET 代码中以枚举形式实现这些常数组,以使 API 调用更加易于使用。
Private Declare Unicode _ Function CredUIPromptForCredentials _ Lib "credui" Alias "CredUIPromptForCredentialsW" _ (ByRef creditUR As CREDUI_INFO, _ ByVal targetName As String, _ ByVal reserved1 As IntPtr, _ ByVal iError As Integer, _ ByVal userName As StringBuilder, _ ByVal maxUserName As Integer, _ ByVal password As StringBuilder, _ ByVal maxPassword As Integer, _ ByRef iSave As Integer, _ ByVal flags As CREDUI_FLAGS) _ As CredUIReturnCodes Private Declare Unicode _ Function CredUIParseUserName _ Lib "credui" Alias "CredUIParseUserNameW" _ (ByVal userName As String, _ ByVal user As StringBuilder, _ ByVal userMaxChars As Integer, _ ByVal domain As StringBuilder, _ ByVal domainMaxChars As Integer) _ As CredUIReturnCodes Private Declare Unicode _ Function CredUIConfirmCredentials _ Lib "credui" Alias "CredUIConfirmCredentialsW" _ (ByVal targetName As String, _ ByVal confirm As Boolean) _ As CredUIReturnCodes Public Declare Auto _ Function DeleteObject Lib "Gdi32" _ (ByVal hObject As IntPtr) As Boolean
注:我在代码中包括了 GDI32 库中的 DeleteObject API 调用,因为如果您决定将自己的位图传递给 CredUIPromptForCredentials API,则您将需要使用该调用。当我在下文中演示自定义位图的使用时,您将可以了解该 API 的使用方法。
对于许多 Win32 API 调用,您都需要一组支持常数(在本文中,我选择将其表示为枚举),并且还可能需要一个或两个结构声明。凭据 API 也遵循上述一般性规则,并且需要多种常数和一个结构。在我的 .NET 类中,我添加了一个枚举来表示 CredUIPromptForCredentials 的标志参数,添加了另外一个枚举来表示全部三个凭据 API 调用可能产生的返回代码集,并添加了一个 CREDUI_INFO 结构声明。
<Flags()> Public Enum CREDUI_FLAGS INCORRECT_PASSWORD = &H1 DO_NOT_PERSIST = &H2 REQUEST_ADMINISTRATOR = &H4 EXCLUDE_CERTIFICATES = &H8 REQUIRE_CERTIFICATE = &H10 SHOW_SAVE_CHECK_BOX = &H40 ALWAYS_SHOW_UI = &H80 REQUIRE_SMARTCARD = &H100 PASSWORD_ONLY_OK = &H200 VALIDATE_USERNAME = &H400 COMPLETE_USERNAME = &H800 PERSIST = &H1000 SERVER_CREDENTIAL = &H4000 EXPECT_CONFIRMATION = &H20000 GENERIC_CREDENTIALS = &H40000 USERNAME_TARGET_CREDENTIALS = &H80000 KEEP_USERNAME = &H100000 End Enum Public Enum CredUIReturnCodes As Integer NO_ERROR = 0 ERROR_CANCELLED = 1223 ERROR_NO_SUCH_LOGON_SESSION = 1312 ERROR_NOT_FOUND = 1168 ERROR_INVALID_ACCOUNT_NAME = 1315 ERROR_INSUFFICIENT_BUFFER = 122 ERROR_INVALID_PARAMETER = 87 ERROR_INVALID_FLAGS = 1004 End Enum Public Structure CREDUI_INFO Public cbSize As Integer Public hwndParent As IntPtr Public pszMessageText As String Public pszCaptionText As String Public hbmBanner As IntPtr End Structure
此步骤不是必需的。您可以简单地将 API 声明为 Public(而不是像我的代码一样,使其声明为 Private),并从应用程序中直接调用它们。不过,我发现调用 API 经常需要完成一些工作,因而我喜欢通过包装 API 调用来向代码的最终用户隐藏这些调用细节。
Private Const MAX_USER_NAME As Integer = 100 Private Const MAX_PASSWORD As Integer = 100 Private Const MAX_DOMAIN As Integer = 100 Public Shared Function PromptForCredentials( _ ByRef creditUI As CREDUI_INFO, _ ByVal targetName As String, _ ByVal netError As Integer, _ ByRef userName As String, _ ByRef password As String, _ ByRef save As Boolean, _ ByVal flags As CREDUI_FLAGS) _ As CredUIReturnCodes Dim saveCredentials As Integer Dim user As New StringBuilder(MAX_USER_NAME) Dim pwd As New StringBuilder(MAX_PASSWORD) saveCredentials = Convert.ToInt32(save) creditUI.cbSize = Marshal.SizeOf(creditUI) Dim result As CredUIReturnCodes result = CredUIPromptForCredentials( _ creditUI, targetName, _ IntPtr.Zero, netError, _ user, MAX_USER_NAME, _ pwd, MAX_PASSWORD, _ saveCredentials, flags) save = Convert.ToBoolean(saveCredentials) userName = user.ToString password = pwd.ToString Return result End Function Public Shared Function ParseUserName
(ByVal userName As String, _ ByRef userPart As String, _ ByRef domainPart As String) _ As CredUIReturnCodes Dim user As New StringBuilder(MAX_USER_NAME) Dim domain As New StringBuilder(MAX_DOMAIN) Dim result As CredUIReturnCodes result = CredUIParseUserName(userName, _ user, MAX_USER_NAME, _ domain, MAX_DOMAIN) userPart = user.ToString() domainPart = domain.ToString() Return result End Function Public Shared Function ConfirmCredentials
(ByVal target As String, _ ByVal confirm As Boolean) As CredUIReturnCodes Return CredUIConfirmCredentials(target, confirm) End Function
注:为了便于使用,我已经使我的所有函数都成为 Shared/Static 函数。因为它们没有将任何信息存储为类级别属性或变量,所以实在没有任何理由强制开发人员在调用这些函数之前创建类实例。
在声明并包装这些 API 之后,我建议将代码放入它自己的程序集中,方法是在 Microsoft?Visual Studio?.NET 中创建一个 Class Library 项目(就像我在本文随附的示例中所做的那样)并且将代码编译为它的唯一类。通过根据这些代码创建程序集,以后可以从需要这些代码的任何项目中进行引用。但是,如果您愿意,也可直接将该类包含到您的项目中。
在完整声明 API 调用、关联的枚举数和结构之后,现在就可以开始从您自己的应用程序中使用凭据 API 了。为了演示调用该 API 的各种方法,我创建了一个简单的示例应用程序,它使用 SQL 身份验证连接到本地 Microsoft?SQL Server 数据库。通常,我总是尝试对我的 SQL Server 使用集成身份验证,但为了该示例,我将假装正在使用不支持这种身份验证的数据库服务器。当您调用 CredUIPromptForCredentials 时,有多种可以设置的标志。尽管所有这些标志都在该 API 的参考页上进行了说明,我仍将对应用程序中使用的几个标志进行详细介绍。
• | ALWAYS_SHOW_UI 通知 API 调用弹出凭据对话框,即使您已经在过去输入并保存了一些凭据。如果不使用该标志并且使用持久性凭据,则当您的应用程序随后建立连接时,用户将不会看到任何提示。如果您预料用户可能在登录时使用不同的凭据信息,则这会非常有用。 |
• | EXPECT_CONFIRMATION 与 CredUIConfirmCredentials API 调用(也包含在本文前面的代码中)配合使用。要避免永久保留“坏”凭据,可以通过该标志实现一个两阶段过程。首先从用户那里获得凭据,然后尝试使用这些凭据进行连接,并且仅当连接成功时才确认这些凭据(并保留它们)。 |
• | GENERIC_CREDENTIALS 指定您只是在查找用户 ID/密码组合,而不是在查找域凭据。我只是将该 API 用于此目的,而由于要求域凭据的受保护资源通常应该由操作系统处理,因此我总是忽略该标志。 |
• | KEEP_USERNAME 修改凭据对话框的用户界面,以便只能输入密码。在某些情况下,如使用数据库密码集连接到 Microsoft?Access 数据库,此时用户 ID 已经固定(或者,如果使用 Access 数据库密码,则用户 ID 不存在),该标志有助于在凭据用户界面中表示这一情况。 |
• | SHOW_SAVE_CHECK_BOX 确保凭据对话框包含一个复选框,以使用户能够对存储这些凭据进行控制。用户的选择将在 CredUI.PromptForCredentials 的 save (boolean) 参数中返回。 |
以下为一个示例,该示例使用凭据 API 和上述标志在连接到 SQL 数据库之前请求密码。首先,我创建了 CREDUI_INFO 结构,并用我的目标(父窗口)和一个标题字符串填充了该结构。
Dim host As String = "MyServer" Dim info As New CREDUI_INFO() With info .hwndParent = Me.Handle .pszCaptionText = host .pszMessageText = _ String.Format("Please Enter Credentials for {0}", host) End With
接下来,我为这一 CredUIPromptForCredentials 调用指定一些标志,在此,这些标志将告诉 API 以下信息:我将请求普通凭据(而不是域凭据);我希望用户界面包含 Save 复选框;即使用户以前已经输入并保存了一组凭据,对话框仍然应该出现;在保留任何凭据之前,都将通过 CredUIConfirmCredentials API 提供确认。
Dim flags As CREDUI_FLAGS flags = CREDUI_FLAGS.GENERIC_CREDENTIALS Or _ CREDUI_FLAGS.SHOW_SAVE_CHECK_BOX Or _ CREDUI_FLAGS.ALWAYS_SHOW_UI Or _ CREDUI_FLAGS.EXPECT_CONFIRMATION Once I have my flags and CREDUI_INFO structure,
I then call the PromptForCredentials API call,
passing in all of my information. Dim result As CredUIReturnCodes result = CredUI.PromptForCredentials(info, _ host, 0, _ userid, password, savePwd, flags)
如果我尚未指定 CREDUI_FLAGS.ALWAYS_SHOW_UI,则仅当该特定目标没有可用的已存储凭据时,凭据对话框才会出现。一般情况下,这意味着(没有该标志)对话框仅当您首次调用该 API 时才会出现,从而提供良好的用户体验。无论该对话框是否出现,该 API 调用都将返回指示成功或失败的结果代码,您在使用返回的用户 ID 和密码值以前,都应该对该结果代码进行检查。
在我的代码中,我在尝试进行数据库连接之前,检查值是否为 NO_ERROR(可以将其更为确定地描述为 SUCCESS)。如果该 API 调用返回 NO_ERROR,但数据库连接失败,则我使用 ConfirmCredentials 调用通知凭据系统这些凭据无效,因而不应该保留。如果数据库连接成功,则我使用 ConfirmCredentials 调用通知凭据系统该用户 ID 和密码组合有效,并且应该保留。
Dim connString As String Dim password, userid As String Dim selectAuthors As String = _ "Select au_id, au_lname, au_fname From authors" If result = CredUIReturnCodes.NO_ERROR Then connString = String.Format( _ "Password={1};User ID={0};" & _ "Initial Catalog=pubs;" & _ "Data Source=MyServer", _ userid, password) Dim conn As New SqlConnection(connString) Try conn.Open() CredUI.ConfirmCredentials(host, True) Catch sqlEx As SqlException If sqlEx.Number = 18456 Then MsgBox("Authentication Failed") CredUI.ConfirmCredentials(host, False) End If Catch ex As Exception MsgBox("Connection Error") CredUI.ConfirmCredentials(host, False) End Try If conn.State = ConnectionState.Open Then Dim cmdAuthors As New SqlCommand( _ selectAuthors, _ conn) Dim daAuthors As New SqlDataAdapter(cmdAuthors) Dim dtAuthors As New DataTable("Authors") daAuthors.Fill(dtAuthors) retrievedData.SetDataBinding(dtAuthors.DefaultView, "") End If ElseIf result <> CredUIReturnCodes.ERROR_CANCELLED Then MsgBox("There was an error in authentication") End If
凭据对话框已经相当漂亮了(参见图 2),它非常不错并且“一致”,但如果您希望使其更个性化一点,我也能够理解。您很幸运,您可以通过 PromptForCredentials API 提供自己的 320x60 图形(参见图 3),以取代默认图像。
图 3. 向标准凭据对话框中添加了自定义图形。
在您自己的代码中完成该操作相当容易,因为 System.Drawing.Bitmap 类提供了一个便利的方法 GetHbitmap 来获取基础图形的本机句柄。您需要创建 System.Drawing.Bitmap 的实例,然后将 CREDUI_INFO 结构的 .hbmBanner 成员设置为该位图的本机句柄。在使用完该 CREDUI_INFO 结构(已经对 PromptForCredentials 进行了调用)之后,需要释放该本机句柄,以避免任何形式的内存泄漏。要释放该句柄,需要使用另一个 API 调用,即 DeleteObject。我已经在我的 CredUI 类中包含了该 API 调用,以使自定义位图的使用变得尽可能地容易。
Dim credBmp As New Bitmap("d:\credui.bmp") Dim info As New CREDUI_INFO() With info .hwndParent = Me.Handle .pszCaptionText = host .pszMessageText = _ String.Format("Please Enter Credentials for {0}", host) .hbmBanner = credBmp.GetHbitmap() End With 'make call to PromptForCredentials '... CredUI.DeleteObject(info.hbmBanner)
为了与 Windows 保持一致,建议您仍然使用默认图形,但能够在需要的时候自定义对话框也很不错。我从示例代码中的一个文件中加载了该图像,但如果您有这方面的爱好,也可以使用 GDI+ 临时绘制该图像。
如果您需要提示用户输入数据库凭据、网站凭据或任何其他凭据组,则完成该任务的最佳方式是使用内置的操作系统功能。除了为用户提供一致的用户界面以外,您还可以因为操作系统缓存与已登录用户关联的凭据而受益。如果您需要有关开发过程中安全问题的详细信息,请参阅由 Michael Howard 与 David LeBlanc 合著的 Writing Secure Code 一书。