SpringBoot 集成 LDAP获取用户信息
欲买桂花同载酒,终不似、少年游。 --唐多令·芦叶满汀洲
测试环境:
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 测试接口
下面测试获取用户、验证接口
- 获取所有用户信息
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)
- 获取单个用户信息
以上面的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)
- 验证用户账户
这里以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 无法进行自动处理引用的原因,解决方法:忽略这个异常,设置 setIgnorePartialResultException
为 true
@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
配置方式推荐这两种
- 属性配置形式:
CN=a,CN=Users,DC=b,DC=com
- 域名形式:
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
- 过滤日期,格式:
yyyyMMddHHmmss.0Z
其中Z
和时区相关 (UTC)(&(objectclass=user)(whenChanged>=19700101000000.0Z))
- 过滤条件 (部分例子)
或 |
:(&(objectclass=user)(|(cn=1)(cn=2))
比较条件 >= , = , <=
- 一些 Demo 实例 详情链接
推荐 LDAP browser :Softerra LDAP Administrat, 官网下载地址
附带简单教程
安装的话直接一路 next 即可,中间可配置安装路径
安装完成后打开软件连接 LDAP
- 新建一个
Profile
输入连接名称,随便输即可
- 连接参数配置
Host
是 AD 域的连接地址,例如域ADtest
和 IP 地址都可以,端口默认 389
base dn 这些信息可以从ADACSI管理器
查看,然后点击下一步
这里是输入连接凭证的,Principal
Password
输入yml
中配置的 username 和 password 点击下一步即可,等待验证链接完成就可以查看所有用的信息啦
6. 完结撒花,一键三连不在话下 😄
更多推荐
所有评论(0)