先决条件:
1.Java应用所在的服务器开通到ad01.acme.com 636端口的网络策略
2.AD管理员通过AD证书服务配置了LDAPS,并侦听636端口
3.创建一个AD用户账号666,用于修改其他用户的密码
用法:
传入两个参数:1.用户名 2.新密码
java -jar modifyADpassword.jar zhangsan NewPa4s#8281
开启SSL调试,诊断潜在的证书问题
java -Djavax.net.debug=ssl:handshake:verbose -jar modifyADpassword.jar
modifyADpassword.jar源码:
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.*;
import javax.net.ssl.*;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.InetAddress;
import java.net.Socket;
import java.security.cert.X509Certificate;
import java.util.Hashtable;
public class ModifyAdPassword {
public static void main(String[] args) {
// --- 检查命令行参数 ---
if (args.length != 2) {
System.out.println("Usage: java ModifyAdPassword <employeeId> <newPassword>");
System.out.println("Example: java ModifyAdPassword 888 MyNewP@ssw0rd!");
return;
}
String employeeId = args[0];
String newPassword = args[1];
System.out.println("Searching for user with employeeId (sAMAccountName): " + employeeId);
// --- 1. 配置连接信息 ---
String ldapHost = "ad01.acme.com"; // 您的AD服务器地址
String adminUser = "666@acme.com"; // 用于登录修改密码的账户 (UPN格式)
String adminPassword = "YOUR_666_PASSWORD"; // 【必须替换】666账户的密码
// --- 2. 设置JNDI环境参数 ---
Hashtable<String, Object> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, "ldaps://" + ldapHost + ":636");
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, adminUser);
env.put(Context.SECURITY_CREDENTIALS, adminPassword);
// --- 3. 【核心】为JNDI连接指定一个信任所有证书的SSLSocketFactory ---
// 这解决了 "unable to verify the first certificate" 的问题
try {
// 告诉JNDI使用我们自定义的Socket工厂类
env.put("java.naming.ldap.factory.socket", TrustAllSSLSocketFactory.class.getName());
} catch (Exception e) {
System.err.println("Failed to set up custom SSL Socket Factory.");
e.printStackTrace();
return;
}
DirContext ctx = null;
try {
// --- 4. 连接到AD服务器 ---
System.out.println("Connecting to AD server...");
ctx = new InitialDirContext(env);
System.out.println("Connection successful.");
// --- 5. 搜索用户DN ---
String userToModifyDN = findUserDNBySamAccountName(ctx, employeeId);
if (userToModifyDN == null) {
System.err.println("Error: User with sAMAccountName '" + employeeId + "' not found.");
return;
}
System.out.println("Found user DN: " + userToModifyDN);
// --- 6. 准备并执行密码修改 ---
// AD要求密码必须是UTF-16LE编码,并且用双引号括起来
byte[] newQuotedPassword;
try {
// 【修复】使用 try-catch 块处理 UnsupportedEncodingException
newQuotedPassword = ("\"" + newPassword + "\"").getBytes("UTF-16LE");
} catch (UnsupportedEncodingException e) {
System.err.println("FATAL ERROR: The system does not support the required UTF-16LE encoding.");
e.printStackTrace();
return; // 如果编码不支持,程序无法继续,直接退出
}
Attribute passwordAttribute = new BasicAttribute("unicodePwd", newQuotedPassword);
ModificationItem[] mods = new ModificationItem[1];
mods[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, passwordAttribute);
System.out.println("Modifying password...");
ctx.modifyAttributes(userToModifyDN, mods);
System.out.println("Password for user '" + employeeId + "' was changed successfully!");
} catch (NamingException e) {
System.err.println("An error occurred during LDAP operation.");
e.printStackTrace();
// 常见错误:
// - 50: 权限不足 (666账户没有权限修改密码)
// - 19: 约束冲突 (新密码不符合域的密码策略)
// - 32: 没有此对象 (userToModifyDN 找不到)
} finally {
// --- 7. 关闭连接 ---
if (ctx != null) {
try {
ctx.close();
System.out.println("Connection closed.");
} catch (NamingException e) {
e.printStackTrace();
}
}
}
}
/**
* 根据sAMAccountName查找用户的完整Distinguished Name (DN)。
*/
private static String findUserDNBySamAccountName(DirContext ctx, String samAccountName) throws NamingException {
String searchBase = "DC=acme,DC=com"; // 搜索的根路径,通常为域的DN
String searchFilter = String.format("(sAMAccountName=%s)", samAccountName);
SearchControls controls = new SearchControls();
controls.setSearchScope(SearchControls.SUBTREE_SCOPE);
controls.setReturningAttributes(new String[0]); // 只需要DN,不需要其他属性
NamingEnumeration<SearchResult> results = ctx.search(searchBase, searchFilter, controls);
try {
if (results.hasMore()) {
SearchResult result = results.next();
return result.getNameInNamespace();
}
} finally {
results.close();
}
return null;
}
/**
* 一个自定义的SSLSocketFactory,它会创建一个信任所有服务器证书的SSL Socket。
* 这是为了解决LDAPS连接中因证书不被信任而导致的连接失败问题。
* 注意:此类仅用于测试环境,因为它会禁用SSL/TLS的安全检查。
*/
static class TrustAllSSLSocketFactory extends SSLSocketFactory {
private final SSLSocketFactory delegate;
public TrustAllSSLSocketFactory() throws Exception {
// 创建一个信任所有证书的SSLContext
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, new TrustManager[]{new X509TrustManager() {
public X509Certificate[] getAcceptedIssuers() { return null; }
public void checkClientTrusted(X509Certificate[] certs, String authType) {}
public void checkServerTrusted(X509Certificate[] certs, String authType) {}
}}, new java.security.SecureRandom());
delegate = context.getSocketFactory();
}
// 以下方法都是委托给内部的SSLSocketFactory
@Override
public String[] getDefaultCipherSuites() { return delegate.getDefaultCipherSuites(); }
@Override
public String[] getSupportedCipherSuites() { return delegate.getSupportedCipherSuites(); }
@Override
public Socket createSocket(Socket s, String host, int port, boolean autoClose) throws IOException {
return delegate.createSocket(s, host, port, autoClose);
}
@Override
public Socket createSocket(String host, int port) throws IOException {
return delegate.createSocket(host, port);
}
@Override
public Socket createSocket(String host, int port, InetAddress localHost, int localPort) throws IOException {
return delegate.createSocket(host, port, localHost, localPort);
}
@Override
public Socket createSocket(InetAddress host, int port) throws IOException {
return delegate.createSocket(host, port);
}
@Override
public Socket createSocket(InetAddress address, int port, InetAddress localAddress, int localPort) throws IOException {
return delegate.createSocket(address, port, localAddress, localPort);
}
}
}