Verwenden der Anmeldeinformationsverwaltung in Windows XP und Windows Server 2003

Veröffentlicht: 25. Apr 2003 | Aktualisiert: 22. Jun 2004
Von Duncan Mackenzie

Bitte beachten Sie, dass die Kommentare der Programmierer in den Beispieldateien englischsprachig sind. Zum leichteren Verständnis wurden sie in diesem Artikel übersetzt.

Auf dieser Seite

Einführung Einführung
Gespeicherte Benutzernamen und Kennwörter Gespeicherte Benutzernamen und Kennwörter
Erstellen der Anmeldeinformations-API-Klasse in .NET Erstellen der Anmeldeinformations-API-Klasse in .NET
Deklarieren der API-Funktionen Deklarieren der API-Funktionen
Konstanten- und Strukturdeklarationen Konstanten- und Strukturdeklarationen
Erstellen von Wrapperfunktionen um API-Aufrufe Erstellen von Wrapperfunktionen um API-Aufrufe
Anfordern von Benutzeranmeldeinformationen  Anfordern von Benutzeranmeldeinformationen
Verwenden einer eigenen Grafik Verwenden einer eigenen Grafik
Zusammenfassung Zusammenfassung

Einführung

In einigen Fällen muss der Benutzer in Ihrer Anwendung Anmeldeinformationen für den Zugriff auf eine geschützte Ressource, z.B. eine Datenbank oder FTP-Site, eingeben. Das Abrufen und Speichern der ID und des Kennworts eines Benutzers stellt jedoch ein Sicherheitsrisiko für das System dar. Im Idealfall sollte der Benutzer gar keine Anmeldeinformationen eingeben müssen (z.B. durch Verwendung einer integrierten Authentifizierung für die Datenbank). Dies ist jedoch nicht immer möglich. Wenn Sie Anmeldeinformationen vom Benutzer anfordern müssen und die Anwendung unter Microsoft® Windows® XP oder Microsoft® Windows Server 2003 ausführen, wird Ihnen diese Aufgabe durch bestimmte Funktionen des Betriebssystems erleichtert.

Gespeicherte Benutzernamen und Kennwörter

Windows XP und Windows Server 2003 verwenden ein als" gespeichert.< Gespeicherte>


DpapiUserCredentials_fig1.gif

Abbildung 1. Dialogfelder zur Verwaltung von Anmeldeinformationen in Windows XP

Unter Windows XP oder Windows Server 2003 kann Ihre Anwendung die API-Funktionen zur Anmeldeinformationsverwaltung verwenden, um den Benutzer zur Eingabe der Daten aufzufordern. Diese APIs bieten Ihnen eine konsistente Benutzeroberfläche (siehe Abbildung 2) und ermöglichen das automatische Zwischenspeichern dieser Anmeldeinformationen im Betriebssystem.


dpapiusercredentials_fig2.gif

Abbildung 2. Windows XP-Standarddialogfeld für Anmeldeinformationen

Ausführlichere Informationen zum Abrufen, Speichern und Verwenden der Anmeldeinformationen eines Benutzers in Ihrer Anwendung finden Sie im Buch Writing Secure Code (in Englisch) von Michael Howard und David LeBlanc, das ich Ihnen als weiterführende Lektüre empfehle. In diesem Artikel wird lediglich die Verwendung der Anmeldeinformationsverwaltungs-APIs in Microsoft® Visual Basic® .NET- und C#-Anwendungen beschrieben.

Erstellen der Anmeldeinformations-API-Klasse in .NET

Deklarieren der API-Funktionen

Da es sich bei diesen Funktionen zur Anmeldeinformationsverwaltung um Win32-API-Aufrufe handelt, müssen Sie für den Zugriff auf die Funktionen externe Definitionen erstellen (C#) oder Definitionen deklarieren (Visual Basic .NET). Neben den Funktionen selbst sind Konstanten und Strukturen für den Zugriff erforderlich. Da die Konstanten in vordefinierten Sätzen verwaltet werden, habe ich diese Sätze in meinem .NET-Code als Enumerationen implementiert, um die Verwendung der API-Aufrufe zu vereinfachen.

 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 

Anmerkung: Ich habe den API-Aufruf DeleteObject aus der GDI32-Bibliothek hinzugefügt, da Sie diesen zum Einfügen einer eigenen Bitmap in die CredUIPromptForCredentials-API benötigen. Die Verwendung dieser API wird in diesem Artikel unter "Verwenden einer eigenen Grafik" erläutert.

Konstanten- und Strukturdeklarationen

Für viele Win32-API-Aufrufe benötigen Sie einen Satz von Konstanten (die ich in diesem Fall als Enumerationen darstelle) und ggf. eine oder zwei Strukturdeklarationen. Die Anmeldeinformations-APIs bilden keine Ausnahme von diesen allgemeinen Regeln und erfordern verschiedene Konstanten sowie eine Struktur. In meiner .NET-Klasse habe ich eine Enumeration für den Flagparameter von CredUIPromptForCredentials, eine weitere Enumeration für den möglichen Satz von Rückgabecodes aller drei Anmeldeinformations-API-Aufrufe und eine Deklaration der CREDUI_INFO-Struktur hinzugefügt.

<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 
 <MarshalAs(UnmanagedType.LPWStr)> Public pszMessageText As String 
 <MarshalAs(UnmanagedType.LPWStr)> Public pszCaptionText As String 
 Public hbmBanner As IntPtr 
End Structure

Erstellen von Wrapperfunktionen um API-Aufrufe

Dieser Schritt ist optional. Sie können die APIs einfach als Public deklarieren (anstelle der Deklaration Private in meinem Code) und direkt in den Anwendungen aufrufen. Das Aufrufen von APIs ist oftmals recht aufwändig, und ich möchte dem Benutzer meines Codes diese Details ersparen, indem ich die API-Aufrufe wrappe.

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

Anmerkung:
Aus Gründen der Einfachheit habe ich alle Funktionen auf Shared/Static gesetzt. Da die Funktionen keine Informationen als Eigenschaften oder Variablen auf Klassenebene speichern, ist es nicht notwendig, dass der Entwickler vor dem Aufruf eine Instanz der Klasse erstellt.

Nach dem Deklarieren und Wrappen der APIs sollten Sie diesen Code ggf. in einer eigenen Assembly platzieren, indem Sie ein Klassenbibliotheksprojekt in Microsoft® Visual Studio® .NET erstellen (wie in den Beispielen zu diesem Artikel) und diesen Code als einzige Klasse kompilieren. Wenn Sie eine Assembly aus diesem Code erstellen, können Sie anschließend in allen Projekten auf den Code verweisen, in denen er benötigt wird - Sie können diese Klasse bei Bedarf jedoch auch in Ihr Projekt einschließen.

Anfordern von Benutzeranmeldeinformationen

Nachdem Sie die Deklarationen der API-Aufrufe, die zugehörigen Enumeratoren und die Struktur erstellt haben, können Sie die Anmeldeinformations-API in Ihrer eigenen Anwendung verwenden. Zur Demonstration der verschiedenen Methoden zum Aufrufen dieser API habe ich eine einfache Beispielanwendung erstellt, die über die SQL-Authentifizierung eine Verbindung zu einer lokalen Microsoft® SQL Server-Datenbank herstellt. Normalerweise würde ich immer versuchen, eine integrierte Authentifizierung für den SQL-Server einzusetzen.

In diesem Beispiel verwende ich jedoch einen Datenbankserver, der diesen Authentifizierungstyp nicht unterstützt. Beim Aufrufen von CredUIPromptForCredentials können Sie verschiedene Flags setzen. Obwohl diese Flags auf der Referenzseite für diese API dokumentiert sind, möchte ich die in meiner Anwendung verwendeten Flags an dieser Stelle ausführlicher beschreiben.

ALWAYS_SHOW_UI weist die API an, das Anmeldeinformationsdialogfeld auch dann anzuzeigen, wenn Sie zuvor bereits Anmeldeinformationen eingegeben und gespeichert haben. Ohne dieses Flag wird bei allen nachfolgend über die Anwendung hergestellten Verbindungen keine Eingabeaufforderung angezeigt, wenn Anmeldeinformationen gespeichert wurden. Dies ist besonders hilfreich, wenn davon auszugehen ist, dass der Benutzer unterschiedliche Informationen für die Anmeldung verwendet.

EXPECT_CONFIRMATION wird zusammen mit dem API-Aufruf CredUIConfirmCredentials (ebenfalls im vorhergehenden Code in diesem Artikel enthalten) verwendet. Dieses Flag ermöglicht einen Zweiphasenprozess, durch den das Speichern fehlerhafter Anmeldeinformationen verhindert werden kann. Zunächst rufen Sie die Anmeldeinformationen vom Benutzer ab, und anschließend versuchen Sie, unter Verwendung dieser Daten eine Verbindung herzustellen. Dabei werden die Informationen nur bestätigt (und gespeichert), wenn die Verbindung erfolgreich hergestellt wird.

GENERIC_CREDENTIALS gibt an, dass Sie im Gegensatz zu Domänenanmeldeinformationen nur eine Kombination aus Benutzer-ID und Kennwort wünschen. Für diesen Zweck verwende ich nur diese API, da gesicherte Ressourcen, die Domänenanmeldeinformationen erfordern, normalerweise vom Betriebssystem gehandhabt werden. Daher übergebe ich immer dieses Flag.

KEEP_USERNAME ändert die Benutzeroberfläche des Anmeldeinformationsdialogfelds, so dass nur ein Kennwort eingegeben werden kann. In einigen Fällen, z.B. bei Verbindungen zu Microsoft® Access-Datenbanken mit festgelegtem Datenbankkennwort, ist die Benutzer-ID fest definiert (oder im Falle des Access-Datenbankkennworts nicht vorhanden). Wenn dies der Fall ist, kann die Benutzeroberfläche für die Anmeldeinformationen mit Hilfe dieses Flags entsprechend angepasst werden.

SHOW_SAVE_CHECK_BOX stellt sicher, dass das Anmeldeinformationsdialogfeld ein Kontrollkästchen enthält, mit dem der Benutzer die Anmeldeinformationen speichern kann. Die Einstellung des Benutzers wird im Speicherparameter (boolescher Wert) von CredUI.PromptForCredentials zurückgegeben.

Im folgenden Beispiel werden die Anmeldeinformations-APIs und die oben beschriebenen Flags verwendet, um vor dem Verbinden mit einer SQL-Datenbank ein Kennwort anzufordern. Als Erstes erstelle und fülle ich die Struktur CREDUI_INFO mit meinem Ziel, dem übergeordneten Fenster und einer Titelzeichenfolge.

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

Anschließend lege ich die Flags für diesen Aufruf an CredUIPromptForCredentials fest. In diesem Beispiel wird Folgendes für die API definiert: Es sollen allgemeine Anmeldeinformationen (im Gegensatz zu Domänenanmeldeinformationen) angefordert werden, die Benutzeroberfläche soll ein Speicherkontrollkästchen enthalten, das Dialogfeld soll auch dann angezeigt werden, wenn der Benutzer zuvor Anmeldeinformationen eingegeben und gespeichert hat, und vor dem Speichern von Anmeldeinformationen soll über die CredUIConfirmCredentials-API eine Bestätigung bereitgestellt werden.

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

Sobald ich meine Flags und CREDUI_INFO Struktur habe, setze ich den PromptForCredentials-API-Aufruf ab und übergebe alle Informationen.

Dim result As CredUIReturnCodes 
result = CredUI.PromptForCredentials(info, _ 
 host, 0, _ 
 userid, password, savePwd, flags)

Gebe ich CREDUI_FLAGS.ALWAYS_SHOW_UI nicht an, wird das Dialogfeld nur angezeigt, wenn für das jeweilige Ziel keine gespeicherten Anmeldeinformationen verfügbar sind. Im Allgemeinen bedeutet dies, dass das Dialogfeld (ohne dieses Flag) nur beim ersten Aufruf dieser API angezeigt wird, wodurch die Benutzerfreundlichkeit verbessert wird. Unabhängig davon, ob das Dialogfeld angezeigt wird, gibt dieser API-Aufruf einen Ergebniscode zurück (Erfolg oder Fehler), den Sie vor der weiteren Verwendung der zurückgegebenen Benutzer-ID- und Kennwortwerte überprüfen sollten.

In meinem Code prüfe ich vor dem Herstellen der Datenbankverbindung auf den Wert NO_ERROR (der auch als SUCCESS bezeichnet werden könnte). Wenn der API-Aufruf NO_ERROR zurückgibt, die Datenbankverbindung jedoch fehlschlägt, teile ich dem Anmeldeinformationssystem mit dem Aufruf ConfirmCredentials mit, dass diese Anmeldeinformationen ungültig sind und daher nicht gespeichert werden sollen. Wird die Datenbankverbindung hergestellt, informiere ich das Anmeldeinformationssystem mit dem Aufruf ConfirmCredentials, dass diese Kombination von Benutzer-ID und Kennwort gültig ist und gespeichert werden soll.

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

Verwenden einer eigenen Grafik

Das Anmeldeinformationsdialogfeld sieht bereits recht gut aus (siehe Abbildung 2) - sehr übersichtlich und konsistent - ich kann jedoch gut verstehen, wenn Sie es für Ihre Zwecke anpassen möchten. Glücklicherweise ermöglicht Ihnen die PromptForCredentials-API die Verwendung einer eigenen 320x60-Grafik (siehe Abbildung 3) anstelle des Standardbildes.


dpapiusercredentials_fig3.gif

Abbildung 3. Dem Anmeldeinformationsdialogfeld wurde eine benutzerdefinierte Grafik hinzugefügt

Das Hinzufügen einer Grafik zu Ihrem eigenen Code ist relativ unkompliziert, da die Klasse System.Drawing.Bitmap eine geeignete Methode, GetHbitmap, zum Abrufen eines systemeigenen Handles für die zugrunde liegenden Grafik bereitstellt. Sie müssen eine Instanz von System.Drawing.Bitmap erstellen, und anschließend setzen Sie das Member .hbmBanner der CREDUI_INFO-Struktur auf den systemeigenen Handle dieser Bitmap.

Nachdem Sie die CREDUI_INFO-Struktur fertig gestellt haben (nach dem PromptForCredentials-Aufruf), müssen Sie diesen systemeigenen Handle freigeben, um ausreichend Speicher bereitzustellen. Für die Freigabe des Handles ist ein weiterer API-Aufruf erforderlich: DeleteObject. Ich habe diesen Aufruf in die Klasse CredUI eingeschlossen, um die Verwendung einer benutzerdefinierten Bitmap so einfach wie möglich zu machen.

Dim credBmp As New Bitmap("javascript:void(null);") 
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 
'Aufruf an PromptForCredentials durchführen 
'... 
CredUI.DeleteObject(info.hbmBanner)

Obwohl ich zur Gewährleistung der Konsistenz mit Windows die Verwendung der Standardgrafik empfehle, ist es gut zu wissen, dass das Dialogfeld bei Bedarf angepasst werden kann. Ich lade das Bild aus einer Datei im Beispielcode, Sie können es jedoch auch mit GDI+ zeichnen.

Zusammenfassung

Wenn Sie Datenbank-, Website- oder sonstige Anmeldeinformationen vom Benutzer anfordern müssen, empfiehlt sich die Verwendung der integrierten Betriebssystemfeatures. Auf diese Weise stellen Sie dem Benutzer nicht nur eine konsistente Benutzeroberfläche zur Verfügung, sondern Sie können auch die Vorteile des Anmeldeinformationscache des Betriebssystems nutzen, der an den angemeldeten Benutzer gebunden ist. Weitere Informationen zu Sicherheitsaspekten bei der Entwicklung finden Sie in Writing Secure Code (in Englisch) von Michael Howard und David LeBlanc.


Anzeigen: