Blog - Artikel

Windows-Authentifizierung in Java

von Stefan Kowski (Kommentare: 0)

Wenn man den Nutzerkreis einer Anwendung einschränken möchte, ist eine Anmeldepflicht die erste Lösung. Der Anwender gibt seinen Benutzernamen und ein Kennwort ein, nach erfolgreicher Prüfung der Werte kann das Programm verwendet werden.

Da die Speicherung von Anmeldedaten viele Probleme macht, stelle ich in diesem Artikel eine einfache Lösung vor: die Delegation der Anmeldefunktion an einen Windows-Domänencontroller. Der Anwender meldet sich dabei mit seinem Windows-Benutzernamen und -Kennwort an, die Prüfung erfolgt durch den Windows-Server.

Welches Protokoll verwende ich für die Anmeldung?

„When in Rome, do as the Romans do.‟

Ich nutze das Protokoll, das Windows selbst für seine Domänenanmeldung verwendet: Kerberos. Kerberos ist ein Challenge-Response-Verfahren, das für den Einsatz in unsicheren Netzwerken entworfen wurde. Es werden keine Kennwörter im Netzwerk übertragen, auch das Abgreifen von Anmeldepaketen für Replay-Attacken ist wirkungslos (mehr Informationen zum Kerberos-Protokoll).

Als Java-API für meine Implementierung verwende ich JAAS (Java Authentication and Authorization Service). Meine Implementierung ist konfigurationsfrei, außer dem Namen der Windows-Domäne werden keine weiteren Daten benötigt. Es gibt daher keine JAAS-Konfigurationsdatei, die angepasst oder ausgeliefert werden müsste.

Man könnte die Anmeldung auch mit Hilfe eines LDAP-Bind durchführen. Davon ist aber abzuraten, da das LDAP-Protokoll Kennwörter gern im Klartext überträgt und eine SSL-Verbindung (ldaps:) daher zwingend erforderlich ist. Zusätzlich kann es erheblichen Konfigurationsaufwand geben, wenn nicht die LDAP-Standardports genutzt werden oder SSL-Zertifikate für die LDAP-Client-Authentifizierung benötigt werden (je nachdem, was die örtliche Netzwerkadministration verlangt).

Wie finde ich den zuständigen Windows-Server?

Die verfügbaren Windows-Server, die Anmeldefunktionen durchführen können, sind im DNS registriert. Wir ermitteln die Server über eine DNS-Abfrage.

/**
  * Get Active Directory domain controllers.
  *
  * Shell example: nslookup -type=SRV _ldap._tcp.dc._msdcs.mydomain.local
  *
  * @param domain
  *            Domain name (e.g. "mydomain.local")
  * @return Domain controllers (list may be empty)
  * @throws NamingException
  */
 private static Collection<InetSocketAddress> getDomainControllers(String domain) throws NamingException {

     final String typeSRV = "SRV";
     final String[] types = new String[] { typeSRV };

     DirContext ctx = new InitialDirContext();

     Attributes attributes = ctx.getAttributes("dns:/_ldap._tcp.dc._msdcs." + domain, types);
     if (attributes.get(typeSRV) == null) {
         return Collections.emptyList();
     }

     NamingEnumeration<?> e = attributes.get(typeSRV).getAll();
     TreeMap<Integer, InetSocketAddress> result = new TreeMap<>();

     while (e.hasMoreElements()) {

         String line = (String) e.nextElement();

         // The line is: priority weight port host
         String[] parts = line.split("\\s+");

         int prio = Integer.parseInt(parts[0]);
         int port = Integer.parseInt(parts[2]);
         String host = parts[3];

         result.put(prio, new InetSocketAddress(host, port));
     }

     return result.values();
 }

In großen Domänen liefert die Funktion gerne mal 20 oder mehr Server zurück. Da meist ohnehin eine DNS-Lastverteilung stattfindet und unsere Funktion vermutlich nur selten aufgerufen wird, verzichte ich im weiteren Programm auf die Auswertung der Server-Prioritäten und nutze einfach den ersten gelieferten Server.

JAAS konfigurieren

Das JAAS-API benötigt eine Konfiguration. Wir aber nicht, daher implementieren wir einfach eine leere Konfigurationsklasse.

/**
  * JAAS configuration.
  */
 public static class StaticConfiguration extends Configuration {

     final AppConfigurationEntry staticConfigEntry;

     public StaticConfiguration(String loginModuleName) {

         Map<String, ?> options = new HashMap<>();
         staticConfigEntry = new AppConfigurationEntry(loginModuleName,
                 AppConfigurationEntry.LoginModuleControlFlag.REQUIRED, options);
     }

     @Override
     public AppConfigurationEntry[] getAppConfigurationEntry(String name) {

         return new AppConfigurationEntry[] { staticConfigEntry };
     }
 }

Zur JAAS-Konfiguration gehört ein Handler, der üblicherweise genutzt wird, um interaktiv Anmeldedaten vom Benutzer abzufragen. Bei uns kommen die Daten aber per API, so dass wir einen Handler implementieren, der diese übergebenen Werte an den JAAS übergibt.

/**
  * JAAS callback handler.
  */
 public static class StaticCallbackHandler implements CallbackHandler {

     /**
      * Constructor.
      *
      * @param username
      *            Windows user name
      * @param password
      *            Windows password
      */
     public StaticCallbackHandler(String username, String password) {

         this.username = username;
         this.password = password;
     }

     @Override
     public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {

         for (int i = 0; i < callbacks.length; i++) {

             if (callbacks[i] instanceof TextOutputCallback) {

                 // unused

             } else if (callbacks[i] instanceof NameCallback) {

                 NameCallback nc = NameCallback.class.cast(callbacks[i]);
                 nc.setName(username);

             } else if (callbacks[i] instanceof PasswordCallback) {

                 PasswordCallback pc = PasswordCallback.class.cast(callbacks[i]);
                 pc.setPassword(password.toCharArray());

             } else {

                 throw new UnsupportedCallbackException(callbacks[i], "Unrecognized Callback");
             }
         }
     }

     /** User name. */
     private String username;

     /** Password. */
     private String password;
 }

Windows-Anmeldung durchführen

Das folgende Code-Beispiel enthält die eigentliche Anmeldefunktion. Zuerst wird ein passender Windows-Server gesucht, anschließend wird das JAAS-Subsystem für Kerberos konfiguriert. Mit Hilfe des LoginContext-Objekts wird dann die eigentliche Anmeldung durchgeführt.

/**
  * Constructor.
  *
  * @param domainName
  *            domain name (e.g. "mydomain.local")
  */
 public ActiveDirectoryAuthentication(String domainName) {

     this.domainName = domainName;
 }

 /**
  * Authenticate user.
  *
  * @param username
  *            Windows user name
  * @param password
  *            Windows password
  * @throws ValidationException
  */
 public void authenticate(String username, String password) throws ValidationException {

     LoginContext lc;
     try {

         // get domain controller for login
         Collection<InetSocketAddress> result = getDomainControllers(domainName);
         if (result.isEmpty()) {
             throw new ValidationException("No domain controllers found for domain " + domainName);
         }
         String loginServer = result.iterator().next().getHostString();
         System.setProperty("java.security.krb5.realm", domainName.toUpperCase());
         System.setProperty("java.security.krb5.kdc", loginServer);

         // perform login
         lc = new LoginContext("", null, new StaticCallbackHandler(username, password),
                 new StaticConfiguration("com.sun.security.auth.module.Krb5LoginModule"));
         lc.login();

         // logout (we want to check the password only)
         lc.logout();

     } catch (LoginException le) {

         // error
         throw new ValidationException("Authentication failed: " + le.getMessage(), le);

     } catch (SecurityException se) {

         // error
         throw new ValidationException("Authentication failed: " + se.getMessage(), se);

     } catch (NamingException ne) {

         // error
         throw new ValidationException("Authentication failed: " + ne.getMessage(), ne);
     }
 }

 /** Windows domain name. */
 private String domainName;
}

Zusammenfassung

Das Beispiel zeigt, dass eine Windows-Authentifizierung in Java leicht zu implementieren ist. Da es keine aufwendigen Konfigurationen und Abhängigkeiten gibt, kann das Code-Beispiel in ganz unterschiedlichen Umgebungen eingesetzt werden.

Zurück