Por diversos momentos na vida de arquitetos/desenvolvedores de software recebemos o bendito requisito vindo do usuário de que o seu sistema web precisa desesperadamente conversar com o Microsoft Active Directory. Desde que me conheço no desenvolvimento vejo isso acontecendo e vejo diversos desenvolvedores sentindo um frio na espinha neste momento.
Creio que tenha sido com o intuíto de ajudar estes pobres é que os geniais lá do Spring resolveram quebrar mais uma. É o Spring LDAP.
Não vou colocar o projeto inteiro aqui, o foco deste post não é ensinar a criar um projeto jsf + spring, mas vou tentar detalhar legal a parte do ldap que tanto nos aflige.
Inicialmente vou colocar o meu pom.xml com todas as dependências, creio que este arquivo seja o mais importante e ao mesmo tempo o mais problematico.
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.app</groupId>
<artifactId>App</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<properties>
<project.build.sourceEncoding>ISO-8859-1</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.faces</groupId>
<artifactId>jsf-api</artifactId>
<version>1.2_08</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.faces</groupId>
<artifactId>jsf-impl</artifactId>
<version>1.2_08</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.14</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.8.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sun.facelets</groupId>
<artifactId>jsf-facelets</artifactId>
<version>1.1.15</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.richfaces.ui</groupId>
<artifactId>richfaces-ui</artifactId>
<version>3.3.1.GA</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>3.0.5.RELEASE</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>2.3.2</version>
<type>jar</type>
<scope>compile</scope>
<exclusions>
<exclusion>
<artifactId>slf4j-api</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>2.1_3</version>
</dependency>
<dependency>
<groupId>org.springframework.webflow</groupId>
<artifactId>spring-faces</artifactId>
<version>2.3.0.RELEASE</version>
<type>jar</type>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>3.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4</version>
<scope>provided</scope>
<exclusions>
<exclusion>
<artifactId>activation</artifactId>
<groupId>javax.activation</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-annotations</artifactId>
<version>3.4.0.GA</version>
<type>jar</type>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.snowdrop</groupId>
<artifactId>snowdrop-vfs</artifactId>
<version>1.0.1.GA</version>
</dependency>
<dependency>
<groupId>com.sun.xml.messaging.saaj</groupId>
<artifactId>saaj-impl</artifactId>
<version>1.3</version>
<exclusions>
<exclusion>
<artifactId>activation</artifactId>
<groupId>javax.activation</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.ws</groupId>
<artifactId>spring-ws</artifactId>
<version>1.5.8</version>
<classifier>all</classifier>
</dependency>
<dependency>
<groupId>org.easymock</groupId>
<artifactId>easymock</artifactId>
<version>3.0</version>
<type>jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>3.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>3.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>3.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>3.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-rmi</artifactId>
<version>2.0.4.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
<version>3.0.5.RELEASE</version>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz</artifactId>
<version>2.0.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.quartz-scheduler</groupId>
<artifactId>quartz-jboss</artifactId>
<version>2.0.0</version>
<scope>provided</scope>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}-${project.version}</finalName>
<directory>${basedir}/target</directory>
<outputDirectory>${basedir}/target/classes</outputDirectory>
<testOutputDirectory>${basedir}/target/test-classes</testOutputDirectory>
<sourceDirectory>${basedir}/src/main/java</sourceDirectory>
<scriptSourceDirectory>${basedir}/src/main/scripts</scriptSourceDirectory>
<testSourceDirectory>${basedir}/src/test/java</testSourceDirectory>
<resources>
<resource>
<directory>${basedir}/src/main/resources</directory>
</resource>
</resources>
<testResources>
<testResource>
<directory>${basedir}/src/test/resources</directory>
</testResource>
</testResources>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.3.2</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
<executable>${env.JAVA_HOME}/bin/javac</executable>
<showDeprecation>true</showDeprecation>
<showWarnings>true</showWarnings>
<optimize>true</optimize>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>2.6</version>
</plugin>
</plugins>
</build>
</project>
Por cima você já consegue ver que é um projeto JSF 1.2, Spring e um pouco de Hibernate, como eu disse, o que não se aplica a sua aplicação deve ser simplesmente ignorado. Tomei a decisão de colocar todo o pom aqui, pois é muito comum ficarmos presos no famoso dependency-hell do Java, mas a única dependência que de fato faz a diferença neste post é spring-security-ldap.
A primeira coisa é criar o bean de ldapTemplate dentro do seu contexto do Spring, é através deste bean que você se comunicará com o AD e ele é a principal figura deste projeto.
Vamos lá:
<bean id="properties">
<property name="locations">
<list>
<value>/WEB-INF/classes/com/app/app/resources/ldap.properties</value>
</list>
</property>
</bean>
<bean id="contextSource">
<property name="url" value="${ldap.url}" />
<property name="userDn" value="${ldap.userName}" />
<property name="password" value="${ldap.password}" />
<property name="pooled" value="${ldap.pooled}"/>
<property name="referral" value="${ldap.referral}"/>
</bean>
<bean id="ldapTemplate">
<constructor-arg ref="contextSource" />
<property name="ignorePartialResultException" value="${ldap.ignore}"/>
</bean>
Nas linhas de código acima criarmos três beans bem simplezinhos. O primeiro properties, apenas carregamos o arquivo especificado (o seu arquivo pode estar em qualquer lugar, ou mesmo não existir) para que possamos parametrizar os valores do servidor ldap de fora do arquivo xml do Spring.
Olha o meu arquivo properties aí:
ldap.url=ldap://dominio.com.br/DC=dominio,DC=com,DC=br
ldap.userName=leonardo.moreira@dominio.com.br
ldap.password=senha
ldap.userPrincipalPath=(userPrincipalName={0})
ldap.pooled=true
ldap.referral=follow
ldap.ignore=true
Vou frisar que o endereço da propriedade ldap.url varia de acordo com suas configurações de ldap. Veja, em geral é sempre o nome do seu domínio seguido pelos sufixos definidos pela galera da sua Infra-estrutura.
Uma dica para ver alguns destes dados é aquela busca avançada de usuários do windows, sabe? Você pode adicionar diversos campos e valores que constituem o domínio e outras informações úteis.
Uma vez com o properties em mãos o aplicamos ao conector do ldap, no caso o bean contextSource. Este bean irá criar a comunicação com o ldap e lhe passar o contexto do mesmo para que você trabalhe livremente utilizando o ldapTemplate. Falando em ldapTemplate o criamos no bean seguinte passando o contextSource no construtor.
Bom, uma vez que nossa aplicação esteja subindo no web-server com os beans descritos acima vem a parte mais fácil que é lidar com o contexto do Ldap.
Aqui na minha aplicação eu criei um serviço simples que apenas faz a busca dos usuários no AD com o critério LIKE (Do SQL lembra?
)
Bom, a primeira coisa de tudo aqui é definir as nossas interfaces, afinal, desacoplamento nunca é ruim, correto?
A interface User descreve os campos que a nossa classe concreta de usuário deverá ter. Esta interface é bem pessoal, tem gente que gosta de criar e tem gente que não vê a necessidade. Em todo caso está aí.
package com.app.app.ldap;
/**
* @author Leonardo Machado Moreira
* @version 1.0 04/11/2011
*/
public interface User {
public String getUserName();
public void setUserName(String userName);
public String getFirstName();
public void setFirstName(String firstName);
public String getLastName();
public void setLastName(String lastName);
public String getEmail();
public void setEmail(String email);
public String getPassword();
public void setPassword(String password);
public String getDepartment();
public void setDepartment(String departement);
public String getLogin();
public void setLogin(String login);
public String [] getGroups();
public void setGroups(String [] groups);
}
A interface User será implementada pela classe DefaultUser, que terá o papel de mapear de fato um usuário do ldap.
/*
* @(#)DefaultUser.java 1.0 04/11/2011
*
* Copyright (c) 2011, GSW Software Ltda. Todos os direitos reservados.
* Propriedade particular/confidencial da GSW. Uso sujeito a termos de licença.
*/
package com.app.app.ldap;
/**
* @author Leonardo Machado Moreira
* @version 1.0 04/11/2011
*/
public class DefaultUser implements User {
private String userName;
private String firstName;
private String lastName;
private String email;
private String password;
private String department;
private String login;
private String groups[];
public String getUserName() {
return userName;
}
public void setUserName(String userName) {
this.userName = userName;
}
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getDepartment() {
return department;
}
public void setDepartment(String department) {
this.department = department;
}
public String [] getGroups() {
return groups;
}
public void setGroups(String [] groups) {
this.groups = groups;
}
@Override
public String getLogin() {
return login;
}
@Override
public void setLogin(String login) {
this.login = login;
}
public String toString() {
StringBuffer buffer = new StringBuffer();
buffer.append("UserImpl[");
buffer.append(" userName = ").append(userName);
buffer.append(" email = ").append(email);
buffer.append(" firstName = ").append(firstName);
buffer.append(" lastName = ").append(lastName);
buffer.append(" password = ").append(password);
buffer.append("]");
return buffer.toString();
}
}
Agora mapearemos o serviço em sí, e lá vai mais uma interface.
Segue:
package com.app.app.ldap;
import java.util.List;
/**
* @author Leonardo Machado Moreira
* @version 1.0 04/11/2011
*/
public interface UserService {
User getUser(final String userName);
List<User> getUsers(final String pattern);
}
Dois métodos simples de tudo, um retorna um usuário específico através da busca pelo nome e o outro retornará uma lista de usuários a partir de uma String qualquer, onde será feito o like.
package com.app.app.ldap;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.filter.LikeFilter;
import org.springframework.ldap.filter.OrFilter;
import org.springframework.stereotype.Service;
/**
* @author Leonardo Machado Moreira
* @version 1.0 04/11/2011
*/
@Service
public class DefaultUserService implements UserService {
@Autowired
private LdapTemplate ldapTemplate;
public User getUser(final String userName) {
OrFilter filter = new OrFilter();
filter.or(new EqualsFilter("givenName", userName));
@SuppressWarnings("unchecked")
List<User> users = getLdapTemplate().search(DistinguishedName.EMPTY_PATH, filter.encode(), new UserAttributesMapper());
if (!users.isEmpty()) {
return users.get(0);
}
return null;
}
@SuppressWarnings("unchecked")
public List<User> getUsers(final String pattern) {
OrFilter filter = new OrFilter();
if (pattern != null) {
filter.or(new LikeFilter("givenName", pattern));
}
List<User> users = new ArrayList<User>();
users.addAll(getLdapTemplate().search(DistinguishedName.EMPTY_PATH, filter.encode(), new UserAttributesMapper()));
return users;
}
public LdapTemplate getLdapTemplate() {
return ldapTemplate;
}
}
É na classe acima que toda a mágica acontece. Ela é que será chamada nos outros locais da sua aplicação (controladores) e ela que encapsulará toda a complexidade da comunicação com o AD.
A classe é tão simples e tão descritiva que tenho até preguiça de explicar.
Fiz o Autowired do ldapTemplate que nós criamos lá no XML um pouco para trás e então é só utiliza-lo a-la-vonte.
A linha mais importante desta classe é obviamente a linha a seguir.
List<User> users = getLdapTemplate().search(DistinguishedName.EMPTY_PATH, filter.encode(), new UserAttributesMapper());
Aqui listamos o filtro e a classe de mapeamento onde a string retornada pelo ldap será convertida na nossa entidade.
Vejam:
public class UserAttributesMapper implements AttributesMapper {
@Override
public Object mapFromAttributes(Attributes attrs) throws NamingException {
DefaultUser user = new DefaultUser();
if (attrs.get("cn") != null) {
user.setUserName((String) attrs.get("cn").get());
}
if (attrs.get("cn") != null)
user.setFirstName((String) attrs.get("cn").get());
if (attrs.get("sn") != null)
user.setLastName((String) attrs.get("sn").get());
if (attrs.get("mail") != null)
user.setEmail((String) attrs.get("mail").get());
if (attrs.get("sAMAccountName") != null)
user.setLogin((String) attrs.get("sAMAccountName").get());
return user;
}
}
É importante ressaltar que não fui eu que inventei estas constantes (sn, cn e etc), também as abomino, mas é a forma que a Microsoft gerencia os dados de seus usuáriso no AD.
Com uma googlada simples eu encontrei o descritivo de todos os atributos, tá no link:
http://www.computerperformance.co.uk/Logon/LDAP_attributes_active_directory.htm
Bom, creio que agora temos 99% do trabalho pronto, vou só mostrar como ficou a chamada lá no controller do meu jsf, onde eu populo um autocomplete o Richfaces, conforme o usuário vai digitando eu vou listando todos os usuários do AD a partir do nome.
@Autowired
private UserService defaultUserService;
public List<com.embraer.icm.ldap.User> autocomplete(Object suggest) {
try {
List<com.embraer.icm.ldap.User> users = new ArrayList<com.embraer.icm.ldap.User>();
users.addAll(defaultUserService.getUsers(suggest + "*"));
return users;
} catch (Exception e) {
return new ArrayList<com.embraer.icm.ldap.User>();
}
}
<h:inputText id="name" value="#{userController.entity.name}"
required="true"
requiredMessage="#{labels['user.labels.required.name']}"
disabled="#{userController.rendered}" style="width:300px;" />
<rich:suggestionbox for="name" var="result"
suggestionAction="#{userController.autocomplete}">
<h:column>
<h:outputText value="#{result.userName}" />
</h:column>
<a4j:support event="onselect" reRender="login, email">
<f:setPropertyActionListener value="#{result.email}"
target="#{userController.entity.email}" />
<f:setPropertyActionListener value="#{result.login}"
target="#{userController.entity.login}" />
</a4j:support>
</rich:suggestionbox>
Pronto, agora sim, tudo que precisamos para nos comunicar com o LDAP e ainda mostrar em um auto complete do Richfaces. É óbvio que não coloquei as configurações básicas de framework, afinal, perderia toda a graça de programar, não é?
Mas quem realmente tiver dificuldade em uma configuração simples de JSF + Spring e etc, pode comentar ai que eu dou uma força, mas dê uma boa googlada antes, tá?
Ah, outra coisa que esqueci de comentar, o ldapTemplate não faz apenas pesquisa, ele chega ao cúmulo de modificar usuários e até cria-los dentro do AD, é só questão de brincar melhor com a sua API e com os seus atributos.
É isso aí, até a próxima.
Leonardo
I’ve been dismayed to discover just how many software developers aren’t really completely up to speed on the mysterious world of character sets, encodings, Unicode, all that stuff. A couple of years ago, a beta tester for
Back in the semi-olden days, when Unix was being invented and K&R were writing
Because bytes have room for up to eight bits, lots of people got to thinking, “gosh, we can use the codes 128-255 for our own purposes.” The trouble was, lots of people had this idea at the same time, and they had their own ideas of what should go where in the space from 128 to 255. The IBM-PC had something that came to be known as the OEM character set which provided some accented characters for European languages and 

