1. 程式人生 > 實用技巧 >信步漫談之AD域伺服器—LDAPS認證改密

信步漫談之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(), "[email protected]");
        //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("操作返回碼:{}\t操作返回結果:{}", adReturnCode.getCode(), adReturnCode.getMsg());
        } catch (AdException e){
            logger.error("異常返回碼:{}\t異常返回結果:{}", e.getCode().getCode(), e.getCode().getMsg());
        }
    }

}
Main

3)修改hosts

檔案路徑:C:\Windows\System32\drivers\etc\hosts

新增IP和域名,域名為證書上的域名,這點很關鍵

4)證書檔案

引用的cacerts見下方證書匯出的證書,匯入到JAVA庫中

三、域伺服器安裝配置證書

安裝

選擇新增角色和功能

選擇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:\nb.cer -keystore "%JAVA_HOME%\jre\lib\security\cacerts" -alias nb -storepass changeit
keytool -import -file D:\nb12.cer -keystore "%JAVA_HOME%\jre\lib\security\cacerts" -alias nb12 -storepass changeit

檢視
keytool -list -keystore "%JAVA_HOME%\jre\lib\security\cacerts"| findstr /i nb

刪除

keytool -delete -alias parent -keystore "%JAVA_HOME%\jre\lib\security\cacerts" -storepass changeit
keytool -delete -alias parent -keystore "%JAVA_HOME%\jre\lib\security\cacerts" -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,則即時失效


六、參考資料(這是被我飄的巨人們,感恩!!)

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