Lightweight Directory Access Protocol (LDAP) is increasingly popular because it simplifies what has been complex, namely, accessing directory services. In this article you will learn what you can do with LDAP and how Java handles LDAP with its Java Naming and Directory Interface (JNDI) API. At the end of the article, you will find a project that provides a white pages service built with LDAP and JNDI.
Before you start the project, you should first familiarize yourself the jargon of naming and directory services.
A naming service lets you find an object in a system based on the name associated with the object. Naming services are easy to find. Take the filesystem in your computer, for instance. Every file in a computer has a name; to access a file you must know its name. In filesystems, files are objects associated with filenames. Another example is the Internet's DNS, which maps easy-to-remember names (such as onjava.com and jUpload.com) to IP addresses. When you work with Enterprise JavaBeans (EJB), you use a naming service to get a reference to a Bean. In short, a naming service allows you to look up an object by its name.
Each naming service has its own rules for making valid names. For example, the rules for valid filenames Linux are different from the rules in Windows.
The association of a name with an object is called a binding. In a filesystem, a filename is bound to a file. In a DNS server, domain names are bound to IP addresses.
Objects in some naming services cannot be stored directly inside the naming service. Instead, the name service stores pointers or references to objects. A reference contains an address, that is, specific information on how to access the object itself.
In a naming service, obviously you have more than one
name-to-object binding. The set of bindings is called a context. There
are two types of contexts: root and subcontext. A root context is the
base name of an object. In a filesystem, the root context is the base
from which all other directories and files are stored. In the Unix
file system, the root context is /. Under Windows it is
normally C:\.
A subcontext is a name that adds another level to the root
context. For example, a directory, such as usr under
/ in a Unix filesystem, is a subcontext. In the Unix
system, this subcontext is called a subdirectory. That is, in a
directory, /usr, the directory usr is a
subcontext of /. In another example, a DNS domain, like
COM or NET, is a context. A DNS domain named relative to another DNS
domain is a subcontext. For example, in the DNS domain
brainysoftware.com, the DNS domain brainysoftware is a subcontext of
COM.
A directory service is an extension of a naming service. In a directory service, an object is also associated with a name. However, each object is allowed to have attributes. You can look up an object by its name; but you can also obtain the object's attributes or search for the object based on its attributes.
Going back to the Unix file system, it's not just a naming service but also a directory service. Each file can have attributes like owner and date. In real world applications, a directory object in a directory server can be used to represent anything: a printer, a computer, a network, or even a person in an organization.
|
An attribute of a directory object is a property of the object. For example, a person can have the following attributes: last name, first name, user name, email address, telephone number, and so on. A printer can have attributes like resolution, color, and speed.
An attribute has an identifier which is a unique name in that
object. Each attribute can have one or more values. For instance, a
person object can have an attribute called LastName.
LastName is the identifier of an attribute. An attribute
value is the content of the attribute. For example, the
LastName attribute can have a value like "Martin".
You can look up a directory object by supplying its name to a directory service. Alternatively, many directory services support searches for objects based on properties, not just names. You can supply a query consisting of a logical expression in which you specify the attributes that the object or objects must have. The query is called a search filter. This style of searching is sometimes called reverse lookup or content-based searching. The directory service searches for and returns the objects that satisfy the search filter.
Directory services are very common these days. There already exist a plethora of directory service implementations:
Accessing a directory service and manipulating its objects used to be complex and difficult. The traditional protocol is X.500, a set of directory recommendations specified by the International Telecommunication Union. X.500 was enormous and complex.
LDAP is a direct descendant of X.500. LDAP was designed at the University of Michigan to simplify access to X.500 directories (hence the "L" for "lightweight" in "LDAP"). LDAP was designed to be powerful enough to solve basic problems in accessing a directory service but simple and light enough so more people can afford to use it.
LDAP, currently at version 3, is now a standard for directory information access. Many companies, including Microsoft, IBM, Novell, Computer Associates, and Sun, have agreed to support it. LDAP is now being used as an important part of a variety of services: authentication systems, mail systems, and e-commerce applications. To date more than 60 LDAP server implementations have been released; approximately 90% of which are standalone LDAP directory servers, 10% of which are components of other applications.
You probably already have an LDAP-aware client installed on your computer. Many email clients can access an LDAP directory for email addresses, including Outlook, Eudora, Netscape Communicator, QuickMail Pro, and Mulberry.
The LDAP naming convention orders components from right to left, delimited by a comma. LDAP arranges all directory objects in a tree, called a directory information tree (DIT). Within the DIT, an organization object, for example, might contain group objects that might in turn contain person objects. When directory objects are arranged in this way, they play the role of naming contexts in addition to being attribute containers.
In addition to client connections and disconnections, an LDAP server must provide the following:
Note that the term binding in LDAP is different from its generic directory services meaning. Binding here refers to the authentication that a user is required to perform before accessing an entry in the directory.
|
There are several public LDAP servers you can use over the Internet; the popular of these is probably BigFoot's (ldap://ldap.bigfoot.com); others include ldap://ldap.four11.com and ldap://ldap.InfoSpace.com.
A number of universities in the US also provides LDAP service to search for students or staff members. For a list of university public LDAP services, see eMailman's Public LDAP Servers.
Your organization may already run a directory service, especially if its very large. If not, you probably need to do some research before deciding on one.
The most popular LDAP server today is iPlanet's Directory Server. Others include Novell's NDS eDirectory, Critical Path's Global Directory Server, Computer Associates' eTrust Directory, Siemens' DirX, and Oracle's Oracle Internet Directory. Deciding the one which is best for your situation is often tricky.
NetworkWorld Fusion published a good article last year which compares the performance of many LDAP servers. If it's to be believed, iPlanet is the best performer and also the fastest; it concludes that iPlanet's Directory Server is the best choice for commercial use.
If you only need an LDAP server for testing, you probably want to use something else. Downloads for the latest version of iPlanet's Directory Server (version 5.0 beta) range from 53 MB to 78 MB, depending on your operating system. For the project in this article, I chose the much slimmer LDAP server from OpenLDAP. Even though not the fastest, theis free product is only a 1.52 MB download. OpenLDAP's products are only available for Linux; but once you have seeded it with entries, you can use this article's project code to access any LDAP server on any operating system.
You can download OpenLDAP from the project's site. The LDAP server is called slapd (a stand-alone LDAP server). The latest version of slapd is 2.0.7. Other programs downloadable from the Web site are the replication server, some libraries, and a variety of tools.
To install slapd, you first need to download
openldap-2_0_7.tgz into the /usr/local/
directory of a Linux system. You can use another directory but you'll
need to do some adjustment to the following instructions.
Installation takes the following steps:
Extract the files --
gunzip -c openldap-2_0_7.tgz | tar xvfB -
-- into a subdirectory, openldap-2.0.7. If you are
using a different version, this subdirectory is called
openldap-VERSION.
Assuming that your current working directory is
/usr/local, run
cd openldap-2.0.7Next, run
./configureThen the following commands:
make depend
make
make testIf everything goes smoothly, you are now ready to install, for which you'll need root access. Run
su root -c 'make install'
|
If installation is as expected, you are now ready to configure
slapd. The configuration file is called slapd.conf and
can be found at the /usr/local/etc/openldap/
directory. Open this file with your favorite text editor.
You should see the following lines.
database ldbm
suffix "dc=<MY-DOMAIN>,dc=<COM>"
rootdn "cn=Manager,dc=<MY-DOMAIN>,dc=<COM>"
rootpw secret
directory /usr/local/var/openldap-ldbm
You need to edit the <MY-DOMAIN> and the <COM> parts to reflect your domain name. Using the correct names ensures that your LDAP server can be accessed from the Internet.
For example, for the brainysoftware.com domain, the configuration lines will look like
database ldbm
suffix "dc=brainysoftware,dc=com"
rootdn "cn=Manager,dc=brainysoftware,dc=com"
rootpw secret
directory /usr/local/var/openldap-ldbm
If your domain contains additional components -- like
sandal.jepit.edu.au -- do something like
database ldbm
suffix "dc=sandal,dc=jepit,dc=edu,dc=au"
rootdn "cn=Manager,dc=sandal,dc=jepit,dc=edu,dc=au"
rootpw secret
directory /usr/local/var/openldap-ldbm
The fourth line (rootpw secret) contains the root password
that you need to supply to the server to make changes to the entries and do
some other functions.
Running slapd requires root access, so run
su root -c /usr/local/libexec/slapd
or
/usr/local/libexec/slapd
if you're already logged in as root.
To check that the server is running and configured correctly, you
can search it with ldapsearch. By default,
ldapsearch is installed as
/usr/local/bin/ldapsearch. Use the following command:
ldapsearch -x -b '' -s base '(objectclass=*)' namingContexts
An important file in an LDAP server is the schema file, which, for
slapd, is called core.schema, located at
/usr/local/openldap-2.0.7/etc/openldap/schema/. It
contains the directory schema of the LDAP server.
A directory schema specifies, among other things, the types of objects that a directory may have and the attributes that are mandatory and optional to that object. A directory schema also contains attribute type definitions, object class definitions, and other information which a server uses to determine how to match a filter or attribute value assertion against the attributes of an entry, and whether to permit add and modify operations.
The LDAP v3 schema is based on the X.500 standard for common objects found in a network like countries, localities, organizations, users/persons, groups and devices.
The LDAP v3 schema is specified in RFC 2252 and RFC 2256.
All LDAP entries in the directory are typed. Each entry belongs to
object classes that identify the type of data represented by the
entry. The object class specifies the mandatory and optional
attributes that can be associated with an entry of that class. The
object classes for all objects in the directory form a class
hierarchy. The classes top and alias are at
the root of the hierarchy. For example, the
organizationalPerson object class is a subclass of the
Person object class, which in turn is a subclass of
top. When creating a new LDAP entry, you must always
specify all of the object classes to which the new entry
belongs. Because many directories do not support object class
subclassing, you also should always include all of the superclasses of
the entry.
Three types of object classes are possible:
For example, for an organizationalPerson object, you
should list in its object classes the
organizationalPerson, person, and
top classes. The organizationalPerson,
person, and top objects are listed as the
following entries in the core.schema file.
objectclass ( 2.5.6.0 NAME 'top' ABSTRACT
MUST objectClass )
objectclass ( 2.5.6.6 NAME 'person' SUP top STRUCTURAL
MUST ( sn $ cn )
MAY ( userPassword $ telephoneNumber $ seeAlso $ description )
)
objectclass ( 2.5.6.7 NAME 'organizationalPerson' SUP person
STRUCTURAL
MAY ( title $ x121Address $ registeredAddress $
destinationIndicator $ preferredDeliveryMethod $
telexNumber $ teletexTerminalIdentifier $
telephoneNumber $ internationaliSDNNumber $
facsimileTelephoneNumber $ street $ postOfficeBox $
postalCode $ postalAddress $ physicalDeliveryOfficeName $
ou $ st $ l
)
)
LDAP v3 specifies that each directory entry may contain an operational attribute that identifies its subschema subentry. A subschema subentry contains the schema definitions for the object classes and attribute type definitions used by entries in a particular part of the directory tree. If a particular entry does not have a subschema subentry, then the subschema subentry of the root DSE, which is named by the empty DN, is used. For more information about the schema, refer to RFCs 2252 and 2256.
|
Adding entries to the server is the first thing you should do. To
add entries to slapd, you use ldapadd, which reads the
content of an ldif file, checks the validity of its
entries, and adds the entries to the server if the entries are
correct.
To add entries to the LDAP server, you need to pass the domain name
and the password for the root user. For example, with the following
command you pass the domain name (sendal.jepit.edu.au)
and the password (secret) and the
example.ldif containing the entries to be added.
ldapadd -x -D "cn=Manager ,dc=sendal,dc=jepit,dc=edu,dc=au" -w
secret -f example.ldif
The argument list of ldapadd can be displayed by
typing ldapadd with no arguments.
As mentioned above, the LDIF is used to represent LDAP entries in text form. The basic syntax of an LDIF entry is
.
[<id>]
dn: <distinguished name>
<attrtype>: <attrvalue>
<attrtype>: <attrvalue>
...
where <id> is the optional entry ID (a positive
decimal number). Normally, you would not supply the <id>,
allowing the database creation tools to do that for you. A line may be
continued by starting the next line with a single space or tab character, as
in
dn: cn=Frank Dominic, o=University of Michigan, c=US
Multiple attribute values are specified on separate lines.
cn: Frank Dominic
cn: Frank B Dominic
If an <attrvalue> contains a non-printing
character, or begins with a space or a colon (:), the
<attrtype> is followed by a double colon and the
value is encoded in base 64 notation. e.g., the value " begins
with a space" would be encoded like this:
cn:: IGJlZ2lucyB3aXRoIGEgc3BhY2U=
Blank lines separate multiple entries within the same LDIF file.
Here is an example of an LDIF file containing three entries.
dn: cn=Barbara J Jensen, o=University of Michigan, c=US
cn: Barbara J Jensen
cn: Babs Jensen
objectclass: person
sn: Jensen
dn: cn=Bjorn J Jensen, o=University of Michigan, c=US
cn: Bjorn J Jensen
cn: Bjorn Jensen
objectclass: person
sn: Jensen
dn: cn=Jennifer J Jensen, o=University of Michigan, c=US
cn: Jennifer J Jensen
cn: Jennifer Jensen
objectclass: person
sn: Jensen
jpegPhoto:: /9j/4AAQSkZJRgABAAAAAQABAAD/2wBDABALD
A4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQ
ERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVG ...
Notice that the jpegPhoto in Jennifer Jensen's entry is encoded in base 64.
The JNDI is API for writing programs to access naming and directory services.
The JNDI is grouped into five packages.
javax.namingjavax.naming.directoryjavax.naming.eventjavax.naming.ldapjavax.naming.spiFor the project in this article you only need the
javax.naming and javax.naming.directory
packages.
JNDI is included in version 1.3 of Java 2 SDK. If you are using this version, you are in luck. For users of JDK 1.1 and Java 2 SDK version 1.2, the JNDI can be downloaded and installed separately. In the Java 2 SDK, version 1.3, you can find service providers for the following services:
If you are using an older version of Java, you must first download the JNDI as a Standard Extension on the JDK 1.1 and Java 2 SDK, version 1.2.
You must also download one or more service providers. These service providers act like JDBC drivers for database access.
|
When accessing a naming service, you first need a service
provider. The first thing to do is to get the initial context, which
is the starting position into the namespace. You acquire the initial
context before you do any other operation. This is because all
operations on naming and directory services are performed relative to
some context. If you specify that your initial context when accessing
a filesystem is the /usr/local directory when you call
the list() method, then it's the contents of the
/usr/local directory that will be returned. You can think
of the initial context as the application default directory.
To obtain the initial context, you call the
InitialContext() constructor, passing all the necessary
environment information in a Hashtable object:
Hashtable env = new Hashtable();
Into the Hashtable, you then put the service provider. For example, if you are using the filesystem service provider from Sun, this is the line of code you need.
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.fscontext.RefFSContextFactory");
The filesystem service provider can be downloaded here.
If you are using a different service provider, replace
put()'s second argument.
Another important environment property that you need to get the
initial context is the PROVIDER_URL. This property is
assigned the location of the initial context. This could be a URL on
the Internet or it could just be a directory in a file system. For
instance, if you decide that your initial context when accessing a
Unix filesystem is the /usr/local directory, then you
need the following line of code.
env.put(Context.PROVIDER_URL, "file:/usr/local");
Or, on a Windows system, if you want the C:\data
directory to be the initial context, your code would look like
the following.
env.put(Context.PROVIDER_URL, "file:C:\\data");
And, optionally, you can also put the user credentials such as the username and password.
env.put(Context.SECURITY_PRINCIPAL, "james");
env.put(Context.SECURITY_CREDENTIALS, "secret");
Having the environment information ready, you can now create the initial context.
Context ctx = new InitialContext(env);
If the object is created successfully, you can use the resulting Context object to access the naming service. The lookup method of the Context interface can be used to retrieve an object by passing its name.
Object obj = ctx.lookup("info.txt");
For example, the following code prepares an environment Hashtable
object, creates an initial context, and retrieves the
info.txt file.
import java.util.Hashtable;
import javax.naming.*;
import java.io.File;
public class Naming {
public static void main(String[] args) {
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.fscontext.RefFSContextFactory");
env.put(Context.PROVIDER_URL,
"file:C:\\123data\\MyArticles\\WhitePagesWithLDAP");
env.put(Context.SECURITY_PRINCIPAL, "james");
env.put(Context.SECURITY_CREDENTIALS, "secret");
try {
Context ctx = new InitialContext(env);
File f = (File)ctx.lookup("info.txt");
}
catch (NamingException e) {
System.out.println(e.toString());
}
}
}
The Object object from the lookup method is cast to a
File object. If the object is a Printer, you can do
something similar:
Printer printer = (Printer) ctx.lookup("BigMomma");
printer.print(report);
Some of the code is in a try-catch wrapper because many methods in the JNDI packages can throw a NamingException.
Other useful methods of the Context interface include the following.
bind -- Binds an object to a name. After the binding,
you can retrieve the object by looking up the name.rebind -- Adds or replaces a binding. If the name is
already bound to an object, it will be unbound and bound with the new
object specified as the argument of this method.unbind -- Removes a binding.list -- Enumerates the names bound in the named
context, along with the class names of objects bound to
them.Every naming method in the Context interface has two overloads: one
that accepts a Name argument and one that accepts a
java.lang.String name. Name is an interface that
represents a generic name; an ordered sequence of zero or more
components.
The overloads that accept Name are useful for applications
that need to manipulate names, that is, composing them, comparing
components, and so on.
A java.lang.String name argument represents a
composite name. The overloads that accept
java.lang.String names are likely to be more useful for
simple applications, such as those that read in a name and look up the
corresponding object.
When you access a directory service, there are several initial
steps to perform. The first is to prepare an environment
Hashtable object to get the initial context.
Hashtable env = new Hashtable();
One of the environment properties you need to set is the
INITIAL_CONTEXT_FACTORY. For example, if you are
accessing an LDAP service, you can use the service provider from
Sun. The code would then look like the following.
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
If you are using a service provider from another vendor, just
replace the second argument to put(). Next, you supply
the location of the service. For example, the following specifies a
location of an LDAP server at
ldap://sendal.jepit.edu.au:389 (389 is the default port for the
LDAP service).
env.put(Context.PROVIDER_URL,
"ldap://sendal.jepit.usyd.edu.au:389");
You can then acquire an initial context by passing the environment
Hashtable. However, unlike accessing a naming system, you use the
DirContext interface instead of the Context
interface.
DirContext ctx = new InitialDirContext(env);
Having a DirContext object, you can access the
directory service using the methods of the DirContext
interface; the important methods of which include
getAttributes, getSchema and
search.
getAttributes -- Returns the attributes of an
object in the directory. For this method to work, you need to pass the
name of the object for which you want the attributes. If you have an
object whose name is "cn=boni, ou=person", you can retrieve the
object's attributes using the following line of code.
Attributes attr = ctx.getAttributes("cn=boni, ou=person");getSchema -- Retrieves the schema associated with
the named object.
search -- Search for entries in a named context or
object. The search must satisfy a given search filter. The syntax of
one of the many search methods is as follows.
public NamingEnumeration search(String name, String filter,
SearchControls cons) throws NamingException
The parameters are given below.
name -- The name of the context or object to search.filter -- The filter expression to use for the
search; may not be null cons -- These control the search. If null, the
default search controls are used (equivalent to (new
SearchControls())).You can create a SearchControls object by using the following code.
SearchControls constraints = new SearchControls();
constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
|
A white pages service for locating a person in an LDAP server. As
mentioned previously, I use the LDAP server from OpenLDAP. In order
to keep the project simple, I use the person object defined in the
core.schema file.
For convenience, the person object in the core.schema file is re-presented here.
objectclass ( 2.5.6.6 NAME 'person' SUP top STRUCTURAL
MUST ( sn $ cn )
MAY ( userPassword $ telephoneNumber $ seeAlso $ description )
)
The person object has two mandatory attributes: sn and
cn, and four optional attributes:
To test the code in this project, you need to populate the directory:
ldapadd -x -D "cn=Manager ,dc=sendal,dc=jepit,dc=edu,dc=au" -w
secret -f example.ldif
This reads the example.ldif file and insert its
content as entries to the server. The example.ldif file
contains the following.
dn: cn=Bulbul, dc=sedal,dc=usyd,dc=edu,dc=au
objectclass: person
cn: Bulbul Kurniawan
sn: Kurniawan
userPassword: secret
telephoneNumber: +61 98371313
dn: cn=boni, dc=sedal,dc=usyd,dc=edu,dc=au
objectclass: person
cn: Boni Milliken
sn: Milliken
userPassword: dog
telephoneNumber: +61 9555 1212
dn: cn=boy, dc=sedal,dc=usyd,dc=edu,dc=au
objectclass: person
cn: Boy Milliken
sn: Milliken
userPassword: taboo
telephoneNumber: +61 98989898
Make sure that you have installed the correct service provider and
your CLASSPATH variable contains the path to the JNDI
packages.
The code for the white pages service is given in Listing 1. The Java code allows you to access the LDAP server and search a person or persons by passing a surname. The code starts by preparing a environment Hashtable object and setting the necessary properties for the environment.
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY,
"com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL,
"ldap://sendal.jepit.edu.au:389");
And then, as explained above, you need a DirContext
object as the initial context, which is done by calling the
InitialDirContext constructor, passing the environment
Hashtable.
DirContext ctx = new InitialDirContext(env);
Once you have a DirContext object, you can use it to
access the LDAP service. To start searching, use the search method by
passing a SearchControls object.
SearchControls constraints = new SearchControls();
constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
NamingEnumeration persons =
ctx.search("dc=sendal,dc=jepit,dc=edu,dc=au",
"(objectclass=person)", constraints);
Then, display the search result, i.e., the attributes of all the person objects that match the search criteria.
For each person object found, you use the getAttributes
method to retrieve the object's attributes. This method returns the
Attributes object. You can then use the get method of the
Attributes object to obtain the value of an attribute by
passing the attribute name.
attributes.get( attributeName );
The part of the code that displays the attribute names of the person objects found is given below.
System.out.println("Distinguished Name \t| " +
"Common Name \t| Surname \t| Phone");
while (persons != null && persons.hasMore()) {
SearchResult sr = (SearchResult) persons.next();
System.out.print( sr.getName() + "\t| "); // distinguised name
Attributes attrs = sr.getAttributes();
attrs.put(new BasicAttribute("sn", searchedSurname));
// attrs.put(new BasicAttribute("cn", "boy"));
System.out.print(attrs.get("cn") + "\t| "); // common name
System.out.print(attrs.get("sn") + "\t| "); // surname
System.out.println(attrs.get("telephoneNumber")); // phone
} // end of while
If you run the code in Listing 1, you can see the result that looks something like the following.
Distinguished Name | Common Name | Surname | Phone
cn=Boni Milliken |cn: boy |sn: Milliken | +61 9555
1212
cn=Boy Milliken |cn: boy |sn: Milliken | +61 98989898
Naming and directory services are important, providing a way to find objects based on their name or other attributes. A directory service is an extension of a naming service in which object has various attributes. So you can you look up an object by its name, and you can get the object's attributes or search for the object based on its attributes. Using a directory service such as an LDAP server, you can create many applications, including the white pages service described above.
Budi Kurniawan is a senior J2EE architect and author.
Return to ONJava.com.
Copyright © 2007 O'Reilly Media, Inc.