信步漫谈之AD域服务器—LDAPS认证改密

一、环境说明

AD域服务器安装环境:Windows Server 2012

二、实例程序

实例程序提供了LDAPS证书认证和免密认证两种方式,以及修改密码、解锁账号。

注意:如需修改AD用户的密码,只可通过LDAPS方式,不可通过LDAP方式。

1)实例结构

实例名:ldap-ssl-demo
com.alfred.ldap.ssl.demo
     enums
     exception
     nocert
     utils

2)实例代码

package com.alfred.ldap.ssl.demo.enums;

/**
 * @Author: alfred
 * @Date: 2020/9/17
 */
public enum AdConfEnum {

    LDAP_IP("LdapIp"),
    LDAP_PORT("LdapPort"),
    LDAP_USER_DN("LdapUserdn"),
    LDAP_PASSWORD("LdapPassword"),
    LDAP_BASE_DN("LdapBaseDn"),
    LDAPS_USER_CERT("LdapsUseCert"),
    LDAPS_CERT("LdapsCert"),
    ;

    private String name;

    AdConfEnum(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}
AdConfEnum
package com.alfred.ldap.ssl.demo.enums;

/**
 * AD操作返回值定义
 *
 * @Author: alfred
 * @Date: 2020/9/12
 */
public enum AdReturnCode {

    //================通用返回值
    PARAM_ERR(2, "参数为空或错误"),//参数为空或错误
    CONNECT_FAIL(3, "连接AD服务器失败"),//连接AD服务器失败
    SERVER_ERR(4, "服务器内部错误"),//服务器内部错误
    //================ad 认证
    VERIFICATION_FAIL(0, "认证失败"),//认证失败
    VERIFICATION_SUCCESS(1, "认证成功"),//认证成功
    //================ad 修改密码
    ADPWD_POLICY_INVALID(5, "AD认证密码不符合策略"),//AD认证密码不符合策略
    ADPWD_MODIFY_SUCCESS(6, "AD认证密码修改成功"),//AD认证密码修改成功
    AD_USER_NOT_EXIST(7, "AD用户不存在"),//AD用户不存在
    ADPWD_TIMEOUT(8, "AD用户密码过期"),//AD用户密码过期
    ADPWD_MUST_MODIFY(9, "AD用户下次登录必须修改密码"),//AD用户下次登录必须修改密码
    //================ad 解锁
    AD_USER_UNLOCAK_SUCCESS(10, "AD用户解锁成功"),//AD用户解锁成功
    AD_USER_UNLOCAK_FAIL(11, "AD用户解锁失败"),//AD用户解锁失败
    ;

    private Integer code;
    private String msg;

    AdReturnCode(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    public Integer getCode() {
        return code;
    }

    public String getMsg() {
        return msg;
    }

}
AdReturnCode
package com.alfred.ldap.ssl.demo.enums;

/**
 * AD用户属性枚举
 *
 * @Author: alfred
 * @Date: 2020/9/12
 */
public enum AdUserAttributeEnum {
    DN("distinguishedname"),//用户DN
    ACCOUNT_EXPIRES("accountexpires"),//账号过期时间
    SAM_ACCOUNT_NAME("sAMAccountName"),//安全主体对象(唯一账号名)
    PWD_LAST_SET("pwdLastSet"),//此项为0,则下次登录必须修改密码
    USER_ACCOUNT_CONTROL("userAccountControl"),
    ;

    private String attr;

    AdUserAttributeEnum(String attr) {
        this.attr = attr;
    }

    public String getSearchParameter(String value){
        return this.getAttr()+"="+value;
    }

    public String getAttr() {
        return attr;
    }

}
AdUserAttributeEnum
package com.alfred.ldap.ssl.demo.exception;


import com.alfred.ldap.ssl.demo.enums.AdReturnCode;

/**
 * AD用户操作,使用抛出异常方式,自定义异常
 *
 * @Author: alfred
 * @Date: 2020/9/12
 */
public class AdException extends RuntimeException {
    private AdReturnCode code;

    public AdException(AdReturnCode code){
        super(code.getMsg());
        this.code = code;
    }

    public AdReturnCode getCode() {
        return code;
    }

}
AdException
package com.alfred.ldap.ssl.demo.nocert;

import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;

public class DummySSLSocketFactory extends SSLSocketFactory {

    private SSLSocketFactory factory;
    
    public DummySSLSocketFactory() {
        try {
            SSLContext sslcontext = SSLContext.getInstance("TLS");
            sslcontext.init( null, // No KeyManager required
            new TrustManager[] { new DummyTrustManager()},
            new java.security.SecureRandom());
            factory = (SSLSocketFactory) sslcontext.getSocketFactory();
        } catch( Exception ex) {
            ex.printStackTrace();
        }
    }
    
    public static SocketFactory getDefault() {
        return new DummySSLSocketFactory();
    }
    
    public Socket createSocket(Socket socket, String s, int i, boolean flag) throws IOException {
        return factory.createSocket( socket, s, i, flag);
    }
    
    public Socket createSocket(InetAddress inaddr, int i, InetAddress inaddr1, int j) throws IOException {
        return factory.createSocket( inaddr, i, inaddr1, j);
    }
    
    public Socket createSocket(InetAddress inaddr, int i) throws IOException {
        return factory.createSocket( inaddr, i);
    }
    
    public Socket createSocket(String s, int i, InetAddress inaddr, int j) throws IOException {
        return factory.createSocket( s, i, inaddr, j);
    }
    
    public Socket createSocket(String s, int i) throws IOException {
        return factory.createSocket( s, i);
    }
    
    public String[] getDefaultCipherSuites() {
        return factory.getSupportedCipherSuites();
    }
    
    public String[] getSupportedCipherSuites() {
        return factory.getSupportedCipherSuites();
    }

}
DummySSLSocketFactory
package com.alfred.ldap.ssl.demo.nocert;

import javax.net.ssl.X509TrustManager;
import java.security.cert.X509Certificate;

public class DummyTrustManager implements X509TrustManager {
    public void checkClientTrusted(X509Certificate[] cert, String authType) {
        return;
    }
    
    public void checkServerTrusted(X509Certificate[] cert, String authType) {
        return;
    }
    
    public X509Certificate[] getAcceptedIssuers() {
        return new X509Certificate[0];
    }
}
DummyTrustManager
package com.alfred.ldap.ssl.demo.utils;

import com.alfred.ldap.ssl.demo.enums.AdConfEnum;
import com.alfred.ldap.ssl.demo.enums.AdReturnCode;
import com.alfred.ldap.ssl.demo.enums.AdUserAttributeEnum;
import com.alfred.ldap.ssl.demo.exception.AdException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.*;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import java.nio.charset.StandardCharsets;
import java.util.Calendar;
import java.util.Properties;

/**
 * LDAPS操作类
 *
 * @Author: alfred
 * @Date: 2020/9/12
 */
public class LdapsUtil {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    private static Control[] connCtls = null;

    private static Properties prop;
    static{
        //初始加载配置
        prop = new Properties();
        //AD域服务器地址,域名
        prop.setProperty(AdConfEnum.LDAP_IP.getName(), "WIN-OAH5ADPDKR0.mytest.com");
        //AD域服务器端口,默认636
        prop.setProperty(AdConfEnum.LDAP_PORT.getName(), "636");
        //AD域服务器管理员账号
        prop.setProperty(AdConfEnum.LDAP_USER_DN.getName(), "administrator@mytest.com");
        //AD域服务器管理员密码
        prop.setProperty(AdConfEnum.LDAP_PASSWORD.getName(), "alfred123!@#");
        //AD域服务器根节点信息,在此范围内进行用户查询
        prop.setProperty(AdConfEnum.LDAP_BASE_DN.getName(), "CN=Computers,DC=mytest,DC=com");
        //LDAPS认证证书,本地路径
        prop.setProperty(AdConfEnum.LDAPS_CERT.getName(), "D:\cacerts");
        //是否使用免密认证,true为不使用,false为使用
        prop.setProperty(AdConfEnum.LDAPS_USER_CERT.getName(), "true");
    }

    /**
     * 判断是否使用证书,如果否,则使用免密认证方式
     * @param env 环境变量属性
     */
    private void loadAdCert(Properties env){
        String useCert = prop.getProperty(AdConfEnum.LDAPS_USER_CERT.getName());
        if(useCert.equalsIgnoreCase("TRUE")){
            if(System.getProperty("javax.net.ssl.trustStore") != null){
                return;
            }
            System.setProperty("javax.net.ssl.trustStore", prop.getProperty(AdConfEnum.LDAPS_CERT.getName()));
        }else{
            env.put("java.naming.ldap.factory.socket", "com.alfred.ldap.ssl.demo.nocert.DummySSLSocketFactory");
        }
    }

    /**
     * 获取AD服务器连接
     *
     * @return 连接对象
     */
    private Object getCtx() {
        Properties env = new Properties();
        String ldapURL = "LDAPS://" + prop.getProperty(AdConfEnum.LDAP_IP.getName()) + ":"
                + prop.getProperty(AdConfEnum.LDAP_PORT.getName()) + "/";

        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.SECURITY_AUTHENTICATION, "simple");// LDAP访问安全级别:"none","simple","strong"
        env.put(Context.PROVIDER_URL, ldapURL);
        env.put(Context.SECURITY_PROTOCOL, "ssl");

        loadAdCert(env);

        try {
            return new InitialLdapContext(env, connCtls);
        } catch (NamingException e) {
            throw new AdException(AdReturnCode.CONNECT_FAIL);
        }
    }

    /**
     * AD用户登录LDAPS认证
     *
     * @param userName 用户名
     * @param passwd 认证密码
     * @return 认证结果
     */
    public AdReturnCode verify(String userName, String passwd) {
        LdapContext ctx = (LdapContext) getCtx();
        try{
            ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, userName);// AD User
            ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, passwd);// AD Password
            ctx.reconnect(connCtls);
        }catch(Exception e){
            //认证失败,开始判断失败原因,当前只判断密码过期
            Long userExpiresDateLong = getUserExpiresDateLong(userName, ctx);
            Long curTime = System.currentTimeMillis();
            logger.info("账户名:{},当前时间戳:{},到期时间戳:{}", new Object[]{userName, curTime, userExpiresDateLong});
            if(curTime.compareTo(userExpiresDateLong) > 0){
                throw new AdException(AdReturnCode.ADPWD_TIMEOUT);
            }else if(getAdUserIsMustModifyPwd(userName, ctx)){
                throw new AdException(AdReturnCode.ADPWD_MUST_MODIFY);
            }else{
                throw new AdException(AdReturnCode.VERIFICATION_FAIL);
            }
        }finally{
            if(ctx != null){
                try {
                    ctx.close();
                } catch (NamingException e) {
                    logger.error("ctx close error",e);
                }
            }
        }
        return AdReturnCode.VERIFICATION_SUCCESS;
    }

    /**
     * 解锁账号
     *
     * @param userName 用户名
     * @return 解锁账号返回结果
     */
    public AdReturnCode enableUser(String userName) {
        LdapContext ctx = (LdapContext) getCtx();

        String userDN = getAdUserDN(userName, ctx);
        BasicAttributes attrsbu = new BasicAttributes();

        //这个是重点
        attrsbu.put(AdUserAttributeEnum.USER_ACCOUNT_CONTROL.getAttr(), "512");
        //解锁后设置下一次登录必须修改密码
//        attrsbu.put(AdUserAttributeEnum.PWD_LAST_SET.getAttr(), "0");

        try {
            ctx.modifyAttributes(userDN, DirContext.REPLACE_ATTRIBUTE, attrsbu);
            return AdReturnCode.AD_USER_UNLOCAK_SUCCESS;
        } catch (NamingException e) {
            throw new AdException(AdReturnCode.AD_USER_UNLOCAK_FAIL);
        }finally{
            if(ctx != null){
                try {
                    ctx.close();
                } catch (NamingException e) {
                    logger.error("ctx close error",e);
                }
            }
        }
    }

    /**
     * 重置密码
     *
     * @param userName 用户名
     * @param newPassword 新密码
     * @return 修改用户密码返回结果
     */
    public AdReturnCode updateUserPassword(String userName, String newPassword) {
        LdapContext ctx = (LdapContext) getCtx();

        ModificationItem[] mods = new ModificationItem[1];
        String newQuotedPassword = """ + newPassword + """;
        byte[] newUnicodePassword = newQuotedPassword.getBytes(StandardCharsets.UTF_16LE);

        mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE,
                new BasicAttribute("unicodePwd", newUnicodePassword));

        // 修改密码
        String userDN = getAdUserDN(userName, ctx);
        try {
            ctx.modifyAttributes(userDN, mods);
            return AdReturnCode.ADPWD_MODIFY_SUCCESS;
        }catch(Exception e){
            throw new AdException(AdReturnCode.ADPWD_POLICY_INVALID);
        }finally{
            if(ctx != null){
                try {
                    ctx.close();
                } catch (NamingException e) {
                    logger.error("ctx close error",e);
                }
            }
        }
    }

    /**
     * 查找用户信息
     *
     * @param userName 用户名
     * @param ctx ldap连接
     * @return 用户属性
     */
    public Attributes getAdUserAttr(String userName, LdapContext ctx) {
        Attributes attrs = null;
        SearchControls control = new SearchControls();
        control.setSearchScope(SearchControls.SUBTREE_SCOPE);
        try {
            String ldapUserdn = prop.getProperty(AdConfEnum.LDAP_USER_DN.getName());
            String ldapPassword = prop.getProperty(AdConfEnum.LDAP_PASSWORD.getName());
            ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, ldapUserdn);
            ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, ldapPassword);
            //有的企业员工的dn不是有cn开头的,而是由uid开头的,这个因企业而异
            //使用cn,若存在重名用户,则返回的是最后一个员工,存在bug
            //NamingEnumeration<SearchResult> en = ctx.search(BASEN, "cn=" + cn, contro);
            //使用sAMAccountName,避免重名,比如存在四个张伟
            //删除域名,才能进行查询
            if(userName.contains("@")){
                userName = userName.substring(0, userName.indexOf("@"));
            }
            NamingEnumeration<SearchResult> en = ctx.search(prop.getProperty(AdConfEnum.LDAP_BASE_DN.getName()),
                    AdUserAttributeEnum.SAM_ACCOUNT_NAME.getSearchParameter(userName),
                    control);
            if (en == null) {
                throw new AdException(AdReturnCode.AD_USER_NOT_EXIST);
            }
            while (en.hasMoreElements()) {
                SearchResult obj = en.nextElement();
                if (obj != null) {
                    attrs = obj.getAttributes();
                    logger.info("获取AD账户属性,账户名:{},属性:{}", new Object[]{userName, attrs});
                    break;
                }
            }
            if(attrs == null){
                throw new AdException(AdReturnCode.AD_USER_NOT_EXIST);
            }
        } catch (NamingException e) {
            logger.error("AD User Get Attr Error:", e);
            throw new AdException(AdReturnCode.AD_USER_NOT_EXIST);
        }
        return attrs;
    }

    /**
     * AD账户时间戳转换
     * @param accountExpiresL 到期时间数据
     * @return  时间戳
     */
    public Long adExpiresToLong(long accountExpiresL){
        Calendar calendar = Calendar.getInstance();
        calendar.clear();
        calendar.set(1601, 0, 1, 0, 0);
        accountExpiresL = accountExpiresL/ 10000 + calendar.getTime().getTime();
        return accountExpiresL;
    }

    /**
     * 获取AD账户失效日期
     *
     * @param userName 用户名
     * @param ctx ldap连接
     * @return 失效时间戳
     */
    public Long getUserExpiresDateLong(String userName, LdapContext ctx){
        Attributes attrs = getAdUserAttr(userName, ctx);
        String accountexpires = attrs.get(
                AdUserAttributeEnum.ACCOUNT_EXPIRES.getAttr()).toString().split(":")[1].trim();
        logger.info("获取AD账户失效日期,账户名:{},配置项:{},值{}", new Object[]{userName, AdUserAttributeEnum.ACCOUNT_EXPIRES.getAttr(), accountexpires});
        return adExpiresToLong(Long.parseLong(accountexpires));
    }

    /**
     * 获取用户的dn
     *
     * @param userName 用户名
     * @param ctx ldap连接
     * @return 用户DN值
     */
    public String getAdUserDN(String userName, LdapContext ctx) {
        Attributes attrs = getAdUserAttr(userName, ctx);
        String userDNAttr = attrs.get(AdUserAttributeEnum.DN.getAttr()).toString();
        logger.info("获取AD账户DN,账户名:{},配置项:{},值{}", new Object[]{userName, AdUserAttributeEnum.DN.getAttr(), userDNAttr});
        return userDNAttr.split(":")[1].trim();
    }

    /**
     * 用户下次登录是否必须修改密码
     *
     * @param userName 用户名
     * @param ctx ldap连接
     * @return true:是  false:否
     */
    public Boolean getAdUserIsMustModifyPwd(String userName, LdapContext ctx) {
        Attributes attrs = getAdUserAttr(userName, ctx);
        String isMustModify = attrs.get(
                AdUserAttributeEnum.PWD_LAST_SET.getAttr()).toString().split(":")[1].trim();
        logger.info("获取是否下次登录必须修改密码,账户名:{},配置项:{},值{}", new Object[]{userName, AdUserAttributeEnum.PWD_LAST_SET.getAttr(), isMustModify});
        return isMustModify.equals("0");
    }

}
LdapsUtil
package com.alfred.ldap.ssl.demo;

import com.alfred.ldap.ssl.demo.enums.AdReturnCode;
import com.alfred.ldap.ssl.demo.exception.AdException;
import com.alfred.ldap.ssl.demo.utils.LdapsUtil;
import org.apache.log4j.BasicConfigurator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * @Author: alfred
 * @Date: 2020/9/17
 */
public class Main {

    private static Logger logger = LoggerFactory.getLogger(Main.class);

    public static void main(String[] args) {
        BasicConfigurator.configure();
        LdapsUtil ldapsUtil = new LdapsUtil();
        try{
            AdReturnCode adReturnCode = ldapsUtil.verify("alfred", "12weSD*(");
//            AdReturnCode adReturnCode = ldapsUtil.updateUserPassword("alfred", "12weSD*(");
//            AdReturnCode adReturnCode = ldapsUtil.enableUser("alfred");
            logger.info("操作返回码:{}	操作返回结果:{}", adReturnCode.getCode(), adReturnCode.getMsg());
        } catch (AdException e){
            logger.error("异常返回码:{}	异常返回结果:{}", e.getCode().getCode(), e.getCode().getMsg());
        }
    }

}
Main

3)修改hosts

文件路径:C:WindowsSystem32driversetchosts

添加IP和域名,域名为证书上的域名,这点很关键

4)证书文件

引用的cacerts见下方证书导出的证书,导入到JAVA库中

5)域节点名称(BaseDn)获取,此项决定了用户查询的范围

进入域服务器,右键用户范围的域根节点,选择展示高级功能

image

右键用户范围的域根节点,选择属性

image

可在属性编辑器中查看到域节点的名称(distinguishedName)

image

三、域服务器安装配置证书

安装

选择添加角色和功能

image

这里写图片描述

这里写图片描述

这里写图片描述

选择Active Directory证书服务
这里写图片描述

这里写图片描述

这里写图片描述

选择添加,前面一步不需要选择,直接下一步
这里写图片描述

后面都直接点击下一步,直到下面这张图开始安装
这里写图片描述

安装成功
这里写图片描述

配置

直接点击安装完成界面上的配置目标服务器上的Active Directory证书服务即可。

1)凭据会自动添加,直接点击下一步

这里写图片描述

2)角色服务将刚才安装的选择

这里写图片描述

3)指定类型一定要为企业CA,如果这一个选项为灰色不可选,需要看一看域配置是否正确

这里写图片描述

4)CA类型选择默认,默认为根CA,直接下一步

这里写图片描述

5)私钥类型选择默认,创建新的私钥,直接下一步

这里写图片描述

6)选择加密算法,默认即可,默认为SHA1加密算法,当前的计算2048为密钥长度即可

这里写图片描述

7)CA名称主机会直接默认生成,不用修改默认即可

这里写图片描述

8)选择有效期,默认为5年

这里写图片描述

9)数据库位置也会自动生成,下一步

这里写图片描述

10)CEP身份验证类型,默认下一步

CEP身份验证是一种基于证书密钥的续订的设置,这里不需要考虑,使用默认选择的集成身份验证即可。

这里写图片描述

11)因为目前并没有设置ssl加密使用的现有证书,所以选择稍后为ssl分配

这里写图片描述

12)确认,开始配置

这里写图片描述

这里写图片描述

13)配置成功

这里写图片描述

此时,CA证书服务安装配置成功,可以在IIS上面添加证书并开始测试

从主界面右上角工具栏中打开证书颁发机构,可以看到里面存在证书模板这一项,表示配置成功
这里写图片描述

四、域服务器导出证书

本着应用隔离的原则,建议把证书服务部署在一台独立的windows server 2012 r2虚拟机之中。证书服务器可以不用考虑高可用,因证书服务宕掉后,除了不能继续颁发证书和不能访问证书吊销信息,并不影响证书的其他验证。

1)证书服务的导出

1.win+r 后 输入 mmc


2.文件 添加/删除管理单元 新建证书 选择本地计算机 如图




3.完成之后,右键 所有任务 申请新证书,做ldap 连接ad域只需要勾选域控制器即可

‘’
4.申请成功,右键所有任务 导出 不要私钥 base64 编码


5.用远程桌面的连接 高级勾选即可导出到桌面位置

6.导出根域控证书(ldap需要域控根证书以及域名证书)

域控根证书在下方安装证书服务就会添加到受信任的根证书办法机构

到其中找到 跟上面申请的证书导出方法一致

2)导入JAVA库

导入
keytool -import -file D:
b.cer -keystore "%JAVA_HOME%jrelibsecuritycacerts" -alias nb -storepass changeit
keytool -import -file D:
b12.cer -keystore "%JAVA_HOME%jrelibsecuritycacerts" -alias nb12 -storepass changeit

查看
keytool -list -keystore "%JAVA_HOME%jrelibsecuritycacerts"| findstr /i nb

删除

keytool -delete -alias parent -keystore "%JAVA_HOME%jrelibsecuritycacerts" -storepass changeit
keytool -delete -alias parent -keystore "%JAVA_HOME%jrelibsecuritycacerts" -storepass changeit

最终导入到本地的两个证书如下图

证书路径不是如下图的话 可以先将域控根证书安装到本地计算机

五、修改密码后仍旧可使用旧密码认证

用户通过程序改掉自己AD账号的密码之后,新密码立即可用,但旧密码也同样可用,新旧密码同时存在的时间为:Win03中是60分钟、Win08和Win2012中则变为5分钟。

为什么会存在一个5分钟的时间?

根据网络资料查询,应该是由发起密码更改的那台DC,为旧密码开启了一个最后生存时间,而这个时间,就是5分钟整.这个5分钟就是为了防止AD同步延时问题,防止DC数量比较多时,用户登录所在的站点内还没有成功的更新到密码的修改的情况。这样,即使新密码没有生效,旧密码依然可用。

解决方法:

1、打开域服务器注册表(regedit)
2、进入目录HKEY_LOCAL_MACHINE/SYSTEM/CurrentControlSet/Control/Lsa
3、在右侧窗口中,右键新建“DWORD值”,名称为OldPasswordAllowedPeriod,回车
4、右键OldPasswordAllowedPeriod,修改数值,该数值即为旧密码失效时间,单位分钟,改为0,则即时失效

image

六、参考资料(这是被我飘过的巨人们,感恩!!)

WINDOWS SERVER 2012证书服务安装配置:https://blog.csdn.net/lcl_xiaowugui/article/details/78746076

AD修改密码延迟的问题:https://blog.csdn.net/jl19861101/article/details/5641649

新旧密码同时存在官网解决方案:https://docs.microsoft.com/en-us/troubleshoot/windows-server/windows-security/new-setting-modifies-ntlm-network-authentication

【windows】windows 2012 r2证书服务安装与高级配置以及如何导出证书(java通过ldap走ssl修改查询操作):https://blog.csdn.net/qq_41207282/article/details/97133887#comments

对接LDAP错误码及常见处理方法:https://blog.90.vc/archives/273

AD域服务器免密认证:https://www.cnblogs.com/huanghongbo/p/12409209.html

原文地址:https://www.cnblogs.com/alfredinchange/p/13691748.html