Исходники.Ру - Программирование
Исходники
Статьи
Книги и учебники
Скрипты
Новости RSS
Магазин программиста

Главная » Статьи по программированию » .NET - Все статьи »

Обсудить на форуме Обсудить на форуме

Управление удостоверениями в Windows XP и Windows Server 2003
Демонстрируется, как получать удостоверений (credentials) пользователя в Microsoft Windows XP и Windows Server 2003 с помощью DPAPI-функции CredUIPromptForCredentials, позволяющей запрашивать аутентификационную информацию стандартным и защищенным способом.

Аннотация

Демонстрируется, как получать удостоверений (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.


Может пригодится:


Автор: Дункан Маккензи, Microsoft Developer Network
Прочитано: 5191
Рейтинг:
Оценить: 1 2 3 4 5

Комментарии: (2)

Прислал: red_mould
Перевел с MSDN хоршо :)

Прислал: red_mould
да чтоб небыло непониманий и т.д. вот в самом MSDN ms-help://MS.MSDNQTR.v80.en/MS.MSDN.v80/MS.NETDEV.v10.en/dnnetsec/html/dpapiusercredentials.htm

Добавить комментарий
Ваше имя*:
Ваш email:
URL Вашего сайта:
Ваш комментарий*:
Код безопастности*:

Рассылка новостей
Рейтинги
© 2007, Программирование Исходники.Ру