Spring LDAP makes it easier to build Spring-based applications that use the Lightweight Directory Access Protocol.
Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.
1. Preface
The Java Naming and Directory Interface (JNDI) is to LDAP programming what Java Database Connectivity (JDBC) is to SQL programming. There are several similarities between JDBC and JNDI/LDAP (Java LDAP). Despite being two completely different APIs with different pros and cons, they share a number of less flattering characteristics:
-
They require extensive plumbing code, even to perform the simplest of tasks.
-
All resources need to be correctly closed, no matter what happens.
-
Exception handling is difficult.
These points often lead to massive code duplication in common use cases of the APIs. As we all know, code duplication is one of the worst “code smells”. All in all, it boils down to this: JDBC and LDAP programming in Java are both incredibly dull and repetitive.
Spring JDBC, a core component of Spring Framework, provides excellent utilities for simplifying SQL programming. We need a similar framework for Java LDAP programming.
2. Introduction
This section offers a relatively quick introduction to Spring LDAP. It includes the following content:
2.1. Overview
Spring LDAP is designed to simplify LDAP programming in Java. Some of the features provided by the library are:
-
JdbcTemplate
-style template simplifications to LDAP programming. -
JPA- or Hibernate-style annotation-based object and directory mapping.
-
Spring Data repository support, including support for QueryDSL.
-
Utilities to simplify building LDAP queries and distinguished names.
-
Proper LDAP connection pooling.
-
Client-side LDAP compensating transaction support.
2.2. Traditional Java LDAP versus LdapTemplate
Consider a method that should search some storage for all persons and return their names in a list. By using JDBC, we would create a connection and run a query by using a statement. We would then loop over the result set and retrieve the column we want, adding it to a list.
Working against an LDAP database with JNDI, we would create a context and perform a search by using a search filter. We would then loop over the resulting naming enumeration, retrieve the attribute we want, and add it to a list.
The traditional way of implementing this person-name search method in Java LDAP looks like the next example. Note the code marked bold - this is the code that actually performs tasks related to the business purpose of the method. The rest is plumbing.
package com.example.repository;
public class TraditionalPersonRepoImpl implements PersonRepo {
public List<String> getAllPersonNames() {
Hashtable env = new Hashtable();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldap://localhost:389/dc=example,dc=com");
DirContext ctx;
try {
ctx = new InitialDirContext(env);
} catch (NamingException e) {
throw new RuntimeException(e);
}
List<String> list = new LinkedList<String>();
NamingEnumeration results = null;
try {
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
results = ctx.search("", "(objectclass=person)", controls);
while (results.hasMore()) {
SearchResult searchResult = (SearchResult) results.next();
Attributes attributes = searchResult.getAttributes();
Attribute attr = attributes.get("cn");
String cn = attr.get().toString();
list.add(cn);
}
} catch (NameNotFoundException e) {
// The base context was not found.
// Just clean up and exit.
} catch (NamingException e) {
throw new RuntimeException(e);
} finally {
if (results != null) {
try {
results.close();
} catch (Exception e) {
// Never mind this.
}
}
if (ctx != null) {
try {
ctx.close();
} catch (Exception e) {
// Never mind this.
}
}
}
return list;
}
}
By using the Spring LDAP AttributesMapper
and LdapTemplate
classes, we get the exact same functionality with the following code:
package com.example.repo;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
public void setLdapTemplate(LdapTemplate ldapTemplate) {
this.ldapTemplate = ldapTemplate;
}
public List<String> getAllPersonNames() {
return ldapTemplate.search(
query().where("objectclass").is("person"),
new AttributesMapper<String>() {
public String mapFromAttributes(Attributes attrs)
throws NamingException {
return attrs.get("cn").get().toString();
}
});
}
}
The amount of boilerplate code is significantly less than in the traditional example.
The LdapTemplate
search method makes sure a DirContext
instance is created, performs the search, maps the attributes to a string by using the given AttributesMapper
,
collects the strings in an internal list, and, finally, returns the list. It also makes sure that the NamingEnumeration
and DirContext
are properly closed and
takes care of any exceptions that might happen.
Naturally, this being a Spring Framework sub-project, we use Spring to configure our application, as follows:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ldap="http://www.springframework.org/schema/ldap"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/ldap https://www.springframework.org/schema/ldap/spring-ldap.xsd">
<ldap:context-source
url="ldap://localhost:389"
base="dc=example,dc=com"
username="cn=Manager"
password="secret" />
<ldap:ldap-template id="ldapTemplate" />
<bean id="personRepo" class="com.example.repo.PersonRepoImpl">
<property name="ldapTemplate" ref="ldapTemplate" />
</bean>
</beans>
To use the custom XML namespace to configure the Spring LDAP components, you need to include references to this namespace in your XML declaration, as in the preceding example. |
2.3. What’s new in 2.2
For complete details of 2.2, see the changelog for 2.2.0.RC1. The highlights of Spring LDAP 2.2 are as follows:
2.4. What’s new in 2.1
For complete details of 2.1, see the changelog for 2.1.0.RC1 and for 2.1.0 The highlights of Spring LDAP 2.1 are as follows.
-
#390: Added Spring Data Hopper support
-
#351: Added support for commons-pool2
-
#370: Added support property placeholders in the XML Namespace
-
#392: Added document Testing Support
-
#401: Added a switch to assertj
-
Migrated from JIRA to GitHub Issues
-
Added Gitter Chat
2.5. What’s new in 2.0
While quite significant modernizations have been made to the Spring LDAP API in version 2.0, great care has been taken to ensure backward compatibility as far as possible. Code that works with Spring LDAP 1.3.x should, with few exceptions, compile and run when you use the 2.0 libraries without any modifications.
The exception is a small number of classes that have been moved to new packages in order to make a couple of important refactorings possible. The moved classes are typically not part of the intended public API, and the migration procedure should be smooth. Whenever a Spring LDAP class cannot be found after upgrade, you should organize the imports in your IDE.
You should expect to encounter some deprecation warnings, though, and there are also a lot of other API improvements. The recommendation for getting as much as possible out of the 2.0 version is to move away from the deprecated classes and methods and migrate to the new, improved API utilities.
The following list briefly describes the most important changes in Spring LDAP 2.0:
-
Java 6 is now required by Spring LDAP. Spring versions starting at 2.0 and up are still supported.
-
The central API has been updated with Java 5+ features such as generics and varargs. As a consequence, the entire
spring-ldap-tiger
module has been deprecated, and we encourage you to migrate to using the core Spring LDAP classes. The parameterization of the core interfaces causes lots of compilation warnings on existing code, and we encourage you to take appropriate action to get rid of these warnings. -
The ODM (Object-Directory Mapping) functionality has been moved to core, and there are new methods in
LdapOperations
andLdapTemplate
that use this automatic translation to and from ODM-annotated classes. See Object-Directory Mapping (ODM) for more information. -
A custom XML namespace is now (finally) provided to simplify configuration of Spring LDAP. See Configuration for more information.
-
Spring LDAP now provides support for Spring Data Repository and QueryDSL. See Spring LDAP Repositories for more information.
-
Name
instances as attribute values are now handled properly with regards to distinguished name equality inDirContextAdapter
and ODM. SeeDirContextAdapter
and Distinguished Names as Attribute Values and ODM and Distinguished Names as Attribute Values for more information. -
DistinguishedName
and associated classes have been deprecated in favor of the standard JavaLdapName
. See Dynamically Building Distinguished Names for information on how the library helps when working withLdapName
objects. -
Fluent LDAP query building support has been added. This makes for a more pleasant programming experience when working with LDAP searches in Spring LDAP. See Building LDAP Queries and Advanced LDAP Queries for more information about the LDAP query builder support.
-
The old
authenticate
methods inLdapTemplate
have been deprecated in favor of a couple of newauthenticate
methods that work withLdapQuery
objects and throw exceptions on authentication failure, making it easier for the user to find out what caused an authentication attempt to fail. -
The samples have been polished and updated to make use of the features in 2.0. Quite a bit of effort has been put into providing a useful example of an LDAP user management application.
2.6. Packaging Overview
At a minimum, to use Spring LDAP you need the following:
-
spring-ldap-core
: The Spring LDAP library -
spring-core
: Miscellaneous utility classes used internally by the framework -
spring-beans
: Interfaces and classes for manipulating Java beans -
slf4j
: A simple logging facade, used internally
In addition to the required dependencies, the following optional dependencies are required for certain functionality:
-
spring-data-ldap
: Base infrastructure for repository support and so on -
spring-context
: Needed if your application is wired up by using the Spring Application Context.spring-context
adds the ability for application objects to obtain resources by using a consistent API. It is definitely needed if you plan to use theBaseLdapPathBeanPostProcessor
. -
spring-tx
: Needed if you plan to use the client-side compensating transaction support. -
spring-jdbc
: Needed if you plan to use the client-side compensating transaction support. -
commons-pool
: Needed if you plan to use the pooling functionality. -
spring-batch
: Needed if you plan to use the LDIF parsing functionality together with Spring Batch.
spring-data-ldap transitively adds spring-repository.xsd , which spring-ldap.xsd uses.
Because of this, Spring LDAP’s XML config support requires the dependency even when Spring Data’s feature set is not in use.
|
2.7. Getting Started
The samples provide some useful examples of how to use Spring LDAP for common use cases.
2.8. Support
If you have questions, ask them on Stack Overflow with the spring-ldap
tag.
The project web page is https://spring.io/spring-ldap/.
2.9. Acknowledgements
The initial effort when starting the Spring LDAP project was sponsored by Jayway. Current maintenance of the project is funded by Pivotal, which has since been acquired by VMware.
Thanks to Structure101 for providing an open source license that has come in handy for keeping the project structure in check.
3. Basic Usage
This section describes the basics of using Spring LDAP. It contains the following content:
3.1. Search and Lookup Using AttributesMapper
The following example uses an AttributesMapper
to build a List of all the common names of all the person objects.
AttributesMapper
that returns a single attributepackage com.example.repo;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
public void setLdapTemplate(LdapTemplate ldapTemplate) {
this.ldapTemplate = ldapTemplate;
}
public List<String> getAllPersonNames() {
return ldapTemplate.search(
query().where("objectclass").is("person"),
new AttributesMapper<String>() {
public String mapFromAttributes(Attributes attrs)
throws NamingException {
return (String) attrs.get("cn").get();
}
});
}
}
The inline implementation of AttributesMapper
gets the desired attribute value from the Attributes
object and returns it. Internally, LdapTemplate
iterates over all entries found, calls the given AttributesMapper
for each entry, and collects the results in a list. The list is then returned by the search
method.
Note that the AttributesMapper
implementation could easily be modified to return a full Person
object, as follows:
package com.example.repo;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
...
private class PersonAttributesMapper implements AttributesMapper<Person> {
public Person mapFromAttributes(Attributes attrs) throws NamingException {
Person person = new Person();
person.setFullName((String)attrs.get("cn").get());
person.setLastName((String)attrs.get("sn").get());
person.setDescription((String)attrs.get("description").get());
return person;
}
}
public List<Person> getAllPersons() {
return ldapTemplate.search(query()
.where("objectclass").is("person"), new PersonAttributesMapper());
}
}
Entries in LDAP are uniquely identified by their distinguished name (DN).
If you have the DN of an entry, you can retrieve the entry directly without searching for it.
This is called a “lookup” in Java LDAP. The following example shows a lookup for a Person
object:
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
...
public Person findPerson(String dn) {
return ldapTemplate.lookup(dn, new PersonAttributesMapper());
}
}
The preceding example looks up the specified DN and passes the found attributes to the supplied AttributesMapper
— in this case, resulting in a Person
object.
3.2. Building LDAP Queries
LDAP searches involve a number of parameters, including the following:
-
Base LDAP path: Where in the LDAP tree should the search start.
-
Search scope: How deep in the LDAP tree should the search go.
-
Attributes to return.
-
Search filter: The criteria to use when selecting elements within scope.
Spring LDAP provides an LdapQueryBuilder
with a fluent API for building LDAP Queries.
Suppose you want to perform a search starting at the base DN dc=261consulting,dc=com
,
limiting the returned attributes to cn
and sn
, with a filter of (&(objectclass=person)(sn=?))
, where we want the ?
to be replaced with the value of the lastName
parameter.
The following example shows how to do it by using the LdapQueryBuilder
:
package com.example.repo;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
...
public List<String> getPersonNamesByLastName(String lastName) {
LdapQuery query = query()
.base("dc=261consulting,dc=com")
.attributes("cn", "sn")
.where("objectclass").is("person")
.and("sn").is(lastName);
return ldapTemplate.search(query,
new AttributesMapper<String>() {
public String mapFromAttributes(Attributes attrs)
throws NamingException {
return (String) attrs.get("cn").get();
}
});
}
}
In addition to simplifying building of complex search parameters, the LdapQueryBuilder and its associated classes also provide proper escaping of any unsafe characters in search filters. This prevents “LDAP injection”, where a user might use such characters to inject unwanted operations into your LDAP operations.
|
LdapTemplate includes many overloaded methods for performing LDAP searches. This is in order to accommodate as many different use cases and programming style preferences as possible. For the vast majority of use cases, the methods that take an LdapQuery as input are the recommended methods to use.
|
The AttributesMapper is only one of the available callback interfaces you can use when handling search and lookup data. See Simplifying Attribute Access and Manipulation with DirContextAdapter for alternatives.
|
For more information on the LdapQueryBuilder
, see Advanced LDAP Queries.
3.3. Dynamically Building Distinguished Names
The standard Java implementation of Distinguished Name (LdapName
)
performs well when it comes to parsing Distinguished Names. However, in practical use, this implementation has a number of shortcomings:
-
The
LdapName
implementation is mutable, which is badly suited for an object that represents identity. -
Despite its mutable nature, the API for dynamically building or modifying Distinguished Names by using
LdapName
is cumbersome. Extracting values of indexed or (particularly) named components is also a little bit awkward. -
Many of the operations on
LdapName
throw checked exceptions, requiringtry-catch
statements for situations where the error is typically fatal and cannot be repaired in a meaningful manner.
To simplify working with Distinguished Names, Spring LDAP provides an LdapNameBuilder
,
as well as a number of utility methods in LdapUtils
that help when working with LdapName
.
3.3.1. Examples
This section presents a few examples of the subjects covered in the preceding sections.
The first example dynamically builds an LdapName
by using LdapNameBuilder
:
LdapName
by using LdapNameBuilder
package com.example.repo;
import org.springframework.ldap.support.LdapNameBuilder;
import javax.naming.Name;
public class PersonRepoImpl implements PersonRepo {
public static final String BASE_DN = "dc=example,dc=com";
protected Name buildDn(Person p) {
return LdapNameBuilder.newInstance(BASE_DN)
.add("c", p.getCountry())
.add("ou", p.getCompany())
.add("cn", p.getFullname())
.build();
}
...
}
Assume that a Person
has the following attributes:
Attribute Name | Attribute Value |
---|---|
|
Sweden |
|
Some Company |
|
Some Person |
The preceding code would then result in the following distinguished name:
cn=Some Person, ou=Some Company, c=Sweden, dc=example, dc=com
The following example extracts values from a distinguished name by using LdapUtils
LdapUtils
package com.example.repo;
import org.springframework.ldap.support.LdapNameBuilder;
import javax.naming.Name;
public class PersonRepoImpl implements PersonRepo {
...
protected Person buildPerson(Name dn, Attributes attrs) {
Person person = new Person();
person.setCountry(LdapUtils.getStringValue(dn, "c"));
person.setCompany(LdapUtils.getStringValue(dn, "ou"));
person.setFullname(LdapUtils.getStringValue(dn, "cn"));
// Populate rest of person object using attributes.
return person;
}
}
Since Java versions prior to and including 1.4 did not provide any public Distinguished Name implementation at all, Spring LDAP 1.x provided its own implementation, DistinguishedName
.
This implementation suffered from a couple of shortcomings of its own and has been deprecated in version 2.0. You should now use LdapName
along with the utilities described earlier.
3.4. Binding and Unbinding
This section describes how to add and remove data. Updating is covered in the next section.
3.4.1. Adding Data
Inserting data in Java LDAP is called binding. This is somewhat confusing, because in LDAP terminology, “bind” means something completely different.
A JNDI bind performs an LDAP Add operation, associating a new entry that has a specified distinguished name with a set of attributes.
The following example adds data by using LdapTemplate
:
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
...
public void create(Person p) {
Name dn = buildDn(p);
ldapTemplate.bind(dn, null, buildAttributes(p));
}
private Attributes buildAttributes(Person p) {
Attributes attrs = new BasicAttributes();
BasicAttribute ocattr = new BasicAttribute("objectclass");
ocattr.add("top");
ocattr.add("person");
attrs.put(ocattr);
attrs.put("cn", "Some Person");
attrs.put("sn", "Person");
return attrs;
}
}
Manual attributes building is — while dull and verbose — sufficient for many purposes. You can, however, simplify the binding operation further, as described in Simplifying Attribute Access and Manipulation with DirContextAdapter
.
3.4.2. Removing Data
Removing data in Java LDAP is called unbinding.
A JNDI unbind performs an LDAP Delete operation, removing the entry associated with the specified distinguished name from the LDAP tree.
The following example removes data by using LdapTemplate
:
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
...
public void delete(Person p) {
Name dn = buildDn(p);
ldapTemplate.unbind(dn);
}
}
3.5. Updating
In Java LDAP, data can be modified in two ways: either by using rebind
or by using modifyAttributes
.
3.5.1. Updating by Using Rebind
A rebind
is a crude way to modify data. It is basically an unbind
followed by a bind
.
The following example uses rebind
:
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
...
public void update(Person p) {
Name dn = buildDn(p);
ldapTemplate.rebind(dn, null, buildAttributes(p));
}
}
3.5.2. Updating by Using modifyAttributes
A more sophisticated way of modifying data is to use modifyAttributes
. This operation takes an array of explicit attribute modifications
and performs them on a specific entry, as follows:
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
...
public void updateDescription(Person p) {
Name dn = buildDn(p);
Attribute attr = new BasicAttribute("description", p.getDescription())
ModificationItem item = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, attr);
ldapTemplate.modifyAttributes(dn, new ModificationItem[] {item});
}
}
Building Attributes
and ModificationItem
arrays is a lot of work. However, as we describe in Simplifying Attribute Access and Manipulation with DirContextAdapter
,
Spring LDAP provides more help for simplifying these operations.
4. Simplifying Attribute Access and Manipulation with DirContextAdapter
A little-known — and probably underestimated — feature of the Java LDAP API is the ability to register a DirObjectFactory
to automatically create objects from found LDAP entries.
Spring LDAP makes use of this feature to return DirContextAdapter
instances in certain search and lookup operations.
DirContextAdapter
is a useful tool for working with LDAP attributes, particularly when adding or modifying data.
4.1. Search and Lookup Using ContextMapper
Whenever an entry is found in the LDAP tree, its attributes and Distinguished Name (DN) are used by Spring LDAP to construct a DirContextAdapter
.
This lets us use a ContextMapper
instead of an AttributesMapper
to transform found values, as follows:
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
...
private static class PersonContextMapper implements ContextMapper {
public Object mapFromContext(Object ctx) {
DirContextAdapter context = (DirContextAdapter)ctx;
Person p = new Person();
p.setFullName(context.getStringAttribute("cn"));
p.setLastName(context.getStringAttribute("sn"));
p.setDescription(context.getStringAttribute("description"));
return p;
}
}
public Person findByPrimaryKey(
String name, String company, String country) {
Name dn = buildDn(name, company, country);
return ldapTemplate.lookup(dn, new PersonContextMapper());
}
}
As shown in the preceding example, we can retrieve the attribute values directly by name without having to go through the Attributes
and Attribute
classes.
This is particularly useful when working with multi-value attributes.
Extracting values from multi-value attributes normally requires looping through a NamingEnumeration
of attribute values returned from the Attributes
implementation.
DirContextAdapter
does this for you
in the getStringAttributes()
or getObjectAttributes()
methods.
The following example uses the getStringAttributes
method:
getStringAttributes()
private static class PersonContextMapper implements ContextMapper {
public Object mapFromContext(Object ctx) {
DirContextAdapter context = (DirContextAdapter)ctx;
Person p = new Person();
p.setFullName(context.getStringAttribute("cn"));
p.setLastName(context.getStringAttribute("sn"));
p.setDescription(context.getStringAttribute("description"));
// The roleNames property of Person is an String array
p.setRoleNames(context.getStringAttributes("roleNames"));
return p;
}
}
4.1.1. Using AbstractContextMapper
Spring LDAP provides an abstract base implementation of ContextMapper
, called AbstractContextMapper
.
This implementation automatically takes care of the casting of the supplied Object
parameter to DirContexOperations
.
Using AbstractContextMapper
, the PersonContextMapper
shown earlier can thus be re-written as follows:
AbstractContextMapper
private static class PersonContextMapper extends AbstractContextMapper {
public Object doMapFromContext(DirContextOperations ctx) {
Person p = new Person();
p.setFullName(ctx.getStringAttribute("cn"));
p.setLastName(ctx.getStringAttribute("sn"));
p.setDescription(ctx.getStringAttribute("description"));
return p;
}
}
4.2. Adding and Updating Data by Using DirContextAdapter
`
While useful when extracting attribute values, DirContextAdapter
is even more powerful for managing the details
involved in adding and updating data.
4.2.1. Adding Data by Using DirContextAdapter
The following example uses DirContextAdapter
to implement an improved implementation of the create
repository method presented in Adding Data:
DirContextAdapter
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
...
public void create(Person p) {
Name dn = buildDn(p);
DirContextAdapter context = new DirContextAdapter(dn);
context.setAttributeValues("objectclass", new String[] {"top", "person"});
context.setAttributeValue("cn", p.getFullname());
context.setAttributeValue("sn", p.getLastname());
context.setAttributeValue("description", p.getDescription());
ldapTemplate.bind(context);
}
}
Note that we use the DirContextAdapter
instance as the second parameter to bind, which should be a Context
.
The third parameter is null
, since we do not specify the attributes explicitly.
Also note the use of the setAttributeValues()
method when setting the objectclass
attribute values.
The objectclass
attribute is multi-value. Similar to the troubles of extracting muti-value attribute data,
building multi-value attributes is tedious and verbose work. By using the setAttributeValues()
method, you can have DirContextAdapter
handle that work for you.
4.2.2. Updating Data by Using DirContextAdapter
We previously saw that updating by using modifyAttributes
is the recommended approach, but that doing so requires us to perform
the task of calculating attribute modifications and constructing ModificationItem
arrays accordingly.
DirContextAdapter
can do all of this for us, as follows:
DirContextAdapter
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
...
public void update(Person p) {
Name dn = buildDn(p);
DirContextOperations context = ldapTemplate.lookupContext(dn);
context.setAttributeValue("cn", p.getFullname());
context.setAttributeValue("sn", p.getLastname());
context.setAttributeValue("description", p.getDescription());
ldapTemplate.modifyAttributes(context);
}
}
When no mapper is passed to a ldapTemplate.lookup()
, the result is a DirContextAdapter
instance.
While the lookup
method returns an Object
, the lookupContext
convenience method method automatically casts the return value to a DirContextOperations
(the interface that DirContextAdapter
implements).
Notice that we have duplicate code in the create
and update
methods. This code maps from a domain object to a context. It can be extracted to a separate method, as follows:
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
...
public void create(Person p) {
Name dn = buildDn(p);
DirContextAdapter context = new DirContextAdapter(dn);
context.setAttributeValues("objectclass", new String[] {"top", "person"});
mapToContext(p, context);
ldapTemplate.bind(context);
}
public void update(Person p) {
Name dn = buildDn(p);
DirContextOperations context = ldapTemplate.lookupContext(dn);
mapToContext(person, context);
ldapTemplate.modifyAttributes(context);
}
protected void mapToContext (Person p, DirContextOperations context) {
context.setAttributeValue("cn", p.getFullName());
context.setAttributeValue("sn", p.getLastName());
context.setAttributeValue("description", p.getDescription());
}
}
4.3. DirContextAdapter
and Distinguished Names as Attribute Values
When managing security groups in LDAP, it is common to have attribute values that represent distinguished names. Since distinguished name equality differs from String equality (for example, whitespace and case differences are ignored in distinguished name equality), calculating attribute modifications using string equality does not work as expected.
For instance, if a member
attribute has a value of cn=John Doe,ou=People
and we call ctx.addAttributeValue("member", "CN=John Doe, OU=People")
,
the attribute is now considered to have two values, even though the strings actually represent the same
distinguished name.
As of Spring LDAP 2.0, supplying javax.naming.Name
instances to the attribute modification methods makes DirContextAdapter
use distinguished name equality when calculating attribute modifications. If we modify the earlier example to be
ctx.addAttributeValue("member", LdapUtils.newLdapName("CN=John Doe, OU=People"))
, it does not render a modification, as the following example shows:
public class GroupRepo implements BaseLdapNameAware {
private LdapTemplate ldapTemplate;
private LdapName baseLdapPath;
public void setLdapTemplate(LdapTemplate ldapTemplate) {
this.ldapTemplate = ldapTemplate;
}
public void setBaseLdapPath(LdapName baseLdapPath) {
this.setBaseLdapPath(baseLdapPath);
}
public void addMemberToGroup(String groupName, Person p) {
Name groupDn = buildGroupDn(groupName);
Name userDn = buildPersonDn(
person.getFullname(),
person.getCompany(),
person.getCountry());
DirContextOperation ctx = ldapTemplate.lookupContext(groupDn);
ctx.addAttributeValue("member", userDn);
ldapTemplate.update(ctx);
}
public void removeMemberFromGroup(String groupName, Person p) {
Name groupDn = buildGroupDn(String groupName);
Name userDn = buildPersonDn(
person.getFullname(),
person.getCompany(),
person.getCountry());
DirContextOperation ctx = ldapTemplate.lookupContext(groupDn);
ctx.removeAttributeValue("member", userDn);
ldapTemplate.update(ctx);
}
private Name buildGroupDn(String groupName) {
return LdapNameBuilder.newInstance("ou=Groups")
.add("cn", groupName).build();
}
private Name buildPersonDn(String fullname, String company, String country) {
return LdapNameBuilder.newInstance(baseLdapPath)
.add("c", country)
.add("ou", company)
.add("cn", fullname)
.build();
}
}
In the preceding example, we implement BaseLdapNameAware
to get the base LDAP path as described in Obtaining a Reference to the Base LDAP Path.
This is necessary because distinguished names as member attribute values must always be absolute from the directory root.
4.4. A Complete PersonRepository
Class
To illustrate the usefulness of Spring LDAP and DirContextAdapter
, the following example shows a complete Person
Repository implementation for LDAP:
package com.example.repo;
import java.util.List;
import javax.naming.Name;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.ldap.LdapName;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.ContextMapper;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.DirContextAdapter;
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.filter.WhitespaceWildcardsFilter;
import static org.springframework.ldap.query.LdapQueryBuilder.query;
public class PersonRepoImpl implements PersonRepo {
private LdapTemplate ldapTemplate;
public void setLdapTemplate(LdapTemplate ldapTemplate) {
this.ldapTemplate = ldapTemplate;
}
public void create(Person person) {
DirContextAdapter context = new DirContextAdapter(buildDn(person));
mapToContext(person, context);
ldapTemplate.bind(context);
}
public void update(Person person) {
Name dn = buildDn(person);
DirContextOperations context = ldapTemplate.lookupContext(dn);
mapToContext(person, context);
ldapTemplate.modifyAttributes(context);
}
public void delete(Person person) {
ldapTemplate.unbind(buildDn(person));
}
public Person findByPrimaryKey(String name, String company, String country) {
Name dn = buildDn(name, company, country);
return ldapTemplate.lookup(dn, getContextMapper());
}
public List findByName(String name) {
LdapQuery query = query()
.where("objectclass").is("person")
.and("cn").whitespaceWildcardsLike("name");
return ldapTemplate.search(query, getContextMapper());
}
public List findAll() {
EqualsFilter filter = new EqualsFilter("objectclass", "person");
return ldapTemplate.search(LdapUtils.emptyPath(), filter.encode(), getContextMapper());
}
protected ContextMapper getContextMapper() {
return new PersonContextMapper();
}
protected Name buildDn(Person person) {
return buildDn(person.getFullname(), person.getCompany(), person.getCountry());
}
protected Name buildDn(String fullname, String company, String country) {
return LdapNameBuilder.newInstance()
.add("c", country)
.add("ou", company)
.add("cn", fullname)
.build();
}
protected void mapToContext(Person person, DirContextOperations context) {
context.setAttributeValues("objectclass", new String[] {"top", "person"});
context.setAttributeValue("cn", person.getFullName());
context.setAttributeValue("sn", person.getLastName());
context.setAttributeValue("description", person.getDescription());
}
private static class PersonContextMapper extends AbstractContextMapper<Person> {
public Person doMapFromContext(DirContextOperations context) {
Person person = new Person();
person.setFullName(context.getStringAttribute("cn"));
person.setLastName(context.getStringAttribute("sn"));
person.setDescription(context.getStringAttribute("description"));
return person;
}
}
}
In several cases, the Distinguished Name (DN) of an object is constructed by using properties of the object.
In the preceding example, the country, company and full name of the Person are used in the DN, which means that updating any of these properties actually requires moving the entry in the LDAP tree by using the rename() operation in addition to updating the Attribute values.
Since this is highly implementation-specific, this is something you need to keep track of yourself, either by disallowing the user to change these properties or performing the rename() operation in your update() method if needed.
Note that, by using Object-Directory Mapping (ODM), the library can automatically handle this for you if you annotate your domain classes appropriately.
|
5. Object-Directory Mapping (ODM)
Object-relational mapping frameworks (such as Hibernate and JPA) offer developers the ability to use annotations to map relational database tables to Java objects.
The Spring LDAP project offers a similar ability with respect to LDAP directories through a number of methods in LdapOperations
:
-
<T> T findByDn(Name dn, Class<T> clazz)
-
<T> T findOne(LdapQuery query, Class<T> clazz)
-
<T> List<T> find(LdapQuery query, Class<T> clazz)
-
<T> List<T> findAll(Class<T> clazz)
-
<T> List<T> findAll(Name base, SearchControls searchControls, Class<T> clazz)
-
<T> List<T> findAll(Name base, Filter filter, SearchControls searchControls, Class<T> clazz)
-
void create(Object entry)
-
void update(Object entry)
-
void delete(Object entry)
5.1. Annotations
Entity classes managed with the object mapping methods are required to be annotated with annotations from the org.springframework.ldap.odm.annotations
package. The available annotations are:
-
@Entry
: Class level annotation indicating theobjectClass
definitions to which the entity maps. (required) -
@Id
: Indicates the entity DN. The field declaring this attribute must be a derivative of thejavax.naming.Name
class. (required) -
@Attribute
: Indicates the mapping of a directory attribute to the object class field. -
@DnAttribute
: Indicates the mapping of a DN attribute to the object class field. -
@Transient
: Indicates the field is not persistent and should be ignored by theOdmManager
.
The @Entry
and @Id
annotations are required to be declared on managed classes.
@Entry
is used to specify which object classes the entity maps to and (optionally) the directory root of the LDAP entries represented by the class.
All object classes for which fields are mapped are required to be declared. Note that, when creating new entries of the managed class,
only the declared object classes are used.
In order for a directory entry to be considered a match to the managed entity, all object classes declared by the directory entry must be declared by the @Entry
annotation.
For example, assume that you have entries in your LDAP tree that have the following object classes: inetOrgPerson,organizationalPerson,person,top
.
If you are interested only in changing the attributes defined in the person
object class, you can annotate your @Entry
with @Entry(objectClasses = { "person", "top" })
.
However, if you want to manage attributes defined in the inetOrgPerson
objectclass, you need to use the following: @Entry(objectClasses = { "inetOrgPerson", "organizationalPerson", "person", "top" })
.
The @Id
annotation is used to map the distinguished name of the entry to a field. The field must be an instance of javax.naming.Name
.
The @Attribute
annotation is used to map object class fields to entity fields.
@Attribute
is required to declare the name of the object class property to which the field maps and may optionally declare the syntax OID of the LDAP attribute, to guarantee exact matching.
@Attribute
also provides the type declaration, which lets you indicate whether the attribute is regarded as binary-based or string-based by the LDAP JNDI provider.
The @DnAttribute
annotation is used to map object class fields to and from components in the distinguished name of an entry.
Fields annotated with @DnAttribute
are automatically populated with the appropriate value from the distinguished name when an entry is read from the directory tree.
Only fields of type String
can be annotated with @DnAttribute
. Other types are not supported.
If the index
attribute of all @DnAttribute
annotations in a class is specified, the DN can also be automatically calculated when creating and updating entries.
For update scenarios, this also automatically takes care of moving entries in the tree if attributes that are part of the distinguished name have changed.
The @Transient
annotation indicates that the field should be ignored by the object directory mapping and not mapped to an underlying LDAP property. Note that if a @DnAttribute
is not to be bound to an Attribute
. That is, it is only part of the Distinguished Name and not represented by an object attribute. It must also be annotated with @Transient
.
5.2. Execution
When all components have been properly configured and annotated, the object mapping methods of LdapTemplate
can be used as follows:
@Entry(objectClasses = { "person", "top" }, base="ou=someOu")
public class Person {
@Id
private Name dn;
@Attribute(name="cn")
@DnAttribute(value="cn", index=1)
private String fullName;
// No @Attribute annotation means this will be bound to the LDAP attribute
// with the same value
private String description;
@DnAttribute(value="ou", index=0)
@Transient
private String company;
@Transient
private String someUnmappedField;
// ...more attributes below
}
public class OdmPersonRepo {
@Autowired
private LdapTemplate ldapTemplate;
public Person create(Person person) {
ldapTemplate.create(person);
return person;
}
public Person findByUid(String uid) {
return ldapTemplate.findOne(query().where("uid").is(uid), Person.class);
}
public void update(Person person) {
ldapTemplate.update(person);
}
public void delete(Person person) {
ldapTemplate.delete(person);
}
public List<Person> findAll() {
return ldapTemplate.findAll(Person.class);
}
public List<Person> findByLastName(String lastName) {
return ldapTemplate.find(query().where("sn").is(lastName), Person.class);
}
}
5.3. ODM and Distinguished Names as Attribute Values
Security groups in LDAP commonly contain a multi-value attribute, where each of the values is the distinguished name
of a user in the system. The difficulties involved when handling these kinds of attributes are discussed in DirContextAdapter
and Distinguished Names as Attribute Values.
ODM also has support for javax.naming.Name
attribute values, making group modifications easy, as the following example shows:
@Entry(objectClasses = {"top", "groupOfUniqueNames"}, base = "cn=groups")
public class Group {
@Id
private Name dn;
@Attribute(name="cn")
@DnAttribute("cn")
private String name;
@Attribute(name="uniqueMember")
private Set<Name> members;
public Name getDn() {
return dn;
}
public void setDn(Name dn) {
this.dn = dn;
}
public Set<Name> getMembers() {
return members;
}
public void setMembers(Set<Name> members) {
this.members = members;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public void addMember(Name member) {
members.add(member);
}
public void removeMember(Name member) {
members.remove(member);
}
}
When you modify group members by using setMembers
, addMember
, and removeMember
and then calling ldapTemplate.update()
,
attribute modifications are calculated by using distinguished name equality, meaning that the text formatting of
distinguished names is disregarded when figuring out whether they are equal.
6. Advanced LDAP Queries
This section covers various how to use LDAP queries with Spring LDAP.
6.1. LDAP Query Builder Parameters
The LdapQueryBuilder
and its associated classes are intended to support all of the parameters that can be supplied to an LDAP search.
The following parameters are supported:
-
base
: Specifies the root DN in the LDAP tree where the search should start. -
searchScope
: Specifies how deep into the LDAP tree the search should traverse. -
attributes
: Specifies the attributes to return from the search. The default is all. -
countLimit
: Specifies the maximum number of entries to return from the search. -
timeLimit
: Specifies the maximum time that the search may take. -
Search filter: The conditions that the entries we are looking for must meet.
An LdapQueryBuilder
is created with a call to the query
method of LdapQueryBuilder
. It is intended as a fluent builder API, where the base parameters are defined first, followed by the filter specification calls. Once filter conditions have been started to be defined with a call to the where
method of LdapQueryBuilder
, later attempts to call (for example) base
are rejected. The base search parameters are optional, but at least one filter specification call is required.
The following query searches for all entries with an object class of Person
:
Person
import static org.springframework.ldap.query.LdapQueryBuilder.query;
...
List<Person> persons = ldapTemplate.search(
query().where("objectclass").is("person"),
new PersonAttributesMapper());
The following query searches for all entries with an object class of person
and a cn
(common name) of John Doe
:
person
and cn=John Doe
import static org.springframework.ldap.query.LdapQueryBuilder.query;
...
List<Person> persons = ldapTemplate.search(
query().where("objectclass").is("person")
.and("cn").is("John Doe"),
new PersonAttributesMapper());
The following query searches for all entries with an object class of person
and starting at a dc
(domain component) of dc=261consulting,dc=com
:
person
starting at dc=261consulting,dc=com
import static org.springframework.ldap.query.LdapQueryBuilder.query;
...
List<Person> persons = ldapTemplate.search(
query().base("dc=261consulting,dc=com")
.where("objectclass").is("person"),
new PersonAttributesMapper());
The following query returns the cn
(common name) attribute for all entries with an object class of person
and starting at a dc
(domain component) of dc=261consulting,dc=com
:
Person
starting at dc=261consulting,dc=com
, returning only the cn
attributeimport static org.springframework.ldap.query.LdapQueryBuilder.query;
...
List<Person> persons = ldapTemplate.search(
query().base("dc=261consulting,dc=com")
.attributes("cn")
.where("objectclass").is("person"),
new PersonAttributesMapper());
The following query uses or
to search for multiple spellings of a common name (cn
):
or
criteriaimport static org.springframework.ldap.query.LdapQueryBuilder.query;
...
List<Person> persons = ldapTemplate.search(
query().where("objectclass").is("person"),
.and(query().where("cn").is("Doe").or("cn").is("Doo"));
new PersonAttributesMapper());
6.2. Filter Criteria
The earlier examples demonstrate simple equals conditions in LDAP filters. The LDAP query builder has support for the following criteria types:
-
is
: Specifies an equals (=) condition. -
gte
: Specifies a greater-than-or-equals (>=) condition. -
lte
: Specifies a less-than-or-equals (⇐) condition. -
like
: Specifies a “like” condition where wildcards can be included in the query — for example,where("cn").like("J*hn Doe")
results in the following filter:(cn=J*hn Doe)
. -
whitespaceWildcardsLike
: Specifies a condition where all whitespace is replaced with wildcards — for example,where("cn").whitespaceWildcardsLike("John Doe")
results in the following filter:(cn=John*Doe)
. -
isPresent
: Specifies a condition that checks for the presence of an attribute — for example,where("cn").isPresent()
results in the following filter:(cn=*)
. -
not
: Specifies that the current condition should be negated — for example,where("sn").not().is("Doe)
results in the following filter:(!(sn=Doe))
6.3. Hardcoded Filters
There may be occasions when you want to specify a hardcoded filter as input to an LdapQuery
. LdapQueryBuilder
has two methods for this purpose:
-
filter(String hardcodedFilter)
: Uses the specified string as a filter. Note that the specified input string is not touched in any way, meaning that this method is not particularly well suited if you are building filters from user input. -
filter(String filterFormat, String… params)
: Uses the specified string as input toMessageFormat
, properly encoding the parameters and inserting them at the specified places in the filter string. -
filter(Filter filter)
: Uses the specified filter.
You cannot mix the hardcoded filter methods with the where
approach described earlier. It is either one or the other. If you specify a filter by using filter()
, you get an exception if you try to call where
afterwards.
7. Configuration
The recommended way of configuring Spring LDAP is to use the custom XML configuration namespace. To make this available, you need to include the Spring LDAP namespace declaration in your bean file, as follows:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ldap="http://www.springframework.org/schema/ldap"
xsi:schemaLocation="http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/ldap https://www.springframework.org/schema/ldap/spring-ldap.xsd">
7.1. ContextSource
Configuration
ContextSource
is defined by using an <ldap:context-source>
tag.
The simplest possible context-source
declaration requires you to specify a server URL, a username, and a password, as follows:
<ldap:context-source
username="cn=Administrator"
password="secret"
url="ldap://localhost:389" />
The preceding example creates an LdapContextSource
with default values (see the table after this paragraph) and the URL and authentication credentials as specified.
The configurable attributes on context-source are as follows (required attributes marked with *):
Attribute | Default | Description |
---|---|---|
|
|
The ID of the created bean. |
|
The username (principal) to use when authenticating with the LDAP server.
This is usually the distinguished name of an admin user (for example, |
|
|
The password (credentials) to use when authenticating with the LDAP server. Required if |
|
|
The URL of the LDAP server to use. The URL should be in the following format: |
|
|
|
The base DN. When this attribute has been configured, all Distinguished Names supplied to and received from LDAP operations are relative to the specified LDAP path. This can significantly simplify working against the LDAP tree. However, there are several occasions when you need to have access to the base path. For more information on this, see Obtaining a Reference to the Base LDAP Path |
|
|
Defines whether read-only operations are performed by using an anonymous (unauthenticated) context.
Note that setting this parameter to |
|
|
Defines the strategy with which to handle referrals, as described here. The valid values are:
|
|
|
Specify whether native Java LDAP connection pooling should be used. Consider using Spring LDAP connection pooling instead. See Pooling Support for more information. |
|
A |
ID of the |
|
A |
ID of the |
|
A reference to a |
7.1.1. DirContext
Authentication
When DirContext
instances are created to be used for performing operations on an LDAP server, these contexts often need to be authenticated.
Spring LDAP offers various options for configuring this.
This section refers to authenticating contexts in the core functionality of the ContextSource , to construct DirContext instances for use by LdapTemplate . LDAP is commonly used for the sole purpose of user authentication, and the ContextSource may be used for that as well. That process is discussed in User Authentication using Spring LDAP.
|
By default, authenticated contexts are created for both read-only and read-write operations. You should specify the username
and password
of the LDAP user to be used for authentication on the context-source
element.
If username is the Distinguished Name (DN) of an LDAP user, it needs to be the full DN of the user from the root of the LDAP tree, regardless of whether a base LDAP path has been specified on the context-source element.
|
Some LDAP server setups allow anonymous read-only access. If you want to use anonymous contexts for read-only operations, set the anonymous-read-only
attribute to true
.
Custom DirContext
Authentication Processing
The default authentication mechanism used in Spring LDAP is SIMPLE
authentication. This means that the principal (as specified by the username
attribute) and the credentials (as specified by the password
) are set in the Hashtable
that is sent to the DirContext
implementation constructor.
There are many occasions when this processing is not sufficient. For instance, LDAP Servers are commonly set up to accept communication only on a secure TLS channel. There might be a need to use the particular LDAP Proxy Auth mechanism or other concerns.
You can specify an alternative authentication mechanism by supplying a DirContextAuthenticationStrategy
implementation reference to the context-source
element. To do so, set the authentication-strategy-ref
attribute.
TLS
Spring LDAP provides two different configuration options for LDAP servers that require TLS secure channel communication: DefaultTlsDirContextAuthenticationStrategy
and ExternalTlsDirContextAuthenticationStrategy
.
Both implementations negotiate a TLS channel on the target connection, but they differ in the actual authentication mechanism.
Where DefaultTlsDirContextAuthenticationStrategy
applies SIMPLE authentication on the secure channel (by using the specified username
and password
), the ExternalTlsDirContextAuthenticationStrategy
uses EXTERNAL SASL authentication, applying a client certificate that is configured by using system properties for authentication.
Since different LDAP server implementations respond differently to explicit shutdown of the TLS channel (some servers require the connection be shut down gracefully, while others do not support it), the TLS DirContextAuthenticationStrategy
implementations support specifying the shutdown behavior by using the shutdownTlsGracefully
parameter. If this property is set to false
(the default), no explicit TLS shutdown happens. If it is true
, Spring LDAP tries to shut down the TLS channel gracefully before closing the target context.
When working with TLS connections, you need to make sure that the native LDAP Pooling functionality (as specified by using the native-pooling attribute) is turned off. This is particularly important if shutdownTlsGracefully is set to false . However, since the TLS channel negotiation process is quite expensive, you can gain great performance benefits by using the Spring LDAP Pooling Support, described in Pooling Support.
|
Custom Principal and Credentials Management
While the user name (that is, the user DN) and password used for creating an authenticated Context
are statically defined by default (the ones defined in the context-source
element configuration are used throughout the lifetime of the ContextSource
), there are several cases where this is not the desired behavior. A common scenario is that the principal and credentials of the current user should be used when performing LDAP operations for that user. You can modify the default behavior by supplying a reference to an AuthenticationSource
implementation to the context-source
element by using the authentication-source-ref
element, instead of explicitly specifying the username
and password
. The AuthenticationSource
is queried by the ContextSource
for principal and credentials each time an authenticated Context
is to be created.
If you use Spring Security, you can make sure the principal and credentials of the currently logged-in user are used at all times by configuring your ContextSource
with an instance of the SpringSecurityAuthenticationSource
shipped with Spring Security. The following example shows how to do so:
<beans>
...
<ldap:context-source
url="ldap://localhost:389"
authentication-source-ref="springSecurityAuthenticationSource"/>
<bean id="springSecurityAuthenticationSource"
class="org.springframework.security.ldap.authentication.SpringSecurityAuthenticationSource" />
...
</beans>
We do not specify any username or password for. our context-source when using an AuthenticationSource . These properties are needed only when the default behavior is used.
|
When using the SpringSecurityAuthenticationSource , you need to use Spring Security’s LdapAuthenticationProvider to authenticate the users against LDAP.
|
7.1.2. Native Java LDAP Pooling
The internal Java LDAP provider provides some very basic pooling capabilities. You can turn this LDAP connection pooling on or off by using the pooled
flag on AbstractContextSource
. The default value is false
(since release 1.3) — that is, the native Java LDAP pooling is turned off. The configuration of LDAP connection pooling is managed by using System
properties, so you need to handle this manually, outside of the Spring Context configuration. You can find details of the native pooling configuration here.
There are several serious deficiencies in the built-in LDAP connection pooling, which is why Spring LDAP provides a more sophisticated approach to LDAP connection pooling, described in Pooling Support. If you need pooling functionality, this is the recommended approach. |
Regardless of the pooling configuration, the ContextSource#getContext(String principal, String credentials) method always explicitly does not use native Java LDAP Pooling, in order for reset passwords to take effect as soon as possible.
|
7.2. LdapTemplate
Configuration
The LdapTemplate
is defined by using a <ldap:ldap-template>
element. The simplest possible ldap-template
declaration is the element by itself:
<ldap:ldap-template />
The element by itself creates an LdapTemplate
instance with the default ID, referencing the default ContextSource
, which is expected to have an ID of contextSource
(the default for the context-source
element).
The following table describes the configurable attributes on ldap-template
:
Attribute | Default | Description |
---|---|---|
|
|
The ID of the created bean. |
|
|
The ID of the |
|
|
The default count limit for searches. 0 means no limit. |
|
|
The default time limit for searches, in milliseconds. 0 means no limit. |
|
|
The default search scope for searches. The valid values are:
|
|
|
Specifies whether a |
|
|
Specifies whether |
|
The ID of the |
7.3. Obtaining a Reference to the Base LDAP Path
As described earlier, you can supply a base LDAP path to the ContextSource
, specifying the root in the LDAP tree to which all operations are relative. This means that you are working only with relative distinguished names throughout your system, which is typically rather handy. There are, however, some cases in which you may need to have access to the base path in order to be able to construct full DNs, relative to the actual root of the LDAP tree. One example would be when working with LDAP groups (for example, the groupOfNames
object class). In that case, each group member attribute value needs to be the full DN of the referenced member.
For that reason, Spring LDAP has a mechanism by which any Spring-controlled bean may be supplied with the base path on startup.
For beans to be notified of the base path, two things need to be in place. First, the bean that wants the base path reference needs to implement the BaseLdapNameAware
interface.
Second, you need to define a BaseLdapPathBeanPostProcessor
in the application context.
The following example shows how to implement BaseLdapNameAware
:
BaseLdapNameAware
package com.example.service;
public class PersonService implements PersonService, BaseLdapNameAware {
...
private LdapName basePath;
public void setBaseLdapPath(LdapName basePath) {
this.basePath = basePath;
}
...
private LdapName getFullPersonDn(Person person) {
return LdapNameBuilder.newInstance(basePath)
.add(person.getDn())
.build();
}
...
}
The following example shows how to define a BaseLdapPathBeanPostProcessor
:
<beans>
...
<ldap:context-source
username="cn=Administrator"
password="secret"
url="ldap://localhost:389"
base="dc=261consulting,dc=com" />
...
<bean class="org.springframework.ldap.core.support.BaseLdapPathBeanPostProcessor" />
</beans>
The default behavior of the BaseLdapPathBeanPostProcessor
is to use the base path of the single defined BaseLdapPathSource
(AbstractContextSource
) in the ApplicationContext
. If more than one BaseLdapPathSource
is defined, you need to specify which one to use by setting the baseLdapPathSourceName
property.
8. Spring LDAP Repositories
Spring LDAP has built-in support for Spring Data repositories. The basic functionality and configuration is described here. When working with Spring LDAP repositories, you should remember the following:
-
You can enable Spring LDAP repositories by using an
<ldap:repositories>
element in your XML configuration or by using an@EnableLdapRepositories
annotation on a configuration class. -
To include support for
LdapQuery
parameters in automatically generated repositories, have your interface extendLdapRepository
rather thanCrudRepository
. -
All Spring LDAP repositories must work with entities that are annotated with the ODM annotations, as described in Object-Directory Mapping (ODM).
-
Since all ODM managed classes must have a Distinguished Name as the ID, all Spring LDAP repositories must have the ID type parameter set to
javax.naming.Name
. The built-inLdapRepository
takes only one type parameter: the managed entity class, defaulting the ID tojavax.naming.Name
. -
Due to specifics of the LDAP protocol, paging and sorting are not supported for Spring LDAP repositories.
8.1. QueryDSL support
Basic QueryDSL support is included in Spring LDAP. This support includes the following:
-
An annotation processor, called
LdapAnnotationProcessor
, for generating QueryDSL classes based on Spring LDAP ODM annotations. See Object-Directory Mapping (ODM) for more information on the ODM annotations. -
A Query implementation, called
QueryDslLdapQuery
, for building and running QueryDSL queries in code. -
Spring Data repository support for QueryDSL predicates.
QueryDslPredicateExecutor
includes a number of additional methods with appropriate parameters. You can extend this interface along withLdapRepository
to include this support in your repository.
9. Pooling Support
Pooling LDAP connections helps mitigate the overhead of creating a new LDAP connection for each LDAP interaction. While Java LDAP pooling support exists, it is limited in its configuration options and features, such as connection validation and pool maintenance. Spring LDAP provides support for detailed pool configuration on a per-ContextSource
basis.
Pooling support is provided by supplying a <ldap:pooling />
child element to the <ldap:context-source />
element in the application context configuration. Read-only and read-write DirContext
objects are pooled separately (if anonymous-read-only
is specified). Jakarta Commons-Pool is used to provide the underlying pool implementation.
9.1. DirContext
Validation
Validation of pooled connections is the primary motivation for using a custom pooling library versus the JDK-provided LDAP pooling functionality. Validation allows pooled DirContext
connections to be checked to ensure that they are still properly connected and configured when checking them out of the pool, checking them into the pool, or while they are idle in the pool.
If connection validation is configured, pooled connections are validated by using DefaultDirContextValidator
.
DefaultDirContextValidator
does a DirContext.search(String, String, SearchControls)
, with an empty name, a filter of "objectclass=*"
, and SearchControls
set to limit a single result with the only the objectclass
attribute and a 500ms timeout. If the returned NamingEnumeration
has results, the DirContext
passes validation. If no results are returned or an exception is thrown, the DirContext
fails validation.
The default settings should work with no configuration changes on most LDAP servers and provide the fastest way to validate the DirContext
.
If you need customization, you can do so by using the validation configuration attributes, described in Pool Configuration.
Connections are automatically invalidated if they throw an exception that is considered non-transient. For example, if a DirContext instance throws a javax.naming.CommunicationException , it is interpreted as a non-transient error and the instance is automatically invalidated, without the overhead of an additional testOnReturn operation. The exceptions that are interpreted as non-transient are configured by using the nonTransientExceptions property of the PoolingContextSource .
|
9.2. Pool Configuration
The following attributes are available on the <ldap:pooling />
element for configuration of the DirContext pool:
Attribute | Default | Description |
---|---|---|
|
|
The maximum number of active connections of each type (read-only or read-write) that can be allocated from this pool at the same time. You can use a non-positive number for no limit. |
|
|
The overall maximum number of active connections (for all types) that can be allocated from this pool at the same time. You can use a non-positive number for no limit. |
|
|
The maximum number of active connections of each type (read-only or read-write) that can remain idle in the pool without extra connections being released. You can use a non-positive number for no limit. |
|
|
The minimum number of active connections of each type (read-only or read-write) that can remain idle in the pool without extra connections being created. You can use zero (the default) to create none. |
|
|
The maximum number of milliseconds that the pool waits (when no connections are available) for a connection to be returned before throwing an exception. You can use a non-positive number to wait indefinitely. |
|
|
Specifies the behavior when the pool is exhausted.
|
|
|
Whether objects are validated before being borrowed from the pool. If the object fails to validate, it is dropped from the pool, and an attempt to borrow another is made. |
|
|
Whether objects are validated before being returned to the pool. |
|
|
Whether objects are validated by the idle object evictor (if any). If an object fails to validate, it is dropped from the pool. |
|
|
The number of milliseconds to sleep between runs of the idle object evictor thread. When non-positive, no idle object evictor thread is run. |
|
|
The number of objects to examine during each run of the idle object evictor thread (if any). |
|
|
The minimum amount of time an object may sit idle in the pool before it is eligible for eviction by the idle object evictor (if any). |
|
|
The search base to be used when validating connections. Used only if |
|
|
The search filter to be used when validating connections. Used only if |
|
|
The ID of a |
|
|
Comma-separated list of |
9.3. Pool2 Configuration
The following attributes are available on the <ldap:pooling2 />
element for configuring the DirContext
pool:
Attribute | Default | Description |
---|---|---|
|
|
The overall maximum number of active connections (for all types) that can be allocated from this pool at the same time. You can use a non-positive number for no limit. |
|
|
The limit on the number of object instances allocated by the pool (checked out or idle), per key. When the limit is reached, the sub-pool is exhausted. A negative value indicates no limit. |
|
|
The maximum number of active connections of each type (read-only or read-write) that can remain idle in the pool, without extra connections being released. A negative value indicates no limit. |
|
|
The minimum number of active connections of each type (read-only or read-write) that can remain idle in the pool, without extra connections being created. You can use zero (the default) to create none. |
|
|
The maximum number of milliseconds that the pool waits (when there are no available connections) for a connection to be returned before throwing an exception. You can use a non-positive number to wait indefinitely. |
|
|
Whether to wait until a new object is available. If max-wait is positive, a |
|
|
Whether objects are validated before borrowing. If the object fails to validate, then borrowing fails. |
|
|
The indicator for whether objects are validated before being borrowed from the pool. If the object fails to validate, it is dropped from the pool, and an attempt to borrow another is made. |
|
|
The indicator for whether objects are validated before being returned to the pool. |
|
|
The indicator for whether objects are validated by the idle object evictor (if any). If an object fails to validate, it is dropped from the pool. |
|
|
The number of milliseconds to sleep between runs of the idle object evictor thread. When non-positive, no idle object evictor thread is run. |
|
|
The number of objects to examine during each run of the idle object evictor thread (if any). |
|
|
The minimum amount of time an object may sit idle in the pool before it is eligible for eviction by the idle object evictor (if any). |
|
|
The minimum amount of time an object may sit idle in the pool before it is eligible for eviction by the idle object evictor, with the extra condition that at least the minimum number of object instances per key remain in the pool. This setting is overridden by |
|
|
The eviction policy implementation that is used by this pool. The pool tries to load the class by using the thread context class loader. If that fails, the pool tries to load the class by using the class loader that loaded this class. |
|
|
The pool serves threads that are waiting to borrow connections fairly. |
|
|
JMX is enabled with the platform MBean server for the pool. |
|
|
The JMX name base that is used as part of the name assigned to JMX enabled pools. |
|
|
The JMX name prefix that is used as part of the name assigned to JMX enabled pools. |
|
|
The indicator for whether the pool has LIFO (last in, first out) behavior with respect to idle objects or as a FIFO (first in, first out) queue. LIFO always returns the most recently used object from the pool, while FIFO always returns the oldest object in the idle object pool |
|
|
The base DN to use for validation searches. |
|
|
The filter to use for validation queries. |
|
|
The ID of a |
|
|
Comma-separated list of |
9.4. Configuration
Configuring pooling requires adding an <ldap:pooling>
element nested in the <ldap:context-source>
element, as follows:
<beans>
...
<ldap:context-source
password="secret" url="ldap://localhost:389" username="cn=Manager">
<ldap:pooling />
</ldap:context-source>
...
</beans>
In a real-world situation, you would probably configure the pool options and enable connection validation. The preceding example demonstrates the general idea.
9.4.1. Validation Configuration
The following example tests each DirContext
before it is passed to the client application and tests DirContext
objects that have been sitting idle in the pool:
<beans>
...
<ldap:context-source
username="cn=Manager" password="secret" url="ldap://localhost:389" >
<ldap:pooling
test-on-borrow="true"
test-while-idle="true" />
</ldap:context-source>
...
</beans>
9.5. Known Issues
This section describes issues that sometimes arise when people use Spring LDAP. At present, it covers the following issues:
9.5.1. Custom Authentication
The PoolingContextSource
assumes that all DirContext
objects retrieved from ContextSource.getReadOnlyContext()
have the same environment and, likewise, that all DirContext
objects retrieved from ContextSource.getReadWriteContext()
have the same environment. This means that wrapping an LdapContextSource
configured with an AuthenticationSource
in a PoolingContextSource
does not function as expected. The pool would be populated by using the credentials of the first user, and, unless new connections were needed, subsequent context requests would not be filled for the user specified by the AuthenticationSource
for the requesting thread.
10. Adding Missing Overloaded API Methods
This section covers how to add your own overloaded API methods to implement new functionality.
10.1. Implementing Custom Search Methods
LdapTemplate
contains several overloaded versions of the most common operations in DirContext
. However, we have not provided an alternative for each and every method signature, mostly because there are so many of them. We have, however, provided a means to call whichever DirContext
method you want and still get the benefits that LdapTemplate
provides.
Suppose you want to call the following DirContext
method:
NamingEnumeration search(Name name, String filterExpr, Object[] filterArgs, SearchControls ctls)
There is no corresponding overloaded method in LdapTemplate
. The way to solve this is to use a custom SearchExecutor
implementation, as follows:
public interface SearchExecutor {
public NamingEnumeration executeSearch(DirContext ctx) throws NamingException;
}
In your custom executor, you have access to a DirContext
object, which you can use to call the method you want. You can then provide a handler that is responsible for mapping attributes and collecting the results. You can, for example, use one of the available implementations of CollectingNameClassPairCallbackHandler
, which collects the mapped results in an internal list. In order to actually perform the search, you need to call the search
method in LdapTemplate
that takes an executor and a handler as arguments. Finally, you need to return whatever your handler has collected. The following example shows how to do all of that:
SearchExecutor
and AttributesMapper
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
...
public List search(final Name base, final String filter, final String[] params,
final SearchControls ctls) {
SearchExecutor executor = new SearchExecutor() {
public NamingEnumeration executeSearch(DirContext ctx) {
return ctx.search(base, filter, params, ctls);
}
};
CollectingNameClassPairCallbackHandler handler =
new AttributesMapperCallbackHandler(new PersonAttributesMapper());
ldapTemplate.search(executor, handler);
return handler.getList();
}
}
If you prefer the ContextMapper
to the AttributesMapper
, the following example shows what it would look like:
SearchExecutor
and ContextMapper
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
...
public List search(final Name base, final String filter, final String[] params,
final SearchControls ctls) {
SearchExecutor executor = new SearchExecutor() {
public NamingEnumeration executeSearch(DirContext ctx) {
return ctx.search(base, filter, params, ctls);
}
};
CollectingNameClassPairCallbackHandler handler =
new ContextMapperCallbackHandler(new PersonContextMapper());
ldapTemplate.search(executor, handler);
return handler.getList();
}
}
When you use the ContextMapperCallbackHandler , you must make sure that you have called setReturningObjFlag(true) on your SearchControls instance.
|
10.2. Implementing Other Custom Context Methods
In the same manner as for custom search
methods, you can actually call any method in DirContext
by using a ContextExecutor
, as follows:
public interface ContextExecutor {
public Object executeWithContext(DirContext ctx) throws NamingException;
}
When implementing a custom ContextExecutor
, you can choose between using the executeReadOnly()
or the executeReadWrite()
method. Suppose you want to call the following method:
Object lookupLink(Name name)
The method is available in DirContext
, but there is no matching method in LdapTemplate
. It is a lookup method, so it should be read-only. We can implement it as follows:
DirContext
method using ContextExecutor
package com.example.repo;
public class PersonRepoImpl implements PersonRepo {
...
public Object lookupLink(final Name name) {
ContextExecutor executor = new ContextExecutor() {
public Object executeWithContext(DirContext ctx) {
return ctx.lookupLink(name);
}
};
return ldapTemplate.executeReadOnly(executor);
}
}
In the same manner, you can perform a read-write operation by using the executeReadWrite()
method.
11. Processing the DirContext
This section covers how to process the DirContext
, including pre- and post-processing.
11.1. Custom DirContext
Pre- and Post-processing
In some situations, you might like to perform operations on the DirContext
before and after the search operation. The interface that is used for this is called DirContextProcessor
. The following listing shows the DirContextProcessor
interface:
public interface DirContextProcessor {
public void preProcess(DirContext ctx) throws NamingException;
public void postProcess(DirContext ctx) throws NamingException;
}
The LdapTemplate
class has a search method that takes a DirContextProcessor
, as follows:
public void search(SearchExecutor se, NameClassPairCallbackHandler handler,
DirContextProcessor processor) throws DataAccessException;
Before the search operation, the preProcess
method is called on the given DirContextProcessor
instance. After the search has run and the resulting NamingEnumeration
has been processed, the postProcess
method is called. This lets you perform operations on the DirContext
to be used in the search and to check the DirContext
when the search has been performed. This can be very useful (for example, when handling request and response controls).
You can also use the following convenience methods when you do not need a custom SearchExecutor
:
public void search(Name base, String filter,
SearchControls controls, NameClassPairCallbackHandler handler, DirContextProcessor processor)
public void search(String base, String filter,
SearchControls controls, NameClassPairCallbackHandler handler, DirContextProcessor processor)
public void search(Name base, String filter,
SearchControls controls, AttributesMapper mapper, DirContextProcessor processor)
public void search(String base, String filter,
SearchControls controls, AttributesMapper mapper, DirContextProcessor processor)
public void search(Name base, String filter,
SearchControls controls, ContextMapper mapper, DirContextProcessor processor)
public void search(String base, String filter,
SearchControls controls, ContextMapper mapper, DirContextProcessor processor)
11.2. Implementing a Request Control DirContextProcessor
The LDAPv3 protocol uses “Controls” to send and receive additional data to affect the behavior of predefined operations. To simplify the implementation of a request control DirContextProcessor
, Spring LDAP provides the AbstractRequestControlDirContextProcessor
base class. This class handles the retrieval of the current request controls from the LdapContext
, calls a template method for creating a request control, and adds it to the LdapContext
. All you have to do in the subclass is to implement the template method called createRequestControl
and the postProcess
method for performing whatever you need to do after the search. The following listing shows the relevant signatures:
public abstract class AbstractRequestControlDirContextProcessor implements
DirContextProcessor {
public void preProcess(DirContext ctx) throws NamingException {
...
}
public abstract Control createRequestControl();
}
A typical DirContextProcessor
is similar to the following example:
DirContextProcessor
implementationpackage com.example.control;
public class MyCoolRequestControl extends AbstractRequestControlDirContextProcessor {
private static final boolean CRITICAL_CONTROL = true;
private MyCoolCookie cookie;
...
public MyCoolCookie getCookie() {
return cookie;
}
public Control createRequestControl() {
return new SomeCoolControl(cookie.getCookie(), CRITICAL_CONTROL);
}
public void postProcess(DirContext ctx) throws NamingException {
LdapContext ldapContext = (LdapContext) ctx;
Control[] responseControls = ldapContext.getResponseControls();
for (int i = 0; i < responseControls.length; i++) {
if (responseControls[i] instanceof SomeCoolResponseControl) {
SomeCoolResponseControl control = (SomeCoolResponseControl) responseControls[i];
this.cookie = new MyCoolCookie(control.getCookie());
}
}
}
}
Make sure you use LdapContextSource when you use controls. The Control interface is specific for LDAPv3 and requires that LdapContext is used instead of DirContext . If an AbstractRequestControlDirContextProcessor subclass is called with an argument that is not an LdapContext , it throws an IllegalArgumentException .
|
11.3. Paged Search Results
Some searches may return large numbers of results. When there is no easy way to filter out a smaller amount, it is convenient to have the server return only a certain number of results each time it is called. This is known as “paged search results”. Each “page” of the result could then be displayed, with links to the next and previous page. Without this functionality, the client must either manually limit the search result into pages or retrieve the whole result and then chop it into pages of suitable size. The former would be rather complicated, and the latter would consume unnecessary amounts of memory.
Some LDAP servers support PagedResultsControl
, which requests that the results of a search operation are returned by the LDAP server in pages of a specified size. The user controls the rate at which the pages are returned, by controlling the rate at which the searches are called. However, you must keep track of a cookie between the calls. The server uses this cookie to keep track of where it left off the previous time it was called with a paged results request.
Spring LDAP provides support for paged results by using the concept for pre- and post-processing of an LdapContext
, as discussed in the previous sections. It does so by using the PagedResultsDirContextProcessor
class. The PagedResultsDirContextProcessor
class creates a PagedResultsControl
with the requested page size and adds it to the LdapContext
. After the search, it gets the PagedResultsResponseControl
and retrieves the paged results cookie, which is needed to keep the context between consecutive paged results requests.
The following example shows how the to use the paged search results functionality:
PagedResultsDirContextProcessor
public List<String> getAllPersonNames() {
final SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
final PagedResultsDirContextProcessor processor =
new PagedResultsDirContextProcessor(PAGE_SIZE);
return SingleContextSource.doWithSingleContext(
contextSource, new LdapOperationsCallback<List<String>>() {
@Override
public List<String> doWithLdapOperations(LdapOperations operations) {
List<String> result = new LinkedList<String>();
do {
List<String> oneResult = operations.search(
"ou=People",
"(&(objectclass=person))",
searchControls,
CN_ATTRIBUTES_MAPPER,
processor);
result.addAll(oneResult);
} while(processor.hasMore());
return result;
}
});
}
For a paged results cookie to continue being valid, you must use the same underlying connection for each paged results call. You can do so by using the SingleContextSource , as demonstrated in the preceding example.
|
12. Transaction Support
Programmers used to working with relational databases coming to the LDAP world often express surprise at the fact that there is no notion of transactions. It is not specified in the protocol, and no LDAP servers support it. Recognizing that this may be a major problem, Spring LDAP provides support for client-side, compensating transactions on LDAP resources.
LDAP transaction support is provided by ContextSourceTransactionManager
, a PlatformTransactionManager
implementation that manages Spring transaction support for LDAP operations. Along with its collaborators, it keeps track of the LDAP operations performed in a transaction, making a record of the state before each operation and taking steps to restore the initial state should the transaction need to be rolled back.
In addition to the actual transaction management, Spring LDAP transaction support also makes sure that the same DirContext
instance is used throughout the same transaction. That is, the DirContext
is not actually closed until the transaction is finished, allowing for more efficient resources usage.
While the approach used by Spring LDAP to provide transaction support is sufficient for many cases, it is by no means “real” transactions in the traditional sense. The server is completely unaware of the transactions, so (for example), if the connection is broken, there is no way to roll back the transaction. While this should be carefully considered, it should also be noted that the alternative is to operate without any transaction support whatsoever. Spring LDAP’s transaction support is pretty much as good as it gets. |
The client-side transaction support adds some overhead in addition to the work required by the original operations.
While this overhead should not be something to worry about in most cases,
if your application does not perform several LDAP operations within the same transaction (for example, modifyAttributes followed by rebind ),
or if transaction synchronization with a JDBC data source is not required (see JDBC Transaction Integration), you gain little by using the LDAP transaction support.
|
12.1. Configuration
Configuring Spring LDAP transactions should look very familiar if you are used to configuring Spring transactions. You can annotate your transacted classes with @Transactional
, create a TransactionManager
instance, and include a <tx:annotation-driven>
element in your bean configuration. The following example shows how to do so:
<ldap:context-source
url="ldap://localhost:389"
base="dc=example,dc=com"
username="cn=Manager"
password="secret" />
<ldap:ldap-template id="ldapTemplate" />
<ldap:transaction-manager>
<!--
Note this default configuration will not work for more complex scenarios;
see below for more information on RenamingStrategies.
-->
<ldap:default-renaming-strategy />
</ldap:transaction-manager>
<!--
The MyDataAccessObject class is annotated with @Transactional.
-->
<bean id="myDataAccessObject" class="com.example.MyRepository">
<property name="ldapTemplate" ref="ldapTemplate" />
</bean>
<tx:annotation-driven />
...
While this setup works fine for most simple use cases, some more complex scenarios require additional configuration.
Specifically, if you need to create or delete subtrees within transactions, you need to use an alternative TempEntryRenamingStrategy , as described in Renaming Strategies.
|
In a real-world situation, you would probably apply the transactions on the service-object level rather than the repository level. The preceding example demonstrates the general idea.
12.2. JDBC Transaction Integration
A common use case when working against LDAP is that some of the data is stored in the LDAP tree but other data is stored in a relational database. In this case, transaction support becomes even more important, since the update of the different resources should be synchronized.
While actual XA transactions is not supported, support is provided to conceptually wrap JDBC and LDAP access within the same transaction by supplying a data-source-ref
attribute to the <ldap:transaction-manager>
element. This creates a ContextSourceAndDataSourceTransactionManager
, which then manages the two transactions virtually as if they were one. When performing a commit, the LDAP part of the operation is always performed first, letting both transactions be rolled back should the LDAP commit fail. The JDBC part of the transaction is managed exactly as in DataSourceTransactionManager
, except that nested transactions are not supported. The following example shows an ldap:transaction-manager
element with a data-source-ref
attribute:
<ldap:transaction-manager data-source-ref="dataSource" >
<ldap:default-renaming-strategy />
<ldap:transaction-manager />
The provided support is all client-side. The wrapped transaction is not an XA transaction. No two-phase commit is performed, as the LDAP server cannot vote on its outcome. |
You can accomplish the same thing for Hibernate integration by supplying a session-factory-ref
attribute to the <ldap:transaction-manager>
element, as follows:
<ldap:transaction-manager session-factory-ref="dataSource" >
<ldap:default-renaming-strategy />
<ldap:transaction-manager />
12.3. LDAP Compensating Transactions Explained
Spring LDAP manages compensating transactions by making a record of the state in the LDAP tree before each modifying operation (bind
, unbind
, rebind
, modifyAttributes
, and rename
).
This lets the system perform compensating operations should the transaction need to be rolled back.
In many cases, the compensating operation is pretty straightforward. For example, the compensating rollback operation for a bind
operation is to unbind the entry.
Other operations, however, require a different, more complicated approach because of some particular characteristics of LDAP databases.
Specifically, it is not always possible to get the values of all Attributes
of an entry, making the aforementioned strategy insufficient for (for example) an unbind
operation.
This is why each modifying operation performed within a Spring LDAP managed transaction is internally split up into four distinct operations: a recording operation, a preparation operation, a commit operation, and a rollback operation. The following table describes each LDAP operation:
LDAP Operation | Recording | Preparation | Commit | Rollback |
---|---|---|---|---|
|
Make a record of the DN of the entry to bind. |
Bind the entry. |
No operation. |
Unbind the entry by using the recorded DN. |
|
Make a record of the original and target DN. |
Rename the entry. |
No operation. |
Rename the entry back to its original DN. |
|
Make a record of the original DN and calculate a temporary DN. |
Rename the entry to the temporary location. |
Unbind the temporary entry. |
Rename the entry from the temporary location back to its original DN. |
|
Make a record of the original DN and the new |
Rename the entry to a temporary location. |
Bind the new |
Rename the entry from the temporary location back to its original DN. |
|
Make a record of the DN of the entry to modify and calculate compensating |
Perform the |
No operation. |
Perform a |
A more detailed description of the internal workings of the Spring LDAP transaction support is available in the Javadoc.
12.3.1. Renaming Strategies
As described in the table in the preceding section, the transaction management of some operations requires the original entry affected by the operation to be temporarily renamed before the actual modification can be made in the commit. The manner in which the temporary DN of the entry is calculated is managed by a TempEntryRenamingStrategy
that is specified in a child element of the <ldap:transaction-manager >
declaration in the configuration. Spring LDAP includes two implementations:
-
DefaultTempEntryRenamingStrategy
(the default): Specified by using an<ldap:default-renaming-strategy />
element. Adds a suffix to the least significant part of the entry DN. For example, for a DN ofcn=john doe, ou=users
, this strategy returns a temporary DN ofcn=john doe_temp, ou=users
. You can configure the suffix by setting thetemp-suffix
attribute. -
DifferentSubtreeTempEntryRenamingStrategy
: Specified by using an<ldap:different-subtree-renaming-strategy />
element. It appends a subtree DN to the least significant part of the DN. Doing so makes all temporary entries be placed at a specific location in the LDAP tree. The temporary subtree DN is configured by setting thesubtree-node
attribute. For example, ifsubtree-node
isou=tempEntries
and the original DN of the entry iscn=john doe, ou=users
, the temporary DN iscn=john doe, ou=tempEntries
. Note that the configured subtree node needs to be present in the LDAP tree.
The DefaultTempEntryRenamingStrategy does not work in some situations. For example, if you plan to do recursive deletes, you need to use DifferentSubtreeTempEntryRenamingStrategy . This is because the recursive delete operation actually consists of a depth-first delete of each node in the sub tree individually. Since you cannot rename an entry that has any children and DefaultTempEntryRenamingStrategy would leave each node in the same subtree (with a different name) instead of actually removing it, this operation would fail. When in doubt, use DifferentSubtreeTempEntryRenamingStrategy .
|
13. User Authentication using Spring LDAP
This section covers user authentication with Spring LDAP. It contains the following topics:
13.1. Basic Authentication
While the core functionality of the ContextSource
is to provide DirContext
instances for use by LdapTemplate
, you can also use it for authenticating users against an LDAP server. The getContext(principal, credentials)
method of ContextSource
does exactly that. It constructs a DirContext
instance according to the ContextSource
configuration and authenticates the context by using the supplied principal and credentials. A custom authenticate method could look like the following example:
public boolean authenticate(String userDn, String credentials) {
DirContext ctx = null;
try {
ctx = contextSource.getContext(userDn, credentials);
return true;
} catch (Exception e) {
// Context creation failed - authentication did not succeed
logger.error("Login failed", e);
return false;
} finally {
// It is imperative that the created DirContext instance is always closed
LdapUtils.closeContext(ctx);
}
}
The userDn
supplied to the authenticate
method needs to be the full DN of the user to authenticate (regardless of the base
setting on the ContextSource
). You typically need to perform an LDAP search based on (for example) the user name to get this DN. The following example shows how to do so:
private String getDnForUser(String uid) {
List<String> result = ldapTemplate.search(
query().where("uid").is(uid),
new AbstractContextMapper() {
protected String doMapFromContext(DirContextOperations ctx) {
return ctx.getNameInNamespace();
}
});
if(result.size() != 1) {
throw new RuntimeException("User not found or not unique");
}
return result.get(0);
}
There are some drawbacks to this approach. You are forced to concern yourself with the DN of the user, you can search only for the user’s uid, and the search always starts at the root of the tree (the empty path). A more flexible method would let you specify the search base, the search filter, and the credentials. Spring LDAP includes an authenticate method in LdapTemplate
that provides this functionality: boolean authenticate(LdapQuery query, String password);
.
When you use this method, authentication becomes as simple as follows:
ldapTemplate.authenticate(query().where("uid").is("john.doe"), "secret");
As described in the next section, some setups may require you to perform additional operations to get actual authentication to occur. See Performing Operations on the Authenticated Context for details. |
Do not write your own custom authenticate methods. Use the ones provided in Spring LDAP. |
13.2. Performing Operations on the Authenticated Context
Some authentication schemes and LDAP servers require some operation to be performed on the created DirContext
instance for the actual authentication to occur. You should test and make sure how your server setup and authentication schemes behave. Failure to do so might result in users being admitted into your system regardless of the supplied DN and credentials. The following example shows a naïve implementation of an authenticate method where a hard-coded lookup
operation is performed on the authenticated context:
public boolean myAuthenticate(String userDn, String credentials) {
DirContext ctx = null;
try {
ctx = contextSource.getContext(userDn, credentials);
// Take care here - if a base was specified on the ContextSource
// that needs to be removed from the user DN for the lookup to succeed.
ctx.lookup(userDn);
return true;
} catch (Exception e) {
// Context creation failed - authentication did not succeed
logger.error("Login failed", e);
return false;
} finally {
// It is imperative that the created DirContext instance is always closed
LdapUtils.closeContext(ctx);
}
}
It would be better if the operation could be provided as an implementation of a callback interface, rather than limiting the operation to always be a lookup
. Spring LDAP includes the AuthenticatedLdapEntryContextMapper
callback interface and a corresponding authenticate
method: <T> T authenticate(LdapQuery query, String password, AuthenticatedLdapEntryContextMapper<T> mapper);
This method lets any operation be performed on the authenticated context, as follows:
AuthenticatedLdapEntryContextMapper<DirContextOperations> mapper = new AuthenticatedLdapEntryContextMapper<DirContextOperations>() {
public DirContextOperations mapWithContext(DirContext ctx, LdapEntryIdentification ldapEntryIdentification) {
try {
return (DirContextOperations) ctx.lookup(ldapEntryIdentification.getRelativeName());
}
catch (NamingException e) {
throw new RuntimeException("Failed to lookup " + ldapEntryIdentification.getRelativeName(), e);
}
}
};
ldapTemplate.authenticate(query().where("uid").is("john.doe"), "secret", mapper);
13.3. Obsolete Authentication Methods
In addition to the authenticate
methods described in the preceding sections, you can use a number of deprecated methods for authentication. While these work fine, we recommend using the LdapQuery
methods instead.
13.4. Using Spring Security
While the approach described in the preceding sections may be sufficient for simple authentication scenarios, requirements in this area commonly expand rapidly. A multitude of aspects apply, including authentication, authorization, web integration, user context management, and others. If you suspect that the requirements might expand beyond just simple authentication, you should definitely consider using Spring Security for your security purposes instead. It is a full-featured, mature security framework that addresses the aforementioned aspects as well as several others.
14. LDIF Parsing
LDAP Directory Interchange Format (LDIF) files are the standard medium for describing directory data in a flat-file format. The most common uses of this format include information transfer and archival. However, the standard also defines a way to describe modifications to stored data in a flat-file format. LDIFs of this later type are typically referred to as changetype or modify LDIFs.
The org.springframework.ldap.ldif
package provides the classes needed to parse LDIF files and deserialize them into tangible objects. The LdifParser
is the main class of the org.springframework.ldap.ldif
package and is capable of parsing files that comply with RFC 2849. This class reads lines from a resource and assembles them into an LdapAttributes
object.
The LdifParser currently ignores changetype LDIF entries, as their usefulness in the context of an application has yet to be determined.
|
14.1. Object Representation
Two classes in the org.springframework.ldap.core
package provide the means to represent an LDIF in code:
-
LdapAttribute
: Extendsjavax.naming.directory.BasicAttribute
adding support for LDIF options as defined in RFC2849. -
LdapAttributes
: Extendsjavax.naming.directory.BasicAttributes
adding specialized support for DNs.
LdapAttribute
objects represent options as a Set<String>
. The DN support added to the LdapAttributes
object employs the javax.naming.ldap.LdapName
class.
14.2. The Parser
The Parser
interface provides the foundation for operation and employs three supporting policy definitions:
-
SeparatorPolicy
: Establishes the mechanism by which lines are assembled into attributes. -
AttributeValidationPolicy
: Ensures that attributes are correctly structured prior to parsing. -
Specification
: Provides a mechanism by which object structure can be validated after assembly.
The default implementations of these interfaces are as follows:
-
org.springframework.ldap.ldif.parser.LdifParser
-
org.springframework.ldap.ldif.support.SeparatorPolicy
-
org.springframework.ldap.ldif.support.DefaultAttributeValidationPolicy
-
org.springframework.ldap.schema.DefaultSchemaSpecification
Together, these four classes parse a resource line by line and translate the data into LdapAttributes
objects.
The SeparatorPolicy
determines how individual lines read from the source file should be interpreted, as the LDIF specification lets attributes span multiple lines. The default policy assesses lines in the context of the order in which they were read to determine the nature of the line in consideration. control attributes and changetype records are ignored.
The DefaultAttributeValidationPolicy
uses REGEX expressions to ensure that each attribute conforms to a valid attribute format (according to RFC 2849) once parsed. If an attribute fails validation, an InvalidAttributeFormatException
is logged, and the record is skipped (the parser returns null
).
14.3. Schema Validation
A mechanism for validating parsed objects against a schema is available through the Specification
interface in the org.springframework.ldap.schema
package. The DefaultSchemaSpecification
does not do any validation and is available for instances where records are known to be valid and need not be checked. This option saves the performance penalty that validation imposes. The BasicSchemaSpecification
applies basic checks, such as ensuring DN and object class declarations have been provided. Currently, validation against an actual schema requires implementation of the Specification
interface.
14.4. Spring Batch Integration
While the LdifParser
can be employed by any application that requires parsing of LDIF files, Spring offers a batch processing framework that offers many file-processing utilities for parsing delimited files such as CSV. The org.springframework.ldap.ldif.batch
package offers the classes needed to use the LdifParser
as a valid configuration option in the Spring Batch framework.
There are five classes in this package. Together, they offer three basic use cases:
-
Reading LDIF records from a file and returning an
LdapAttributes
object. -
Reading LDIF records from a file and mapping records to Java objects (POJOs).
-
Writing LDIF records to a file.
The first use case is accomplished with LdifReader
. This class extends Spring Batch’s AbstractItemCountingItemStreamItemReader
and implements its ResourceAwareItemReaderItemStream
. It fits naturally into the framework, and you can use it to read LdapAttributes
objects from a file.
You can use MappingLdifReader
to map LDIF objects directly to any POJO. This class requires you to provide an implementation of the RecordMapper
interface. This implementation should implement the logic for mapping objects to POJOs.
You can implement RecordCallbackHandler
and provide the implementation to either reader. You can use this handler to operate on skipped records. See the Spring Batch API documentation for more information.
The last member of this package, the LdifAggregator
, can be used to write LDIF records to a file. This class invokes the toString()
method of the LdapAttributes
object.
15. Utilities
This section describes additional utilities that you can use with Spring LDAP.
15.1. Incremental Retrieval of Multi-Valued Attributes
When there are a very large number of attribute values (>1500) for a specific attribute, Active Directory typically refuses to return all these values at once. Instead, the attribute values are returned according to the Incremental Retrieval of Multi-valued Properties method. Doing so requires the calling part to inspect the returned attribute for specific markers and, if necessary, make additional lookup requests until all values are found.
Spring LDAP’s org.springframework.ldap.core.support.DefaultIncrementalAttributesMapper
helps when working with this kind of attributes, as follows:
Object[] attrNames = new Object[]{"oneAttribute", "anotherAttribute"};
Attributes attrs = DefaultIncrementalAttributeMapper.lookupAttributes(ldapTemplate, theDn, attrNames);
The preceding example parses any returned attribute range markers and makes repeated requests as necessary until all values for all requested attributes have been retrieved.
16. Testing
This section covers testing with Spring LDAP. It contains the following topics:
16.1. Using an Embedded Server
spring-ldap-test is compatible with ApacheDS 1.5.5. Newer versions of ApacheDS are not supported.
|
To get started, you need to include the spring-ldap-test
dependency.
The following listing shows how to include the spring-ldap-test
for Maven:
<dependency>
<groupId>org.springframework.ldap</groupId>
<artifactId>spring-ldap-test</artifactId>
<version>{project-version}</version>
<scope>test</scope>
</dependency>
The following listing shows how to include the spring-ldap-test
for Gradle:
testCompile "org.springframework.ldap:spring-ldap-test:{project-version}"
16.2. ApacheDS
To use ApacheDS, you need to include a number of ApacheDS dependencies.
The following example shows how to include the ApacheDS dependencies for Maven:
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-core</artifactId>
<version>1.5.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-core-entry</artifactId>
<version>1.5.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-protocol-shared</artifactId>
<version>1.5.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-protocol-ldap</artifactId>
<version>1.5.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.server</groupId>
<artifactId>apacheds-server-jndi</artifactId>
<version>1.5.5</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.directory.shared</groupId>
<artifactId>shared-ldap</artifactId>
<version>0.9.15</version>
<scope>test</scope>
</dependency>
The following example shows how to include the ApacheDS dependencies for Gradle:
testCompile "org.apache.directory.server:apacheds-core:1.5.5",
"org.apache.directory.server:apacheds-core-entry:1.5.5",
"org.apache.directory.server:apacheds-protocol-shared:1.5.5",
"org.apache.directory.server:apacheds-protocol-ldap:1.5.5",
"org.apache.directory.server:apacheds-server-jndi:1.5.5",
"org.apache.directory.shared:shared-ldap:0.9.15"
The following bean definition creates an embedded LDAP server:
<bean id="embeddedLdapServer" class="org.springframework.ldap.test.EmbeddedLdapServerFactoryBean">
<property name="partitionName" value="example"/>
<property name="partitionSuffix" value="dc=261consulting,dc=com" />
<property name="port" value="9321" />
</bean>
spring-ldap-test
provides a mechanism to populate the LDAP server by using org.springframework.ldap.test.LdifPopulator
. To use it, create a bean similar to the following:
<bean class="org.springframework.ldap.test.LdifPopulator" depends-on="embeddedLdapServer">
<property name="contextSource" ref="contextSource" />
<property name="resource" value="classpath:/setup_data.ldif" />
<property name="base" value="dc=jayway,dc=se" />
<property name="clean" value="true" />
<property name="defaultBase" value="dc=jayway,dc=se" />
</bean>
Another way to work against an embedded LDAP server is by using org.springframework.ldap.test.TestContextSourceFactoryBean
, as follows:
<bean id="contextSource" class="org.springframework.ldap.test.TestContextSourceFactoryBean">
<property name="defaultPartitionSuffix" value="dc=jayway,dc=se" />
<property name="defaultPartitionName" value="jayway" />
<property name="principal" value="uid=admin,ou=system" />
<property name="password" value="secret" />
<property name="ldifFile" value="classpath:/setup_data.ldif" />
<property name="port" value="1888" />
</bean>
Also, org.springframework.ldap.test.LdapTestUtils
provides methods to programmatically work with an embedded LDAP server.
16.3. UnboundID
To use UnboundID, you need to include an UnboundID dependency.
The following example shows how to include the UnboundID dependency for Maven:
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.1</version>
<scope>test</scope>
</dependency>
The following example shows how to include the UnboundID dependency for Gradle:
testCompile "com.unboundid:unboundid-ldapsdk:3.1.1"
The following bean definition creates an embedded LDAP server:
<bean id="embeddedLdapServer" class="org.springframework.ldap.test.unboundid.EmbeddedLdapServerFactoryBean">
<property name="partitionName" value="example"/>
<property name="partitionSuffix" value="dc=261consulting,dc=com" />
<property name="port" value="9321" />
</bean>
spring-ldap-test
provides a way to populate the LDAP server by using org.springframework.ldap.test.unboundid.LdifPopulator
. To use it, create a bean similar to the following:
<bean class="org.springframework.ldap.test.unboundid.LdifPopulator" depends-on="embeddedLdapServer">
<property name="contextSource" ref="contextSource" />
<property name="resource" value="classpath:/setup_data.ldif" />
<property name="base" value="dc=jayway,dc=se" />
<property name="clean" value="true" />
<property name="defaultBase" value="dc=jayway,dc=se" />
</bean>
Another way to work against an embedded LDAP server is by using org.springframework.ldap.test.unboundid.TestContextSourceFactoryBean
.
To use it, create a bean similar to the following:
<bean id="contextSource" class="org.springframework.ldap.test.unboundid.TestContextSourceFactoryBean">
<property name="defaultPartitionSuffix" value="dc=jayway,dc=se" />
<property name="defaultPartitionName" value="jayway" />
<property name="principal" value="uid=admin,ou=system" />
<property name="password" value="secret" />
<property name="ldifFile" value="classpath:/setup_data.ldif" />
<property name="port" value="1888" />
</bean>
Also, org.springframework.ldap.test.unboundid.LdapTestUtils
provide methods to programmatically work with an embedded LDAP server.