Аннотация
Демонстрируется, как получать удостоверений (credentials)
пользователя в Microsoft Windows XP и Windows Server 2003 с помощью
DPAPI-функции CredUIPromptForCredentials, позволяющей запрашивать
аутентификационную информацию стандартным и защищенным способом.
Относится к:
Microsoft® .NET
Microsoft Windows XP
Microsoft Windows Server 2003
Скачать исходный код к этой статье.
Содержание
Введение
Хранимые имена пользователей и пароли
Создание класса для работы с Credential API в .NET
Получение удостоверений пользователя
Использование собственных изображений
Резюме
Введение
Иногда приложению требуются удостоверения пользователя для доступа к
защищенному ресурсу вроде базы данных или FTP-сайта. Однако получение и
хранение идентификатора и пароля пользователя связано с риском нарушения
защиты системы. По возможности вообще не следует иметь дело с
удостоверениями (учетными данными) пользователя (например при
использовании интегрированной аутентификации в базе данных), но иногда
этого не избежать. Если вам необходимо получить от пользователя учетные
данные, и ваше приложение выполняется под управлением Microsoft®
Windows® XP или Microsoft® Windows Server 2003, то
вы можете воспользоваться предоставляемыми системой функциями и
облегчить эту задачу.
Хранимые имена пользователей и пароли
В Windows XP и Windows Server 2003 для сопоставления набора учетных
данных с одной учетной записью Windows применяется средство "хранимые
имена пользователей и пароли" (рис. 1), сохраняющее эти учетные данные с
использованием
Data Protection API (DPAPI).
Рис. 1. Диалоговые окна для управления учетными данными в Windows
XP
Если ваше приложение выполняется в Windows XP или Windows .NET, вы
можете использовать функции Credential Management API, запрашивающие у
пользователя учетные данные. Применение этих API-функций позволит
реализовать согласованный UI (рис. 2) и автоматически кэшировать учетные
данные в операционной системе.
Рис. 2. Стандартное диалоговое окно Windows XP для ввода учетных
данных
Проблемы, связанные с получением, хранением и использованием учетных
данных в приложении подробно обсуждаются в книге Майкла Говарда (Michael
Howard) и Дэвида Лебланка (David LeBlanc)
Writing Secure
Code. Если вас интересует дополнительная информация, советую
прочесть эту книгу. А в своей статье я просто продемонстрирую
использование Credential Management API в приложениях на Microsoft®
Visual Basic® .NET и C#.
Создание класса для работы с Credential API в .NET
Объявление API-функций
Так как функции Credential Management входят в 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
Примечание Я добавил API-функцию DeleteObject из библиотеки
GDI32, так как она потребуется, если вы передадите собственную битовую
карту функции CredUIPromptForCredentials. Применение этой функции
демонстрируется далее в статье в примере, использующем собственную
битовую карту.
Объявления констант и структур
Для многих Win32-функций нужен набор вспомогательных констант
(которые в нашем случае я реализовал как перечислимые) и, возможно, пара
структур. Не являются исключением и функции Credential Management - им
требуется множество констант и одна структура. В своем .NET-классе я
добавил перечислимое для параметра flag функции
CredUIPromptForCredentials, еще одно перечислимое для набора возможных
возвращаемых значений всех трех функций API Credential Management и
объявление структуры 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 EnumPublic 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 EnumPublic Structure CREDUI_INFO
Public cbSize As Integer Public hwndParent As IntPtr
<MarshalAs(UnmanagedType.LPWStr)> Public pszMessageText As String
<MarshalAs(UnmanagedType.LPWStr)> Public pszCaptionText As String
Public hbmBanner As IntPtr
End Structure
Создание функций - оболочек API-вызовов
Этот этап не обязателен. Вы можете просто объявить 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 FunctionPublic 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 FunctionPublic 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 (как это сделал я с примерами к статье) и откомпилировать его.
Создав сборку на основе этого кода, вы сможете ссылаться на нее из
любого проекта, где она нужна. Хотя при желании можно просто включить
класс в проект.
Получение удостоверений от пользователя
Объявив API-функции, а также связанные с ними перечислимые и
структуры, можно использовать API в приложении. Для демонстрации
различных способов применения API я создал простое приложение-пример,
которое соединяется с локальной базой данных Microsoft® SQL
Server, используя SQL-аутентификацию. В обычных условиях я применял бы
интегрированную аутентификацию SQL Server, но в демонстрационных целях
притворюсь, будто мой сервер не поддерживает такую аутентификацию. При
вызове CredUIPromptForCredentials можно установить множество флагов.
Хотя все они документированы в справке для этой API-функции, я расскажу
о некоторых из них подробнее.
- ALWAYS_SHOW_UI указывает API-функции выводить диалоговое окно
для ввода учетных данных, даже если вы уже получили и сохранили
ранее какие-то учетные данные. Без этого флага пользователь не
увидит диалоговое окно, если какие-то данные уже сохранены. Это
очень полезно, если вы предполагаете, что учетные данные могут
меняться.
- EXPECT_CONFIRMATION используется в сочетании с
CredUIConfirmCredentials (содержащейся в коде, приведенном выше).
Этот флаг разрешает двухэтапный процесс, препятствующий сохранению
"неправильных" учетных данных. Сначала вы получаете учетные данные
от пользователя, а затем пытаетесь соединиться по этим данным, и
подтверждаете (и сохраняете) их, только если соединение успешно.
- GENERIC_CREDENTIALS указывает, что вас интересует только
сочетание идентификатора пользователя и пароля, в противоположность
учетным данным домена. Я использую API только с этим флагом, так как
защищенные ресурсы, требующие учетных данных домена обычно
обслуживаются операционной системой.
- KEEP_USERNAME изменяет интерфейс диалогового окна так, чтобы
можно было ввести только пароль. В некоторых случаях, например при
соединении с базой данных Microsoft® Access с
установленным Database Password, идентификатор пользователя
фиксирован (или не существует, как в случае с Database Password в
Access), и флаг помогает отразить этот факт в UI.
- SHOW_SAVE_CHECK_BOX добавляет в диалоговое окно флажок,
позволяющий управлять хранением учетных данных. Выбранный
пользователем режим возвращается в булевом параметре save
CredUI.PromptForCredentials.
Далее приведен пример, использующий Credential API и приведенные выше
флаги для запроса пароля перед соединением с базой данных. Вначале я
создаю структуру 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, в
данном случае задающие, что мне нужны универсальные (а не доменные)
учетные данные, что интерфейс должен содержать флажок Save, что
диалоговое окно должно появиться, даже если пользователь ввел и сохранил
набор учетных данных, и что учетные данные не надо сохранять, пока я не
потребую подтверждения через функцию CredUIConfirmCredentials.
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
После того как флаги и структура CREDUI_INFO готова, я вызываю
PromptForCredentials и передаю ей все данные.
Dim result As CredUIReturnCodes
result = CredUI.PromptForCredentials(info, _
host, 0, _
userid, password, savePwd, flags)
Если бы я не указал CREDUI_FLAGS.ALWAYS_SHOW_UI, диалоговое окно
возникало бы только при отсутствии сохраненных учетных данных для
указанной мишени (target). Обычно это означает, что (без этого флага)
диалоговое окно появляется только при первом вызове API, что делает
работу пользователя удобнее. Вне зависимости от появления диалога
API-функция возвращает код результата, указывающий успешное или
неудачное завершение, который следует проверить до использования
возвращенных значений идентификатора и пароля.
В своем коде я проверяю значение на NO_ERROR (которое уместнее было
бы называть SUCCESS) до того, как соединяться с базой данных. Если API
возвращает NO_ERROR, но соединение с базой данных неудачно, я вызываю
ConfirmCredentials и сообщаю системе, что эти учетные данные
неправильны и сохранять их не следует. В противном случае я вызываю ту
же функцию и сообщаю, что учетные данные верны и их нужно сохранить.
Dim connString As StringDim password, userid As StringDim 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 IfElseIf result <> CredUIReturnCodes.ERROR_CANCELLED
Then MsgBox("There was an error in authentication")
End If
Использование собственных изображений
Диалоговое окно ввода учетных данных выглядит уже неплохо (рис. 2) -
очень симпатично и знакомо, - но я понимаю, что вы, возможно,
захотите в какой-то мере персонализировать его. К счастью, API-функция
PromptForCredentials принимает изображение размером 320x60 (рис. 3),
используемое вместо изображения по умолчанию.
Рис. 3. Собственное изображение, добавленное к стандартному
диалоговому окну учетных данных
Сделать это в коде весьма просто, так как класс
System.Drawing.Bitmap содержит удобный метод
GetHbitmap, получающий описатель изображения. Вам надо лишь создать
экземпляр
System.Drawing.Bitmap и записать в член .hbmBanner структуры
CREDUI_INFO описатель битовой карты. Закончив со структурой CREDUI_INFO
(и вызвав функцию PromptForCredentials), необходимо освободить
описатель во избежание утечки памяти. Для его освобождения потребуется
другая API-функция, DeleteObject, которую я включил в класс CredUI
для упрощения использования собственной битовой карты.
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
´ Вызвать PromptForCredentials
´ ...
CredUI.DeleteObject(info.hbmBanner)
Для согласованности с интерфейсом Windows советую не изменять
изображения по умолчанию, но такая возможность полезна на случай, если
вам это все же понадобится. В коде примера я загружаю изображение из
файла, но вы можете легко рисовать его "на лету" через GDI+.
Резюме
Если вам нужно запросить у пользователя учетные данные для базы
данных или Web-сайта либо какие-то другие учетные данные, лучше всего
делать это средствами операционной системы. Помимо предоставления
пользователю знакомого интерфейса, вы задействуете преимущества кэша
учетных данных в операционной системе, связанного с зарегистрированным в
системе пользователем. Если вас интересуют дополнительные сведения по
вопросам защиты при разработке, ознакомьтесь с уже упоминавшейся книгой
Writing Secure
Code.
Более подробную информацию по обсуждавшейся тематике см. в:
Информацию об авторе см. на сайте GotDotNet по ссылке
Duncan Mackenzie´s profile.