LDAP und JNDI: Für immer zusammen

Das Lightweight Directory Access Protocol (LDAP), das seine Wurzeln im X.500-Protokoll hat, wurde Anfang der neunziger Jahre als Standardverzeichnisprotokoll entwickelt. LDAP definiert, wie Clients auf Daten auf dem Server zugreifen sollen, nicht wie diese Daten auf dem Server gespeichert werden. Dadurch kann LDAP zu einem Frontend für jede Art von Datenspeicher werden.

( Hinweis: Informationen zum Herunterladen des vollständigen Quellcodes dieses Artikels finden Sie im Abschnitt Ressourcen unten.)

Die Grundstruktur von LDAP basiert auf einer einfachen Informationsbaum-Metapher, die als Verzeichnisinformationsbaum (DIT) bezeichnet wird. Jedes Blatt im Baum ist ein Eintrag; Der erste oder oberste Eintrag ist der Stammeintrag. Ein Eintrag enthält einen definierten Namen (DN) und eine beliebige Anzahl von Attribut / Wert-Paaren. Der DN, der der Name eines Eintrags ist, muss eindeutig sein. Es zeigt die Beziehung zwischen dem Eintrag und dem Rest des DIT auf ähnliche Weise wie der vollständige Pfadname einer Datei die Beziehung zu den übrigen Dateien in einem Dateisystem. Während ein Pfad zu einer Datei von links nach rechts liest, liest ein DN im Gegensatz dazu von rechts nach links. Hier ist ein Beispiel für einen DN:

uid = styagi, ou = people, o = myserver.com 

Der am weitesten links stehende Teil des DN, der als Relative Distinguished Name (RDN) bezeichnet wird, besteht aus einem Attribut / Wert-Paar. Im obigen Beispiel wäre dieses Paar uid=styagi. LDAP-Attribute verwenden häufig Mnemonics, von denen einige Beispiele in Tabelle 1 aufgeführt sind.

o Organisation
ou Organisationseinheit
cn Gemeinsamen Namen
sn Nachname
givenname Vorname
uid Benutzeridentifikation
dn Distinguished Name
mail E-Mail-Addresse
Tabelle 1. Einige gängige LDAP-Attribute

Informationen zu Attributen, Attributabgleichsregeln und Beziehungen zwischen Objektklassen werden im Serverschema definiert . Jedes Attribut kann einen oder mehrere Werte haben, je nachdem, wie das Schema definiert ist. Ein Benutzer kann beispielsweise mehr als eine E-Mail-Adresse haben. Es gibt auch ein spezielles Attribut namens Objektklasse , das die erforderlichen und zulässigen Attribute für einen bestimmten Eintrag angibt. Wie Objekte in Java können Objektklassen in LDAP erweitert werden, um vorhandene Attribute beizubehalten und neue hinzuzufügen.

Ein Namensdienst ordnet Namen Objekten zu und findet Objekte anhand ihrer Vornamen. (Die RMI-Registrierung ist ein gutes Beispiel für einen Namensdienst.) Viele Namensdienste werden um einen Verzeichnisdienst erweitert . Während ein Namensdienst das Nachschlagen eines Objekts anhand seines Namens ermöglicht, ermöglicht ein Verzeichnisdienst auch, dass solche Objekte Attribute haben. Infolgedessen können wir mit einem Verzeichnisdienst die Attribute eines Objekts nachschlagen oder anhand ihrer Attribute nach Objekten suchen.

Wo passt JNDI in diesen LDAP-Jargon? JNDI macht für LDAP das, was JDBC für Oracle macht - es bietet eine Standard-API für die Interaktion mit Namens- und Verzeichnisdiensten über eine Service Provider-Schnittstelle (SPI), die einem JDBC-Treiber entspricht. LDAP ist eine Standardmethode für den Zugriff auf Verzeichnisinformationen. JNDI bietet Java-Anwendungen und -Objekten eine leistungsstarke und transparente Schnittstelle für den Zugriff auf Verzeichnisdienste wie LDAP. In der folgenden Tabelle 2 sind gängige LDAP-Vorgänge und ihre JNDI-Entsprechungen aufgeführt. (Ausführliche Informationen zur JNDI-Spezifikation finden Sie unter Ressourcen.)

Betrieb Was es macht JNDI-Äquivalent
Suche Durchsuchen Sie das Verzeichnis nach übereinstimmenden Verzeichniseinträgen DirContext.search()
Vergleichen Sie Vergleichen Sie den Verzeichniseintrag mit einer Reihe von Attributen DirContext.search()
Hinzufügen Fügen Sie einen neuen Verzeichniseintrag hinzu DirContext.bind(), DirContext.createSubcontext()
Ändern Ändern Sie einen bestimmten Verzeichniseintrag DirContext.modifyAttributes()
Löschen Löschen Sie einen bestimmten Verzeichniseintrag Context.unbind(), Context.destroySubcontext()
Umbenennen Benennen Sie den DN um oder ändern Sie ihn Context.rename()
Binden Starten Sie eine Sitzung mit einem LDAP-Server new InitialDirContext()
Lösen Beenden Sie eine Sitzung mit einem LDAP-Server Context.close()
Verlassen Brechen Sie einen zuvor an den Server gesendeten Vorgang ab Context.close(), NamingEnumneration.close()
Extended Extended operations command LdapContext.extendedOperation()
Tabelle 2. Allgemeine LDAP-Operationen und JNDI-Äquivalente

Bearbeiten Sie Objekte auf dem LDAP-Server

Kommen wir zur Sache und sehen, wie Objekte auf dem LDAP-Server bearbeitet werden. Die Standard-LDAP-Operationen umfassen:

  • Stellen Sie eine Verbindung zum Server her
  • Binden Sie an den Server (stellen Sie sich dies als Authentifizierung vor)
  • Fügen Sie neue Einträge im LDAP-Server hinzu
  • Ändern Sie einen Eintrag
  • Eintrag löschen
  • Durchsuchen Sie den Server nach einem Eintrag

Wir werden jeden dieser Schritte in den folgenden Abschnitten anhand von Beispielen untersuchen.

Bevor Sie die Beispiele ausführen, müssen Sie den LDAP-Server, die JNDI-Klassen und (sofern Sie die Schemaüberprüfung nicht deaktivieren möchten) das Java-Schema installieren. Installationsinformationen finden Sie im Schema-Verzeichnis der JNDI-Zip-Datei. In unseren Beispielen werden Netscape Directory Server 4.1 und JDK 2 verwendet. (Informationen zum Installieren dieser Pakete finden Sie unter Ressourcen.)

Stellen Sie eine Verbindung zum Server her

Um eine Verbindung zum Server herzustellen, müssen Sie einen Verweis auf ein Objekt erhalten, das die DirContextSchnittstelle implementiert . In den meisten Anwendungen wird dazu ein InitialDirContextObjekt verwendet, das a Hashtableals Argument verwendet. Das Hashtableenthält verschiedene Einträge, z. B. die zu verwendenden Klassen Hostname, Port und JNDI-Dienstanbieter:

Hashtable env = new Hashtable (); env.put (Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); env.put (Context.PROVIDER_URL, "ldap: // localhost: 389"); DirContext ctx = neuer InitialDirContext (env);

An den Server binden

Sobald die Verbindung hergestellt ist, muss sich der Client möglicherweise selbst authentifizieren. Dieser Vorgang wird auch als Bindung an den Server bezeichnet. (Beachten Sie, dass sich die Wortbindung auch auf das Hinzufügen von etwas zum Verzeichnis beziehen kann.)

In LDAP Version 2 mussten sich alle Clients während der Verbindung authentifizieren, Version 3 ist jedoch standardmäßig anonym. Wenn die Standardwerte verwendet werden, sind die Verbindungen ebenfalls anonym. LDAP-Server verwalten Rechte mithilfe von Zugriffssteuerungslisten (ACLs), die bestimmen, welcher bestimmte Zugriff für einen Eintrag durch eine Anwendung verfügbar ist. LDAP unterstützt drei verschiedene Sicherheitstypen:

  • Einfach: Authentifiziert sich schnell mit einfachen Benutzernamen und Passwörtern.
  • SSL: Authentifiziert sich mit SSL-Verschlüsselung über das Netzwerk.
  • SASL: Verwendet MD5 / Kerberos-Mechanismen. SASL ist ein einfaches, auf Authentifizierung und Sicherheitsschicht basierendes Schema

Der Client authentifiziert sich beim Server, indem er Werte für verschiedene Umgebungsvariablen in der ContextSchnittstelle angibt (siehe unten):

Hashtable env = new Hashtable(); env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.ldap.LdapCtxFactory"); env.put(Context.PROVIDER_URL, "ldap://localhost:389"); env.put(Context.SECURITY_AUTHENTICATION,"simple"); env.put(Context.SECURITY_PRINCIPAL,"cn=Directory Manager"); // specify the username env.put(Context.SECURITY_CREDENTIALS,"password"); // specify the password DirContext ctx = new InitialDirContext(env); 

Add new entries in the LDAP server: The options

The LDAP directory server can act as a repository for Java objects. JNDI provides an object-oriented view of this directory, which means that Java objects can be added to and retrieved from the directory without the client needing to manage data representation issues.

Objects can be stored in three ways:

  • Store the Java objects themselves
  • Store a reference to the object
  • Store information as attributes

Let's take a look at each of these in more detail.

Store the Java objects themselves

If a class implements the java.io.Serializable interface, it can be serialized and deserialized from storage media. If we need a simple name-object binding (as in the RMI registry), then the Context.bind() method can store the object. But if we need the more powerful technique of associating attributes with the stored object, we'd employ the DirConext.bind() method instead. Whichever method we use, the object's state is serialized and stored in the server:

MyObject obj = new MyObject(); ctx.bind("cn=anobject", obj); 

Once stored, we can retrieve the object by looking up its name in the directory:

MyObject obj = (MyObject)ctx.lookup("cn=anobject"); 

When an application serializes an object by writing it to an object stream, it records information that identifies the object's class in the serialized stream. However, the class's definition, which is contained in the classfile, is not itself recorded. The system that deserializes the object is responsible for determining how to locate and load the necessary class files.

Alternatively, the application can record the codebase with the serialized object in the directory, either when the binding occurs or by subsequently adding an attribute using DirContext.modifyAttributes(). (We'll examine this second technique later in this article.) Any attribute can record the codebase as long as the application reading back the object is aware of the attribute name. As another option, we can employ the attribute "javaCodebase" specified in the LDAP schema for storing Java objects if schema checking is enabled on the server.

The above example can be modified to supply a codebase attribute containing the location of the MyObject class definition:

// Create object to be bound MyObject obj = new MyObject(); // Perform bind and specify codebase BasicAttribytes battr = new BasicAttributes("javaCodebase","//myserver.com/classes") ctx.bind("cn=anobject", obj, battr); 

AddSeriaize.java demonstrates how to add 10 instances of java.util.Vector, which implements java.io.Serializable; the result can be seen in Figure 1.

Store a reference to the object

Instead of storing the entire serialized state of an object, you can store a reference to that object instead. JNDI's javax.naming.Reference class records address information about objects not directly bound to the directory service. The reference to an object contains the following information:

  • The class name of the referenced object
  • A vector of javax.naming.RefAddr objects that represents the addresses
  • The name and location of the object factory to use during reconstruction

The javax.naming.RefAddr abstract class, seen in Figure 2 below, contains information indicating the ways in which you can contact the object (e.g., via a location in memory, a lookup on another machine, etc.) or recreate it with the same state. The class defines an association between content and type. The content (an object) stores information required to rebuild the object and the type (a string) identifies the purpose of the content.

RefAddr also overrides the java.lang.Object.equals(Object obj) and java.lang.Object.hashcode() methods to ensure that two references are equal if the content and type are equal. RefAddr has two concrete subclasses: javax.naming.StringRefAddr, which stores strings, and javax.naming.BinaryRefAddr, which stores an array of bytes. For example, a string reference address could be an IP, URL, hostname, or something similar.

Consider the example of a referenceable Apartment class. The ADDReference.java example creates a few instances of Apartment and stores them in the server. What happens internally? Since the object is referenceable, a reference is stored and not the serialized object. When the example tries to look up an apartment belonging to styagi, it gets a reference from the server that contains information about the factory class needed, the apartment size, and its location. It then requests that the factory create an Apartment object with the right size and location and return that object. All this happens transparently to the user.

Context ctx = new InitialContext(env);

ctx.bind("apartment=styagi,ou=JavaObjects,o=myserver.com",new Apartment("studio","Mill Complex"));

ctx.bind("apartment=mojoe,ou=JavaObjects,o=myserver.com", new Apartment("2 room","Farm House Apartments"));

ctx.bind("apartment=janedoe,ou=JavaObjects,o=myserver.com",new Apartment("1 room","Pond Side"));

ctx.bind("apartment=rogerp,ou=JavaObjects,o=myserver.com", new Apartment("3 room","Mill Complex"));

ctx.bind("apartment=jamesm,ou=JavaObjects,o=myserver.com", new Apartment("studio","Fox Hill Apartments"));

ctx.bind("apartment=paulh,ou=JavaObjects,o=myserver.com", new Apartment("duplex","Woodbridge"));

ctx.bind("apartment=vkevink,ou=JavaObjects,o=myserver.com",new Apartment("1 room","Woodgate Apartments"));

Apartment apt = (Apartment)ctx.lookup("apartment=styagi,ou= JavaObjects,o=myserver.com");

System.out.println(apt);

The Apartment class would look something like:

public class Apartment implements Referenceable{ private String size; public String location; public Apartment(String size,String location){ this.size=size; this.location=location; } public Reference getReference() throws NamingException{ String classname = Apartment.class.getName(); StringRefAddr classref = new StringRefAddr("Apartment details", size+ ":" +location); String classfactoryname=ApartmentFactory.class.getName(); Reference ref = new Reference(classname,classref,classfactoryname,null); return ref; } public String toString(){ return ("This apartment is "+size+ " and is located at " +location); } } 

Die Factory, mit der die ApartmentObjekte neu erstellt wurden ApartmentFactory, wird Referencewie oben gezeigt in der gespeichert . Das ApartmentFactoryimplementiert die javax.naming.spi.ObjectFactorySchnittstelle, die die getObjectInstance()Methode enthält, die ein neu erstelltes Objekt basierend auf der Referenz zurückgibt, wie unten dargestellt: