基于springboot+Ldaps协议进行AD账号管理

一、背景

      近期使用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

  • 9
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值