一、背景
近期使用springboot 自带ldapsTemplate进行AD账号管理技术改造,查阅了多份资料结合实战,整理一份可运行的案例,考虑到ldapTemplate返回的错误信息晦涩难懂,详细参照文中细节一定可以大大减轻你接入时的痛苦!
二、结合代码实战解析
2.1 ldaps依赖引入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-ldap</artifactId>
</dependency>
2.2 application.yaml配置添加
ldap:
#ldaps服务地址,636为ldaps端口
url: ldaps://10.230.22.76:636/
base: DC=ddd,DC=com
username: 'ddd\ldap_admin'
passwd: 'xxxxxxxx'
以实际服务器及账号密钥为主,其中base很重要,代表你的顶级树域;
2.3 核心配置类
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.support.LdapContextSource;
import javax.naming.Context;
import java.util.Hashtable;
import java.util.Objects;
/**
* @author z先生
* @date 2023/12/04
*/
@Slf4j
@Configuration
// @ConditionalOnExpression("${ldap.enabled:false}")
public class LdapConfiguration {
private LdapTemplate ldapTemplate;
@Value("${ldap.url}")
private String ldapUrl;
@Value("${ldap.base}")
private String ldapBaseDc;
@Value("${ldap.username}")
private String ldapUsername;
@Value("${ldap.passwd}")
private String ldapPasswd;
/**
* 继承LdapContextSource重写getAnonymousEnv方法来加载,
* 使连接ldap时用SSL连接(由于修改AD密码时必须使用SSL连接)
*/
public class SsldapContextSource extends LdapContextSource {
public Hashtable<String, Object> getAnonymousEnv() {
System.setProperty("com.sun.jndi.ldap.object.disableEndpointIdentification", "true");
Hashtable<String, Object> anonymousEnv = super.getAnonymousEnv();
anonymousEnv.put("java.naming.security.protocol", "ssl");
anonymousEnv.put("java.naming.ldap.factory.socket", CustomSslSocketFactory.class.getName());
anonymousEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
return anonymousEnv;
}
}
@Bean
public LdapContextSource contextSource() {
SsldapContextSource ldapContextSource = new SsldapContextSource();
ldapContextSource.setBase(ldapBaseDc);
ldapContextSource.setUrl(ldapUrl);
ldapContextSource.setUserDn(ldapUsername);
ldapContextSource.setPassword(ldapPasswd);
ldapContextSource.setPooled(false);
ldapContextSource.setReferral("follow");
ldapContextSource.afterPropertiesSet();
return ldapContextSource;
}
@Bean
public LdapTemplate ldapTemplate(LdapContextSource contextSource) {
if (Objects.isNull(contextSource)) {
throw new RuntimeException("ldap contextSource error");
}
if (null == ldapTemplate) {
ldapTemplate = new LdapTemplate(contextSource);
}
return ldapTemplate;
}
}
关联CustomSslSocketFactory,用于跳过证书认证;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
/**
* 自定义的SSL工厂里面加载自己实现X509TrustManager,信任自签证书
* @author z先生
*/
public class CustomSslSocketFactory extends SSLSocketFactory {
private SSLSocketFactory socketFactory;
public CustomSslSocketFactory() {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, new TrustManager[]{new DummyTrustmanager()}, new SecureRandom());
socketFactory = ctx.getSocketFactory();
} catch (Exception ex) {
ex.printStackTrace(System.err);
}
}
public static SocketFactory getDefault() {
return new CustomSslSocketFactory();
}
@Override
public String[] getDefaultCipherSuites() {
return socketFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return socketFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket socket, String string, int num, boolean bool) throws IOException {
return socketFactory.createSocket(socket, string, num, bool);
}
@Override
public Socket createSocket(String string, int num) throws IOException, UnknownHostException {
return socketFactory.createSocket(string, num);
}
@Override
public Socket createSocket(String string, int num, InetAddress netAdd, int i) throws IOException, UnknownHostException {
return socketFactory.createSocket(string, num, netAdd, i);
}
@Override
public Socket createSocket(InetAddress netAdd, int num) throws IOException {
return socketFactory.createSocket(netAdd, num);
}
@Override
public Socket createSocket(InetAddress netAdd1, int num, InetAddress netAdd2, int i) throws IOException {
return socketFactory.createSocket(netAdd1, num, netAdd2, i);
}
/**
* 证书
*/
public static class DummyTrustmanager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] cert, String string) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] cert, String string) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[0];
}
}
}
核心ad账号实体类
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.ldap.odm.annotations.Attribute;
import org.springframework.ldap.odm.annotations.DnAttribute;
import org.springframework.ldap.odm.annotations.Entry;
import org.springframework.ldap.odm.annotations.Id;
import javax.naming.Name;
/**
* sss
*
* @author: z先生
* 2023/12/04
*/
@Data
@NoArgsConstructor
@Entry(objectClasses = {"user", "organizationalPerson", "person", "top"})
public final class AdPerson {
@Id
@JsonIgnore
private Name id;
@Attribute(name = "samAccountName")
private String samAccountName;
@DnAttribute(value = "CN", index = 0)
@Attribute(name = "cn")
private String cn;
@Attribute(name = "sn")
private String sn;
/**
* 取值:First Name
*/
@Attribute(name = "givenName")
private String givenName;
@Attribute(name = "userPrincipalName")
private String userPrincipalName;
@Attribute(name = "displayName")
private String displayName;
@Attribute(name = "mobile")
private String mobile;
@Attribute(name = "mail")
private String mail;
/**
* ouName:从映射表中获取 ad_ou
*/
private String ouName;
@Attribute(name = "name")
private String name;
/**
* 职位 jobTitle
*/
@Attribute(name = "title")
private String title;
/**
* 部门名称:从映射表中获取 display_name
*/
@Attribute(name = "department")
private String department;
/**
* 领导:从映射表中获取 managerOu
*/
@Attribute(name = "manager")
private String manager;
/**
* 密码,默认,从配置文件中获取
*/
@Attribute(name = "password")
private String password;
@Attribute(name = "telephoneNumber")
private String telephoneNumber;
/**
* 集团工号:customerString1
*/
@Attribute(name = "employeeID")
private String employeeID;
/**
* 子公司工号:personIdExternal
*/
@Attribute(name = "employeeNumber")
private String employeeNumber;
/**
* 公司:company
*/
@Attribute(name = "company")
private String company;
@Attribute(name = "co")
private String co;
@Attribute(name = "l")
private String l;
@Attribute(name = "description")
private String description;
private String payGrade;
}
三、AD账号管理相关方法
主服务类
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.query.LdapQuery;
import org.springframework.ldap.query.LdapQueryBuilder;
import org.springframework.ldap.support.LdapNameBuilder;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.naming.Name;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* ldap服务方法类
*
* @author: z先生
* 2023/12/05
*/
@Slf4j
@Service
public class LdapService {
@Autowired
private LdapTemplate ldapTemplate;
@Value("${ldap.base}")
private String baseOu;
private final String ACCOUNT_ENABLE = Integer.toString(0x0220);
private final String ACCOUNT_DISABLE = Integer.toString(0x0220);
private final static String INNER_ACCOUNT_DEL_OU = "CN=%s,OU=HQ,OU=Disable,OU=IT-Service";
private final static String OUT_ACCOUNT_DEL_OU = "CN=%s,OU=External,OU=Disable,OU=IT-Service";
private final static String SAM_ACCOUNT = "samAccountName";
private final static String ACCOUNT_CTL = "userAccountControl";
private final static String U_P_P = "userPrincipalName";
/**
* 移动ou
*
* @param person
* @param toOu
* @return
*/
public Boolean removeOu(AdPerson person, String toOu) {
String originOu = getDnStr(person, baseOu);
// ldapTemplate.rename(dnName, getDnName(person));
ldapTemplate.rename(originOu, toOu);
return true;
}
}
核心工具类
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import java.util.List;
/**
* @author: z先生
* 2023/12/05
*/
public class LdapUtil {
private final static String NORMAL_OU = "CN=%s%s";
private final static String DC = ",DC=ddd,DC=com";
public static String getDnNameById(LdapName id) {
List<Rdn> list = id.getRdns();
StringBuilder sb = new StringBuilder(64);
int i = list.size();
while (--i >= 0) {
Rdn tmp = list.get(i);
sb.append(tmp.getType()).append("=").append(tmp.getValue()).append(",");
}
sb.setLength(sb.length() - 1);
return sb.toString();
}
public static String getDnStr(AdPerson person, String baseOu) {
if (person.getId() != null) {
return getDnNameById((LdapName) (person.getId()));
}
Integer index = person.getOuName().indexOf(baseOu);
String ou = "";
if (index > 0) {
ou = "," + person.getOuName().substring(0, index - 1);
}
return String.format(NORMAL_OU, person.getDisplayName(), ou);
}
public static String getOuNameById(LdapName id) {
List<Rdn> list = id.getRdns();
StringBuilder sb = new StringBuilder(64);
for (Rdn tmp : list) {
if (tmp.getType().equals("OU")) {
sb.append(tmp.getValue()).append("/");
}
}
sb.setLength(sb.length() - 1);
return sb.toString();
}
public static String getWholeOuName(LdapName id) {
return getDnNameById(id) + DC;
}
public static String getAdOuName(String managerOu) {
return managerOu.substring(managerOu.indexOf(",") + 1);
}
}
日志工具类
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* 日志工具类
*
* @author: z先生
* 2023/3/14
*/
public class LogUtil {
public static String printEx(Exception ex) {
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
ex.printStackTrace(printWriter);
return stringWriter.toString();
}
}
3.1 查询
/**
* @param keyword
* @param isPrecise true精确查询,false模糊查询
* @return
*/
public List<AdPerson> queryUser(String keyword, Boolean isPrecise) {
//keyword = keyword + "*";
LdapQuery query = isPrecise ?
LdapQueryBuilder.query().where(SAM_ACCOUNT).is(keyword)
: LdapQueryBuilder.query().where(SAM_ACCOUNT).is("*" + keyword + "*");
Long start = System.currentTimeMillis();
Long start = System.currentTimeMillis();
List<AdPerson> list = ldapTemplate.find(query, AdPerson.class);
System.out.println("延时:" + (System.currentTimeMillis() - start));
return list;
}
查询时,最好根据实际场景来确定使用精确还是模糊查询,精确查询延时较低,模糊查询延时较大;
3.2新增
/**
* 账号创建
*
* @param person
* @return
* @throws UnsupportedEncodingException
*/
public Boolean createUser(AdPerson person) throws UnsupportedEncodingException {
Name name = getDnName(person);
person.setId(name);
person.setOuName(null);
String psd = person.getPassword();
person.setPassword(null);
ldapTemplate.create(person);
//密码等属性,只能通过更改属性的方式写入
List<ModificationItem> itemList = new ArrayList<>();
if (StringUtils.isNotBlank(psd)) {
//unicodePwd构建
String newQuotedPassword = "\"" + psd + "\"";
byte[] newUnicodePassword = newQuotedPassword.getBytes("UTF-16LE");
itemList.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute("unicodePwd", newUnicodePassword)));
}
//职级
if (StringUtils.isNotBlank(person.getPayGrade())) {
itemList.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute(AD_P_GRADE, person.getPayGrade())));
}
itemList.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute(SAM_ACCOUNT, person.getSamAccountName())));
itemList.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute(ACCOUNT_CTL, ACCOUNT_ENABLE)));
ModificationItem[] items = new ModificationItem[itemList.size()];
itemList.toArray(items);
ldapTemplate.modifyAttributes(name, items);
return true;
}
密码、samAccount需要以更改属性的方式传入;
3.3更新
/**
* 更新时需要忽略的属性
*/
private Set<String> ignoreSet = new HashSet<>(Arrays.asList(SAM_ACCOUNT, "password", "cn", "ouName", "mail","name"));
/**
* 账号更新
*
* @param person
* @return
*/
public Boolean updateUser(AdPerson person) throws UnsupportedEncodingException {
//查询账户
List<AdPerson> lists = queryUser(person.getSamAccountName(), true);
if (CollectionUtils.isEmpty(lists)) {
log.info("updateUser失败,未查询到{}", person.getMail());
return createUser(person);
}
AdPerson old = lists.get(0);
String oldOu = getDnNameById((LdapName) (old.getId()));
String newOu = getDnStr(person, baseOu);
//比对差异,进行属性替换
JSONObject oldJson = JSON.parseObject(JSON.toJSONString(old));
JSONObject newJson = JSON.parseObject(JSON.toJSONString(person));
List<ModificationItem> items = new ArrayList<>();
for (String key : newJson.keySet()) {
if (ignoreSet.contains(key)) {
continue;
}
String nValue = getValue(newJson, key);
String oValue = getValue(oldJson, key);
if (!nValue.equals(oValue)) {
String attr = key;
if (key.equals("payGrade")) {
attr = AD_P_GRADE;
}
items.add(new ModificationItem(DirContext.REPLACE_ATTRIBUTE, new BasicAttribute(attr, nValue)));
}
}
if (items.size() > 0) {
ModificationItem[] modificationItems = new ModificationItem[items.size()];
items.toArray(modificationItems);
ldapTemplate.modifyAttributes(oldOu, modificationItems);
}
//替换ou
if (!oldOu.equals(newOu)) {
ldapTemplate.rename(oldOu, newOu);
}
return true;
}
private String getValue(JSONObject js, String key) {
String result = js.getString(key);
return StringUtils.isBlank(result) ? "" : result;
}
3.4停用
/**
* 停用户
*
* @param person
* @return
*/
public Boolean disableAccount(AdPerson person) {
Name dnName = getDnName(person);
ModificationItem[] items = new ModificationItem[1];
items[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute(ACCOUNT_CTL, ACCOUNT_DISABLE));
ldapTemplate.modifyAttributes(dnName, items);
//外协员工和正式员工
String delOu = person.getSamAccountName().startsWith("e-") ?
String.format(OUT_ACCOUNT_DEL_OU, person.getDisplayName()) : String.format(INNER_ACCOUNT_DEL_OU, person.getDisplayName());
removeOu(person, delOu);
return true;
}
3.5启用
public Boolean enableAccount(AdPerson person) {
Name dnName = getDnName(person);
ModificationItem[] items = new ModificationItem[1];
items[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
new BasicAttribute(ACCOUNT_CTL, ACCOUNT_ENABLE));
try {
ldapTemplate.modifyAttributes(dnName, items);
} catch (Exception e) {
log.warn("开启账号失败:" + LogUtil.printEx(e));
return false;
}
return true;
}
四、接口封装
基于LdapService进行接口封装和实现即可,无难度,自行实现
五、结语
网上杂文很多,很多是不能用的;本篇是最近实践结论,100%可行;欢迎打赏
有问题加我微信 popodadan