使用Spring ldap操作active directory

发布时间:2020-01-24 12:54:17阅读:(2013)

本文介绍spring boot集成ldap操作微软active directory。

依赖环境

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.4.RELEASE</version>
<relativePath/>
</parent>
...
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-ldap</artifactId>
</dependency>
...
</dependencies>

配置文件

spring:
ldap:
urls: ldaps://*.*.*.*:636 # ad地址,这里需要使用ssl链接
base: dc=***,dc=com # 配置基础的dn,在操作中就可以省略这部分
username: cn=admin,OU=Manager,dc=***,dc=com # 管理员dn
password: ****** # 管理员密码

注:ad使用的ssl证书为自签证书,需要再java中配置,信任此证书,方法如下:

  1. 找ad管理员要ad的ca根证书
  2. 使用命令行导出信任证书库 keytool -import -trustcacerts -alias umapad-ssl -file /path/to/ldap.cer -keystore "/path/to/ldap-mapad.jks" (此过程中会提示输入密码)
  3. 在代码中设置信任证书库及密码(可以放在spring boot main函数开头或者使用启动参数设置)
public static void main(String[] args) {
System.setProperty("javax.net.ssl.trustStore","/path/to/ldap-mapad.jks");
System.setProperty("javax.net.ssl.trustStorePassword","******");
SpringApplication.run(LdapDemoApplication.class, args);
}
// 或者启动时添加
-Djavax.net.ssl.trustStore="/path/to/ldap-mapad.jks"
-Djavax.net.ssl.trustStorePassword="******"

实体

import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.Data;
import org.springframework.ldap.odm.annotations.Attribute;
import org.springframework.ldap.odm.annotations.Entry;
import org.springframework.ldap.odm.annotations.Id;

import javax.naming.Name;

/**
* ldap用户实体
*/
@Data
@Entry(objectClasses = "person")
public final class Person {

@Id
@JsonSerialize(using = ToStringSerializer.class)
private Name dn;

@Attribute(name = "cn")
private String name;

@Attribute(name = "sAMAccountName")
private String account;

@Attribute(name = "mail")
private String email;

@Attribute(name = "userAccountControl")
protected String status;
// 其他属性需要可以继续添加
}

import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.support.LdapUtils;

import javax.naming.NamingException;
import javax.naming.directory.Attributes;

/**
* ldap用户实体映射(使用ldap的odm时不需要使用映射,其他情况需要使用映射)
*/
public class LdapPersonAttributeMapper implements AttributesMapper<Person> {
@Override
public Person mapFromAttributes(Attributes attributes) throws NamingException {
Person person = new Person();
person.setName(attributes.get("cn").get().toString());
person.setAccount(attributes.get("sAMAccountName").get().toString());
person.setEmail(attributes.get("mail").get().toString());
person.setStatus(attributes.get("userAccountControl").get().toString());
person.setDn(LdapUtils.newLdapName(attributes.get("distinguishedName").get().toString()));
return person;
}
}

账号查询:(由于ad中sAMAccountName是唯一的,因此本文中查找都通过此属性查找)

方法一:

public Person find(String account){
Person person =ldapTemplate.findOne(LdapQueryBuilder.query().where("sAMAccountName").is(account).and("objectClass").is("person"),Person.class);
return person;
}

此方法存在一个问题:单查不到的时候会抛出一个EmptyResultDataAccessException异常

方法二:

public Person find(String account){
List<Person> list = ldapTemplate.find(LdapQueryBuilder.query()
.where("sAMAccountName").is(account)
.and("objectClass").is("person"), Person.class);
if(list==null || list.size()==0){
return null;
}else if(list.size()>1){
throw new RuntimeException("匹配到多个用户");
}else {
return list.get(0);
}
}

查询所有用户:

方法一:(此方法使用odm,因此不需要使用映射)

public List<Person> queryAll(){
List<Person> list = ldapTemplate.find(LdapQueryBuilder.query().base("ou=测试公司").where("objectClass").is("person"), Person.class);
return list;
}

方法二:

public List<Person> queryAll(){
List<Person> list = ldapTemplate.search(LdapQueryBuilder.query().base("ou=测试公司").where("objectClass").is("person"), new LdapPersonAttributeMapper());
return list;
}

分页查询所有用户:

由于有些ad中数据量非常大,批量拉取一般会有限制(默认一次可以拉1000条记录),虽然这个限制可以调整,但是对大量数据页不是很友好,因此这里可以使用分页查询

public List<Person> queryAllPage() {
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
controls.setReturningObjFlag(true);

SearchExecutor executor = ctx -> ctx.search(LdapUtils.newLdapName("ou=测试公司"), "(&(objectClass=person))", controls);
PagedResultsCookie cookie = null;
PagedResultsDirContextProcessor requestControl;
AttributesMapperCallbackHandler<Person> callbackHandler = new AttributesMapperCallbackHandler<>(new LdapPersonAttributeMapper());
do {
requestControl = new PagedResultsDirContextProcessor(500, cookie);
ldapTemplate.search(executor, callbackHandler, requestControl);
cookie = requestControl.getCookie();
}while (requestControl.hasMore());
return callbackHandler.getList();
}

增量查询:

部分场景下,我们需要同步ad的数据,但每次都全量同步数据太多,因此可以使用ad中的修改时间来实现增量更新

public List<Person> queryDiff(Date startTime){
SimpleDateFormat format = new SimpleDateFormat("yyyyMMddHHmmss.SZ");
List<Person> list = ldapTemplate.find(LdapQueryBuilder.query().base("ou=测试公司").where("objectClass").is("person")
.and("whenChanged").gte(format.format(startTime)), Person.class);
return list;
}
// 或者
public List<Person> queryDiff(Date startTime){
List<Person> list = ldapTemplate.search(LdapQueryBuilder.query().base("ou=测试公司").where("objectClass").is("person")
.and("whenChanged").gte(format.format(startTime)), new LdapPersonAttributeMapper());
return list;
}

注:增量查询也可以使用分页,只需将分页查询中"(&(objectClass=person))"修改为"(&(objectClass=person)(whenChanged>="+format.format(startTime)+"))"即可

添加OU

public boolean addOU(String name, String baseDn) {
Attributes ouAttributes=new BasicAttributes();
BasicAttribute ouBasicAttribute=new BasicAttribute("objectClass");
ouBasicAttribute.add("organizationalUnit");
ouAttributes.put(ouBasicAttribute);
if(StringUtils.isBlank(baseDn)){
baseDn = "";
}
LdapName dn = LdapUtils.newLdapName(baseDn);
try {
dn.add(new Rdn("ou", name));
} catch (InvalidNameException e) {
log.error("{}",e.getMessage(), e);
return false;
}
ldapTemplate.bind(dn,null,ouAttributes);
return true;
}
// 使用: addOU("部门一","ou=测试公司")
// addOU("部门二","ou=部门一,ou=测试公司")

删除OU

/**
* dn: ou的dn
* recursive: 是否级联删除此OU下的OU、用户、分组等
*/
public boolean deleteOU(String dn, boolean recursive) {
ldapTemplate.unbind(dn, recursive);
return true;
}
// 使用:deleteOU("ou=部门二,ou=部门一,ou=测试公司", false) 删除部门二
// deleteOU("ou=部门一,ou=测试公司", true) 级联删除部门一级以下的数据

添加用户

public boolean addUser(String name, String account, String baseDn) {
Attributes attrs = new BasicAttributes();
BasicAttribute oc = new BasicAttribute("objectClass");
oc.add("top");
oc.add("person");
oc.add("organizationalPerson");
oc.add("user");
attrs.put(oc);
attrs.put("sAMAccountName", account);
attrs.put("cn", name);
attrs.put("displayName", name);
attrs.put("mail", account+"@xxx.com");
int status = Constants.NORMAL_ACCOUNT;
attrs.put("userAccountControl", String.valueOf(status));
String pwd = "Hello@1234"; // 初始密码

attrs.put("unicodePwd", encodePwd(pwd));
if(StringUtils.isBlank(baseDn)){
baseDn = "";
}
LdapName dn = LdapNameBuilder.newInstance(baseDn)
.add("cn", name)
.build();
ldapTemplate.bind(dn, null, attrs);
return true;
}

private byte[] encodePwd(String source){
String quotedPassword = "\"" + source + "\""; // 注意:必须在密码前后加上双引号
return quotedPassword.getBytes(StandardCharsets.UTF_16LE);
}

// 使用:addUser("张三","san.zhang","ou=部门一,ou=测试公司")

注:用户userAccountControl属性详见 官方文档

删除用户

public boolean deleteUser(String dn) {
ldapTemplate.unbind(dn);
return true;
}
// 使用: deleteUser("cn=张三,ou=部门一,ou=测试公司")

修改用户密码

public boolean updatePwd(String dn, String newPwd) {
ModificationItem[] mods = new ModificationItem[1];
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute("unicodePwd", encodePwd(newPwd)));
ldapTemplate.modifyAttributes(dn, mods);
return true;
}
// 使用:updatePwd("cn=张三,ou=部门一,ou=测试公司","newPassw0rd")

添加用户组

public boolean addGroup(String name, String baseDn) {
Attributes groupAttributes=new BasicAttributes();
BasicAttribute oc=new BasicAttribute("objectClass");
oc.add("top");
oc.add("group");
groupAttributes.put(oc);
groupAttributes.put("cn", name);
groupAttributes.put("sAMAccountName", name);
BasicAttribute member = new BasicAttribute("member");
member.add("cn=张三,ou=部门一,ou=测试公司");
member.add("cn=李四,ou=部门一,ou=测试公司");
groupAttributes.put(member);
if(StringUtils.isBlank(baseDn)){
baseDn = "";
}
LdapName dn = LdapUtils.newLdapName(baseDn);
try {
dn.add(new Rdn("cn", name));
} catch (InvalidNameException e) {
log.error("{}",e.getMessage(), e);
return false;
}
ldapTemplate.bind(dn,null,groupAttributes);
return true;
}
// 使用:addGroup("分组一","ou=测试公司")

将用户添加至用户组

public boolean addUserToGroup(String userDn, String groupDn) {
DirContextOperations ctxGroup = ldapTemplate.lookupContext(groupDn);
DirContextOperations ctxUser = ldapTemplate.lookupContext(userDn);
ctxGroup.addAttributeValue("member", ctxUser.getStringAttribute("distinguishedName"));
ldapTemplate.modifyAttributes(ctxGroup);
return true;
}
// 使用: addUserToGroup("cn=小明,ou=部门一,ou=测试公司","cn=分组一,ou=测试公司")

从用户组中删除用户

public boolean removeUserFromGroup(String userDn, String groupDn) {
DirContextOperations ctxGroup = ldapTemplate.lookupContext(groupDn);
DirContextOperations ctxUser = ldapTemplate.lookupContext(userDn);
ctxGroup.removeAttributeValue("member", ctxUser.getStringAttribute("distinguishedname"));
ldapTemplate.modifyAttributes(ctxGroup);
return true;
}
// 使用: removeUserFromGroup("cn=小明,ou=部门一,ou=测试公司","cn=分组一,ou=测试公司")

密码验证(可以用于登录验证)

public boolean auth(String account, String password) {
// 注:此函数有个bug,当password为空是,校验也能通过,因此需要先校验password非空
ldapTemplate.authenticate(query().where("sAMAccountName").is(account), password);
return true;
}

源码:gitee github

发表评论

评论列表(有1条评论2013人围观)
晚风2021-12-02 20:09:52

你好,我试用了你的方法连接SSL,但是还是会报下面的问题,请问这这个有办法解决吗?谢谢!

org.springframework.ldap.CommunicationException: simple bind failed: Ip:636; nested exception is javax.naming.CommunicationException: simple bind failed: Ip:636 [Root exception is java.net.SocketException: Connection or outbound has closed]