Blog

Interaktion mit ActiveDirectory durch Powershell über SSH mit Java

Alexander Bij

Alexander Bij

Aktualisiert Oktober 22, 2025
10 Minuten

Active Directory ist die Implementierung von LDAP durch Microsoft. LDAP wird häufig für die Authentifizierung und Autorisierung von Benutzern verwendet. Es gibt Möglichkeiten, das LDAP direkt von Java aus ohne die Powershell zu ändern. Mit Spring-LDAP in Kombination mit Spring-ODM können Sie das AD (ActiveDirectory) lesen, schreiben und abfragen. Uns 'Entwicklern' gefällt das, aber dem Betrieb nicht. Die Verwendung einer solchen API ist Low-Level und Sie müssen bei der Manipulation des AD darauf achten, dass Sie die richtigen Felder und Attribute setzen:

  • Sie müssen die erforderlichen Felder kennen (unicodePwd, anstelle von userPassword)
  • Sie müssen die verschlüsselten Felder wie userAccountControl, Passwort in Bytes verstehen
  • Sie als Entwickler sind für die Interaktion verantwortlich, und Sie können die AD bescheißen.
  • Sie müssen Probleme beheben, wenn Sie AD ändern, aktualisieren oder upgraden können.

Das Wichtigste in unserem Fall ist, dass die Operatoren die Eigentümer von AD sind und die Verantwortung dafür tragen. Sie empfehlen dringend, die Powershell zu verwenden, wenn Sie Benutzerkonten ändern. Die Powershell wird auf dem Domain Controller ausgeführt, auf dem sich AD befindet, und kann als API verwendet werden. Sie läuft nur unter Windows, aber wir wollen unsere Java-Software auf Linux hosten, also haben wir uns für die Kommunikation über SSH entschieden.

1. Sie benötigen einen SSH-Server auf dem Domain Controller . Der Sinn des SSH-Servers besteht darin, eine sichere Verbindung herzustellen und wenn der Client verbunden ist, nicht cmd.exe, sondern powershell.exe zu verwenden. In allen Produkten ist es möglich, cmd zu ändern -> powershell. Wir haben verschiedene SSH-Server-Anwendungen ausprobiert. Hier ist eine Liste, die wir ausprobiert haben und die unsere Erfahrungen enthält:

  • Cygwin + OpenSSH, kostenlose Do-it-yourself-Lösung. Nicht beliebt bei Windows-Betreibern.
  • FreeSSHd, kostenlos, funktionierte aber nach der Installation nicht.
  • Powershellinside funktionierte, aber wir hatten Probleme mit der Interaktion über SSH. Wir meldeten das Problem und es wurde innerhalb einer Woche behoben. In der Zwischenzeit haben wir andere Produkte ausprobiert.
  • Bitvise ssh-server, dies wurde unsere Wahl, es hat eine GUI und funktioniert einfach.

Nachdem Sie das Tool eingerichtet haben, setzen Sie sich neben den Operator, bis Sie sich mit PuTTy verbinden können, melden Sie sich an und sehen Sie die Powershell. Stellen Sie sicher, dass das Modul ActiveDirectory verfügbar ist, indem Sie in der Powershell eingeben: import-module ActiveDirectory

2. Verbinden Sie sich von Java über SSH: Eine gute Java-API ist Jsch. Neben der Standardversion gibt es eine umgeschriebene Version von vngx:"Sie wurde auf Java 6 mit den neuesten Sprachfunktionen und verbesserter Codeklarheit aktualisiert." In diesem Beispiel werde ich die umgeschriebene Version von Jsch verwenden, beide sind in der Mavenrepo verfügbar. Thrust the server by IP? Wenn Sie sich mit PuTTy verbinden, werden Sie gefragt, ob Sie den Fingerabdruck des Servers thrustet: The server's host key is not cached in the registry. You have no guarantee that the server is the computer you think it is. The server's key fingerprint is: ssh-rsa 1024 7b:e5:6f:a7:f4:f9:81:62:5c:e3:1f:bf:8b:57:6c:5a If you trust this host, hit Yes to add the key to PuTTY's cache and carry on connecting. If you want to carry on connecting just once, without adding the key to the cache, hit No. If you do not trust this host, hit Cancel to abandon the connection. Bei der Verbindung mit Jsch können Sie dasselbe tun. Sie müssen die Datei knownHostFile angeben, die die Fingerabdrücke der Server enthält, die Sie "kennen". Unter Linux finden Sie die Datei unter ~/.ssh/known_hosts. Unter Windows speichert PuTTy die Fingerabdrücke in der Registry. Ich habe Cygwin verwendet und mich mit ssh mit dem Domänencontroller verbunden und in einem Unterordner von Cygwin erscheint die Datei known_hosts. In meinem Code-Beispiel prüfe ich, ob die Datei gesetzt ist, andernfalls überprüfe ich den Fingerabdruck des Servers beim Verbinden nicht. Jsch channel type shell Wir möchten mit der Konsole interagieren, einen Befehl schreiben und die Ergebnisnachricht überprüfen. Wenn Sie den Kanaltyp shell verwenden, wird er wie ein Shell-Fenster mit einer Breite und Höhe behandelt. Die Lesezeichen vom Server enthalten Steuerzeichen für Position, Eingabe, Farben usw. Ich habe keine Möglichkeit gefunden, die Sitzung mit der Einstellung setPty(false) zum Laufen zu bringen. Mit dieser Einstellung funktioniert nur der erste Lesevorgang ohne Steuerzeichen, aber danach werden keine weiteren Eingaben mehr gelesen. Um mit dem ActiveDirectory zu interagieren, muss das entsprechende Modul zuerst geladen werden. Ich habe die Erfahrung gemacht, dass dies eine Weile dauert (~1,5 Sekunden). Dies muss nur einmal geschehen, was Sie nicht bei jedem Befehl, den Sie ausführen, wollen. Damit es gut funktioniert, habe ich mich entschieden, 1 Sitzung und 1 Kanal offen zu halten und das Modul nur beim ersten Mal zu laden. 3. Erstellen Sie einen Benutzer! und entfernen Sie ihn. Alles, was wir tun müssen, ist, den 'New-ADUser'-Befehl an die Powershell zu senden und AD die Arbeit machen zu lassen. Wir wollen überprüfen, ob der Befehl erfolgreich war oder die Fehlermeldung verwenden. Mit dem Befehl $? können Sie den Status des letzten Befehls überprüfen. True -> Oke und False -> Es ist etwas passiert... Wenn der Befehl nicht erfolgreich war, verwende ich die vorherige Nachricht des Servers, die den Fehler enthält. Dann wird eine neue PowershellException mit der vorherigen Nachricht ausgelöst. In der PowershellException werden der Befehl und die ursächliche Meldung in ein lesbares Format geparst, ohne die Steuerzeichen. Sie können versuchen, den Befehl zunächst in der Powershell oder in PuTTy auszuführen (mit geladenem Modul ActiveDirectory!): New-ADUser -SamAccountName "abij" -Name "abij" -DisplayName "Alexander" -EmailAddress "Fake@Email.com" -AccountPassword (ConvertTo-SecureString -AsPlainText "Welkcome01" -Force) -ChangePasswordAtLogon $false -PasswordNeverExpires $true -Enabled $true -path "ou=Users" Remove-ADUser -Identity "abij" -Confirm:$false Mit dem Modul ActiveDirectory können Sie eine Menge tun, prüfen Sie alle verfügbaren Optionen 4. Der Code Besuchen Sie GitHub - abij/ad-powershell-over-ssh für das vollständige Codebeispiel. Verwenden Sie Maven, um die Abhängigkeiten zu verwalten und dann können Sie den PowershellServiceIntegrationTest verwenden, um mit Ihrem AD zu interagieren. [java title="PowershellService.java"] public class PowershellService { private String adUserPath; private static final String CREATE_USER = "New-ADUser -SamAccountName"${samAccountName}"" + " -Name "${name}"" + " -DisplayName "${displayname}"" + " -EmailAddress "${email}"" + " -AccountPassword (ConvertTo-SecureString -AsPlainText "${passwordPlain}" -Force)" + " -ChangePasswordAtLogon $false -PasswordNeverExpires $true -Enabled $true" + " -pfad "${adUserPath}""; private static final String REMOVE_USER = "Remove-ADUser -Identität "${samAccountName}" -Bestätigung:$false"; public void createUser(String displayname, String email, String username, String password) throws JSchException, IOException { Karte<String, String> model = new LinkedHashMap<String, String>(); model.put("samAccountName", username); model.put("name", username); model.put("Passwort", Passwort); model.put("displayname", displayname); model.put("email", email); model.put("passwordPlain", password); model.put("adUserPath", adUserPath); PowershellSession.getInstance().execute(commando(CREATE_USER, model)); } public void removeUser(String username) throws JSchException, IOException { Karte<String, String> model = new LinkedHashMap<String, String>(); model.put("samAccountName", username); PowershellSession.getInstance().execute(commando(REMOVE_USER, model)); } /**

  • Füllen Sie den angegebenen String mit dem Modell.
  • Die Vorlage "Hi ${foo}", mit dem Modell ("foo" -> "bar") wird in: "Hallo bar".
  • @wirft IllegalStateException, wenn nicht alle Ersatzfelder ausgefüllt sind.
  • @see StringUtils#replaceEach(String, String[], String[]) */ private String commando(String Vorlage, Karte<String, String> Modell) { Liste<Zeichenfolge> searchList = new ArrayList<Zeichenfolge>(model.size()); for (String key : model.keySet()) { searchList.add("${" + Schlüssel + "}"); } String[] replaceList = model.values().toArray(new String[model.size()]); String command = StringUtils.replaceEach(template, searchList.toArray(new String[model.size()]), replaceList); //TODO Schöner: Mustervergleich ${.+} if (befehl.enthält("${")) { throw new IllegalStateException("Befehl enthält unausgefüllte Parameter: " + Befehl); } Befehl zurückgeben; } public void setAdUserPath(String adUserPath) { this.adUserPath = adUserPath; } } [/java] Und die entsprechende Sitzung: [java title="PowershellSession.java"] /**
    • PowerShell-Sitzung als Singleton. Mit der Synchronisierung um Execute, die es ermöglicht
    • dies bedeutet, dass Sie jeweils 1 Befehl über 1 Kanal ausführen. Mit Jsch können Sie
    • mehrere Kanäle öffnen und Befehle parallel ausführen.
    • Nichts wird Sie davon abhalten, dieses Beispiel an Ihre Bedürfnisse anzupassen. / public class PowershellSession { private static final Logger LOG = LoggerFactory.getLogger(PowershellSession.class); // Es gibt nur 1 Sitzung, mit getInstance() können Sie sie verwenden. private PowershellSession() {} private static final PowershellSession INSTANCE = new PowershellSession(); public static PowershellSession getInstance() { return INSTANCE; } private static final int DEFAULT_BUFFER_SIZE = 1024; private static final Charset UTF8 = Charset.forName("UTF-8"); private static final int AD_MODULE_LOADINGTIME = 1500; // 1,5 sec Wartezeit zum Laden des AD-Moduls. // erforderliche Einstellungen private String username; private byte[] password; private String host; // optionale Einstellungen mit Standardwerten: private int port = 22; private String knownHostFile = null; // Keine knownHostFile -> prüfen Sie den Fingerabdruck des Servers nicht. private int socketTimeout = 5 1000;// 5s. private int readTimeout = 3 * 1000; // 3s maximale Wartezeit & Lesen vom Server private int pollTimeout = 20; // 20 ms vor erneutem Lesen. private Session Sitzung; private ChannelShell shell; private InputStream fromServer; private OutputStream toServer; public void init() throws JSchException { JSch jSch = JSch.getInstance(); SessionConfig config = new SessionConfig(); config.setProperty(SSHConfigConstants.PREFFERED_AUTHS, "password"); if (knownHostFile == null) { config.setProperty(SSHConfigConstants.STRICT_HOST_KEY_CHECKING, "no"); } session = jSch.createSession(username, host, port, config); } public void disconnect() { if (shell != null && shell.isConnected()) { shell.disconnect(); LOG.debug("Die Kanal-Shell ist nicht mehr verbunden."); } if (session != null && session.isConnected()) { session.disconnect(); LOG.debug("Die Verbindung wurde unterbrochen."); } } public synchronized void execute(String command) throws JSchException, IOException { checkConnection(); writeToServer(command); verifyCommandSucced(); } // * ** Helper-Methoden ** private void checkConnection() throws JSchException, IOException { if (!session.isConnected()) { session.connect(socketTimeout, Passwort); LOG.debug("Sitzung verbunden mit Host: {}", session.getHost()); } sonst { LOG.debug("Die Sitzung ist immer noch verbunden."); } if (shell == null || !shell.isConnected()) { shell = session.openChannel(ChannelType.SHELL); shell.connect(socketTimeout); LOG.debug("ChannelShell ist verbunden."); fromServer = shell.getInputStream(); toServer = shell.getOutputStream(); readFromServer(); // Lesen Sie die ersten Daten: Windows PowerShell ... Alle Rechte vorbehalten. loadModuleActiveDirectory(); } sonst { LOG.debug("Kanal (Shell) noch offen."); } } private void loadModuleActiveDirectory() throws IOException { LOG.debug("import-module ActiveDirectory..."); writeToServer("import-module ActiveDirectory"); sleep(AD_MODULE_LOADINGTIME, "Nach dem Laden des Moduls ActiveDirectory konnte nicht geschlafen werden."); verifyCommandSucced(); } private void writeToServer(String command) throws IOException { String commandWithEnter = command; if (!command.endsWith("rn")) { commandWithEnter += "rn"; } toServer.write((commandWithEnter).getBytes(UTF8)); toServer.flush(); } private String readFromServer() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); byte[] buffer = new byte[DEFAULT_BUFFER_SIZE] String linePrompt = "" + username + ">"; // zeigt an, dass die Konsole einen Zeilenumbruch hat, hören Sie auf zu lesen. long timeout = System.currentTimeMillis() + readTimeout; while (System.currentTimeMillis() < Zeitüberschreitung && !Util.byte2str(bos.toByteArray()).contains(linePrompt)) { while (fromServer.available() > 0) { int count = fromServer.read(buffer, 0, DEFAULT_BUFFER_SIZE); wenn (Anzahl >= 0) { bos.write(buffer, 0, count); } sonst { Pause; } } if (shell.isClosed()) { Pause; } // Drehen Sie sich nicht wie verrückt durch die while-Schleife sleep(pollTimeout, "Der Schlaf zwischen den Lesevorgängen mit pollTimeout ist fehlgeschlagen: " + pollTimeout); } String result = bos.toString("UTF-8"); LOG.debug("vom Server gelesen:n{}", Ergebnis); Ergebnis zurückgeben; } /**
  • @throws PowershellException Wenn die Nachricht nicht erfolgreich ausgeführt wurde, mit Detailinformationen. */ private void verifyCommandSucced() throws IOException { String message = readFromServer(); writeToServer("$?"); // Fragt den Powershell-Status des letzten Befehls ab? if (!readFromServer().contains("True")) { throw new PowershellException(Nachricht); } } private void sleep(long timeout, String msg) { versuchen { Thread.sleep(timeout); } catch (Exception ee) { LOG.debug(msg); } } @Override protected void finalize() throws Throwable { super.finalize(); disconnect(); // Immer die Verbindung trennen! } // * ** Setzer ** public void setUsername(String username) {this.username = username;} public void setPassword(String password) {this.password = password.getBytes(UTF8);} public void setHost(String host) {this.host = host;} public void setPort(int port) {this.port = port;} public void setKnownHostFile(String knownHostFile) {this.knownHostFile = knownHostFile;} public void setSocketTimeout(int socketTimeout) {this.socketTimeout = socketTimeout;} public void setSession(Session session) {this.session = session;} public void setReadTimeout(int readTimeout) {this.readTimeout = readTimeout;} public void setPollTimeout(int pollTimeout) {this.pollTimeout = pollTimeout;} } [/java]

Verfasst von

Alexander Bij

Contact

Let’s discuss how we can support your journey.