测试环境:
SpringBoot: 2.4.0
Window Server: 2012R2
Ldap browser: Softerra LDAP Administrator 2021.1 (32-bit)
附带 Spring LDAP 文档 官网文档

1. 新建一个 SpringBoot 项目

添加 LDAP 依赖包

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-ldap</artifactId>
</dependency>

2. 配置 LDAP 连接

application.yml中配置
注意下面对应的 dc 等信息均以 a,b 字符代替,使用时替换成项目的地址即可

2.1 http 389 端口

spring:
  ldap:
    urls: ldap://a.b.com:389
    username: c@b.com
    password: xxxxxxxxx
    # 注意:这里的 base 会覆盖 Persion 中的 base
    base: dc=b,dc=com

2.2 https ssl 636 端口

spring:
  ldap:
    urls: ldaps://a.b.com:636
    username: c@b.com
    password: xxxxxxxxx
    base: dc=b,dc=com

3. 添加数据接口

3.1 创建 AD 用户实例类

@Data
@Entry(base = "dc=b,dc=com", objectClasses = "user")
public class Person {

    @Id
    private Name id;
    @Attribute(name = "cn")
    private String cn;
    @Attribute(name = "sn")
    private String sn;
    @Attribute(name="mail")
    private String mail;
    @Attribute(name="telephoneNumber")
    private String telephoneNumber;
    @Attribute(name="description")
    private String description;
    @Attribute(name="givenName")
    private String givenName;

}

3.2 创建一个 PersonReposity 数据接口

public interface PersonRepository extends LdapRepository<Person> {}

3.3 测试接口

下面测试获取用户、验证接口

  1. 获取所有用户信息
personRepository.findAll().forEach(System.out::pringln)

打印结果(这里屏蔽了部分敏感信息就不展示了)

Person(id=CN=Administrator,CN=Users, cn=Administrator, sn=null, mail=null, telephoneNumber=null, description=***, givenName=null)
Person(id=CN=Guest,CN=Users, cn=Guest, sn=null, mail=null, telephoneNumber=null, description=****, givenName=null)
Person(id=CN=***,CN=Users, cn=Alan, sn=null, mail=null, telephoneNumber=null, description=null, givenName=null)
Person(id=CN=***,OU=Domain Controllers, cn=***, sn=null, mail=null, telephoneNumber=null, description=null, givenName=null)
Person(id=CN=***,CN=Users, cn=krbtgt, sn=null, mail=null, telephoneNumber=null, description=***, givenName=null)
Person(id=CN=***,CN=Users, cn=huangalan, sn=huang, mail=null, telephoneNumber=null, description=null, givenName=alan)
Person(id=CN=a,CN=Users, cn=a, sn=a, mail=test@123.com, telephoneNumber=123123123123123, description=teststsetsetsetsetsetset, givenName=a-name)
  1. 获取单个用户信息
    以上面的 cn=a用户为例
personRepository.findOne(query().where("cn").is("a"), Person.class);

打印结果:

Person(id=CN=a,CN=Users, cn=a, sn=a, mail=test@123.com, telephoneNumber=123123123123123, description=teststsetsetsetsetsetset, givenName=a-name)
  1. 验证用户账户
    这里以 cn=a为例,传入用户名和密码测试
String password = "123456";
String username = "a";
personRepository.authenticate(query().where("cn").is(username ), password ,
                (dirContext, ldapEntryIdentification) ->
                        ldapTemplate.findOne(query().where(attribute).is(username), Person.class));

打印结果:

Person(id=CN=a,CN=Users, cn=a, sn=a, mail=test@123.com, telephoneNumber=123123123123123, description=teststsetsetsetsetsetset, givenName=a-name)

LdapRepository 已经封装了很多操作用户接口,这里就不一一测试了,有兴趣的童鞋可以自己测试,诸如:

delete()
deleteAll()
findById()
existById()
....
直接调用即可

4. 整合封装

将上面接口整合成一个 service

public interface LdapService {

    /**
     * 获取所有用户
     * @return list {@link Person}
     */
    Iterable<Person> findAllPersons();

    /**
     * 获取单个用户
     * @param attribute 属性
     * @param value 值
     * @return {@link Person}
     */
    Person findOnePerson(String attribute, String value);

    /**
     * 验证的属性
     * @param attribute 属性
     * @param username 用户名
     * @param password 密码
     * @return {@link Person}
     */
    Person authenticate(String attribute, String username, String password);

}
@Service
@Slf4j
public class LdapServiceImpl implements LdapService {

    private final LdapTemplate ldapTemplate;
    private final PersonRepository personRepository;

    @Autowired
    public LdapServiceImpl(LdapTemplate ldapTemplate, PersonRepository personRepository) {
        this.ldapTemplate = ldapTemplate;
        this.personRepository = personRepository;
    }

    @Override
    public Iterable<Person> findAllPersons() {
        return personRepository.findAll();
    }

    @Override
    public Person findOnePerson(String attribute, String value) {
        try {
            return ldapTemplate.findOne(query().where(attribute).is(value), Person.class);
        } catch (EmptyResultDataAccessException e) {
            log.error("Found 0 result, error: {}", e.getMessage());
        }
        return null;
    }

    @Override
    public Person authenticate(String attribute, String username, String password) {
        return ldapTemplate.authenticate(
                query().where(attribute).is(username),
                password,
                (dirContext, ldapEntryIdentification) ->
                        ldapTemplate.findOne(query().where(attribute).is(username), Person.class));
    }
}

整合完毕 !!

5. 可能遇到的问题

PartialResultException: Unprocessed Continuation Reference(s); remaining name '/'
这个是 AD 无法进行自动处理引用的原因,解决方法:忽略这个异常,设置 setIgnorePartialResultExceptiontrue

@Autowired
private final LdapTemplate ldapTemplate;
......
ldapTemplate.setIgnorePartialResultException(true);

AuthenticationException: [LDAP: error code 49 - 80090308: LdapErr: DSID-0C0903C5, comment: AcceptSecurityContext error, data 52e, v2580
这个错误一般是上面第二步配置 yml 的 username 不正确造成

spring:
  ldap:
    urls: ldap://a.b.com:389
    ## 这里出错
    username: c@b.com
    password: xxxxxxxxx
    base: dc=b,dc=com

解决方法:
username 配置方式推荐这两种

  1. 属性配置形式:CN=a,CN=Users,DC=b,DC=com
  2. 域名形式: a@b.com a 为登陆用户名,b.com 是上面配置的 dc=b,dc=com 组合

无法搜索全部用户的一个解决方案:递归
这只是其中一个解决方案,其他的还在摸索。原理:利用递归遍历所有 OU 下的用户并汇总

@Override
    public List<Person> findAllPersonsByRecursion(String ou) {
        allUsers = new ArrayList<>();
        recursiveSearchOu("OU=YourOU");
        return allUsers;
    }

    public void recursiveSearchOu(String ou) {
        List<String> leafNodes = ldapTemplate.list(ou);
        for (String node : leafNodes) {
            String[] temp = node.split("=");
            if ("OU".equals(StringUtils.upperCase(temp[0]))) {
            	// 拼接 OU,递归搜索
                recursiveSearchOu(node + "," + ou);
            }
            if ("CN".equals(StringUtils.upperCase(temp[0]))) {
                // 防止循环读入,第一个直接读取全部数据并中断
                LdapName name = LdapNameBuilder.newInstance()
                        .add(ou)
                        .build();
                // 查找当前 OU 下的所有 User
                List<Person> personList = ldapTemplate.findAll(name, searchControl(), Person.class);
                // 汇总每个 ou 下的 user
                allUsers.addAll(personList);
                break;
            }
        }
    }

    private SearchControls searchControl() {
        SearchControls controls = new SearchControls();
        // 设定搜索范围 OBJECT_SCOPE、ONELEVEL_SCOPE、SUBTREE_SCOPE
        controls.setSearchScope(2);
        // 搜索时间限制 0 - 无限制
        controls.setTimeLimit(0);
        // 搜索数量(entry)限制 0 - 无限制
        controls.setCountLimit(0);
        // 是否返回实体 obj, 如果为 false 则仅返回 name 和 class
        controls.setReturningObjFlag(true);
        // 设定返回的属性, 如果为 null 则返回全部属性
        controls.setReturningAttributes(null);
        return controls;
    }

关于LDAP数据过多(size limited exceeded error)的解决方案:分页查询
微软官网有介绍到 AD 的页查询数据限制 详情链接
大概意思就是查询 AD 用户数据时会存在一个数量限制,例如1000条用户数据。超过这个限制的数据不能查询到,造成 无法查询部分用户 的问题。而针对这个问题,SpringBoot 提供了分页查询的解决方案:文档链接
Demo:

public List<String> getAllPersonNames() {
        final SearchControls searchControls = new SearchControls();
        // 设置搜索域范围
        searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
		
		// 获取上下文数据源
        ContextSource contextSource = ldapTemplate.getContextSource();
        // 每次查询分页数据大小
        final PagedResultsDirContextProcessor processor = new PagedResultsDirContextProcessor(500);
        return SingleContextSource.doWithSingleContext(contextSource, new LdapOperationsCallback<List<String>>() {
            @Override
            public List<String> doWithLdapOperations(LdapOperations operations) {
                List<String> result = new LinkedList<>();

                LdapName ldapName = LdapNameBuilder.newInstance()
                        .add("OU=YourOU")
                        .build();
                AttributesMapper<String> attributesMapper = new AttributesMapper<String>() {
                    @Override
                    public String mapFromAttributes(Attributes attributes) throws NamingException {
                    	// 这里返回需要的属性数据,可根据需要返回相应的 Person 持久化对象
                    	// attributes 包含用户的可视属性数据
                        return (String) attributes.get("CN").get();
                    }
                };

                do {
                	// do while 循环分页查询
                    List<String> oneResult = operations.search(
                            ldapName,
                            "(&(objectclass=user))",
                            searchControls,
                            attributesMapper,
                            processor);
                    // 汇总数据
                    result.addAll(oneResult);
                } while(processor.hasMore());
                return result;
            }
        });
    }

这样就可以查询到所有用户数据了

拓展

LdapTemplate 的 Filter

  1. 过滤日期,格式:yyyyMMddHHmmss.0Z 其中 Z 和时区相关 (UTC)
    (&(objectclass=user)(whenChanged>=19700101000000.0Z))
    
  2. 过滤条件 (部分例子)
    或 |(&(objectclass=user)(|(cn=1)(cn=2))
    比较条件 >= , = , <=
  3. 一些 Demo 实例 详情链接

推荐 LDAP browser :Softerra LDAP Administrat, 官网下载地址

附带简单教程
安装的话直接一路 next 即可,中间可配置安装路径
安装完成后打开软件连接 LDAP

  1. 新建一个 Profile
    在这里插入图片描述
    输入连接名称,随便输即可
    在这里插入图片描述
  2. 连接参数配置
    在这里插入图片描述
    Host 是 AD 域的连接地址,例如域 ADtest 和 IP 地址都可以,端口默认 389
    base dn 这些信息可以从AD ACSI管理器查看,然后点击下一步
    在这里插入图片描述
    这里是输入连接凭证的,Principal Password 输入 yml 中配置的 username 和 password 点击下一步即可,等待验证链接完成就可以查看所有用的信息啦
    在这里插入图片描述

6. 完结撒花,一键三连不在话下 😄

在这里插入图片描述

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐