简单记录个人项目中基于Java使用LDAP的相关经验,供后续参考。

Java实现

JDK自带类库中的javax.naming包默认提供了LDAP的原生支持,其使用方式类似如下

public boolean ldapAuth(String username, String password) throws NamingException {
    boolean loginResult = false;
    
    Hashtable<String, String> adminEnv = new Hashtable<>();
    adminEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
    adminEnv.put(Context.PROVIDER_URL, "ldap://ldap.lucumt.com");
    adminEnv.put(Context.SECURITY_AUTHENTICATION, "simple");
    adminEnv.put(Context.SECURITY_PRINCIPAL, "cn=admin,ou=adminUsers,ou=sso,dc=lucumt,dc=com");
    adminEnv.put(Context.SECURITY_CREDENTIALS, "qWertFx@");

    DirContext adminCtx = new InitialDirContext(adminEnv);

    String searchBase = "ou=sso,dc=lucumt,dc=com";
    String searchFilter = "cn=" + username + "";
    SearchControls controls = new SearchControls();
    controls.setSearchScope(SearchControls.SUBTREE_SCOPE);

    NamingEnumeration<SearchResult> results =
            adminCtx.search(searchBase, searchFilter, controls);

    if (results.hasMore()) {
        SearchResult result = results.next();
        String foundUserDn = result.getNameInNamespace();

        // 3. 使用搜索到的用户 DN 进行认证
        Hashtable<String, String> userEnv = new Hashtable<>();
        userEnv.putAll(adminEnv);
        userEnv.put(Context.SECURITY_PRINCIPAL, foundUserDn);
        userEnv.put(Context.SECURITY_CREDENTIALS, password);

        DirContext userCtx = new InitialDirContext(userEnv);
        System.out.println("认证成功!用户 DN:" + foundUserDn);
        userCtx.close();
        loginResult = true;
    } else {
        System.err.println("用户不存在!");
    }

    adminCtx.close();
    return loginResult;
}
public static void testLDAP() {
    // LDAP 服务器地址
    String ldapUrl = "ldap://ldap.lucumt.com";
    // LDAP 管理员的用户名
    String ldapUser = "cn=admin,ou=adminUsers,ou=sso,dc=lucumt,dc=com";
    // LDAP 管理员的密码
    String ldapPassword = "qWertFx@";
    // 搜索的基础 DN
    String baseDN = "ou=sso,dc=lucumt,dc=com";
    // 搜索的过滤器
    String searchFilter = "(objectClass=person)";

    // 配置 LDAP 环境
    Hashtable < String, String > env = new Hashtable < > ();
    env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
    env.put(Context.PROVIDER_URL, ldapUrl);
    env.put(Context.SECURITY_AUTHENTICATION, "simple");
    env.put(Context.SECURITY_PRINCIPAL, ldapUser);
    env.put(Context.SECURITY_CREDENTIALS, ldapPassword);

    try {
        // 创建 LDAP 上下文
        DirContext ctx = new InitialDirContext(env);
        // 设置搜索控件
        SearchControls searchControls = new SearchControls();
        searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
        searchControls.setReturningAttributes(new String[] {
            "cn",
            "sn",
            "mail"
        }); // 要返回的属性

        // 执行搜索
        NamingEnumeration < SearchResult > results = ctx.search(baseDN, searchFilter, searchControls);

        while (results.hasMore()) {
            SearchResult result = results.next();
            Attributes attributes = result.getAttributes();

            Attribute cn = attributes.get("cn");
            Attribute sn = attributes.get("sn");
            Attribute mail = attributes.get("mail");

            System.out.println("CN: " + (cn != null ? cn.get() : "N/A"));
            System.out.println("SN: " + (sn != null ? sn.get() : "N/A"));
            System.out.println("Mail: " + (mail != null ? mail.get() : "N/A"));
            System.out.println("----------------------------");
        }

        // 关闭上下文
        ctx.close();

    } catch (NamingException e) {
        e.printStackTrace();
    }
}

Spring实现

JDK自带的LDAP 操作起来较为复杂,可利用Spring提供的LdapTemplate简化上述操作。

仿照下述代码引入相关依赖与配置

<dependency>
    <groupId>org.springframework.ldap</groupId>
    <artifactId>spring-ldap-core</artifactId>
</dependency>
spring:
  ldap:
    urls: ldap://ldap.lucumt.com
    base: ou=sso,dc=lucumt,dc=com
    username: cn=admin,ou=adminUsers,ou=sso,dc=lucumt,dc=com
    password: qWertFx@
ldap:
   mail: email
   userName: cn
   realName: displayName
   phone: mobile

之后的代码如下所示,可看出同原始的Java 实现相比较,其篇幅已经缩减不少(高亮部分为核心代码)

import com.lucumt.app.config.LdapConfig;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.stereotype.Service;

@Slf4j
@Service
public class AuthServiceImpl implements IAuthService {

    @Resource
    private LdapTemplate ldapTemplate;

    @Resource
    private LdapConfig ldapConfig;

    @Override
    public ResponseVM ldapLogin(UserVO loginUser) {
        String filter = ldapConfig.getUid() + "=" + loginUser.getUserName();
        boolean success = ldapTemplate.authenticate("", filter, loginUser.getPassword());
        if (!success) {
            return ResponseVM.fail("用户名或密码错误");
        }
        return ResponseVM.success("用户登录成功");
    }
    
    @Override
    public User fetchUserInfo(String username) {
        String filter = ldapConfig.getUid() + "=" + username;
        return ldapTemplate.searchForObject("", filter, context -> {
            DirContextAdapter ctx = (DirContextAdapter) context;
            User u = new User();
            u.setUserName(ctx.getStringAttribute(ldapConfig.getUsername()));
            u.setRealName(this.convertAttributeValue(ctx.getStringAttribute(ldapConfig.getRealName())));
            u.setEmail(this.convertAttributeValue(ctx.getStringAttribute(ldapConfig.getMail())));
            u.setPhone(this.convertAttributeValue(ctx.getStringAttribute(ldapConfig.getPhone())));
            return u;
        });
    }

}
import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Getter
@Setter
@Component
@ConfigurationProperties(prefix = "ldap")
public class LdapConfig {

    private String uid;
    private String userName;
    private String realName;
    private String mail;
    private String phone;
}

动态实现

对于某些标准化产品而言,需要交付到客户使用环境后进行测试与配置,此时需要提供LDAP动态配置的功能

LDAP动态配置

可利用类似如下代码基于不同的配置动态的创建LdapTemplate,核心为通过new关键字重新创建相关对象

private LdapTemplate createLdapTemplate(LdapConfigVO config) {
    LdapContextSource source = new LdapContextSource();
    source.setUrl(config.getUrls());
    source.setBase(config.getBase());

    //非匿名验证时需要设置管理员账号信息
    if (!config.isAnonymous()) {
        source.setUserDn(config.getUsername());
        source.setPassword(config.getPassword());
    }

    //确保上述配置生效
    source.afterPropertiesSet();

    LdapTemplate template = new LdapTemplate(source);
    return template;
}

LDAPS支持

部分场景需要使用基于SSL/TSL实现的LDAPS,要支持此功能只需做如下2处修改即可

1.将LDAP的地址修改为加密形式,类似ldaps://ldap.lucumt.com

2.利用下述代码导入LDAPS的证书

# Windows导入与验证
keytool -import -alias ldap-ca -file ldap-lucumt.crt -keystore "%JAVA_HOME%\jre\lib\security\cacerts" -storepass Pass@Word
keytool -list -keystore "%JAVA_HOME%\lib\security\cacerts" -storepass Pass@Word -alias ldap-ca

# Linux导入与验证
keytool -import -alias ldap-ca -file ldap-lucumt.crt -keystore "${JAVA_HOME}/jre/lib/security/cacerts" -storepass Pass@Word
keytool -list -keystore "${JAVA_HOME}/lib/security/cacerts" -storepass Pass@Word -alias ldap-ca

导入过程中的正常输出类似如下

LDAP证书导入结果

证书查看结果类似如下

LDAP证书查看结果