15.2.4 | Der Java Authentication and Authorization Service (JAAS) |
Mit demJava Authentication and Authorization Service (JAAS) steht in Java ein API zur Anmeldung von Benutzern an ein Java-Programm zur Verfügung. Die Architektur von JAAS wurde wesentlich von den bei Unix bekannten Pluggable Authentication Modules (PAM) geprägt. Die Idee von PAM ist, die Authentifzierung nicht »hart« in der Anwendung zu kodieren, sondern in externe Module mit einheitlicher Schnittstelle auszulagern. Welches Modul zum Einsatz kommt, wird extern konfiguriert. Auf diese Weise kann das Authentifizierungsverfahren sehr einfach durch einen Austausch des PAM-Moduls gewechselt werden, ohne Änderungen an der Anwendung vornehmen zu müssen. Das gleiche gilt auch für die Authentifizierungsmodule von JAAS. Im Wesentlichen bietet JAAS die folgenden Vorteile:
Der nächste Abschnitt behandelt die Benutzung der Standard-Module von JAAS sowie ein Beispiel für ein anwenderdefiniertes Modul. Am Schluss wird gezeigt, wie man eine authentifizierte Benutzerkennung verwenden kann, um erweiterte Privilegien zu erhalten.
- Flexible Authentifizierung über einfach auswechselbare und kaskadierbare Authentifizierungsmodule
- Möglichkeit eines Single-Sign-On an der Anwendung, in dem beispielsweise die Kennung des Betriebsystem-Benutzers, unter dem die Anwendung ausgeführt wird, ohne weitere Benutzerinteraktion an Java »durchgereicht« werden kann.
- Mit der Autorisierungskomponente von JAAS ist es schließlich möglich, die Regeln in den zuvor beschriebenen Sicherheits-Policies an eine Benutzeridentität zu knüpfen. Das heißt, es kommt nicht nur darauf an, woher ein Bytecode stammt, sondern auch wer ihn ausführt, um bestimmte Privilegien zu erhalten.
Das Prinzip von JAAS ist, dass dem Benutzer, der die Anwendung ausführt (in der JAAS-Terminologie »Subject« genannt) eine oder mehrere authentifizierte Benutzerkennungen (als »Principals« bezeichnet) zugeordnet werden. Ein Principal kann z. B. eine erfolgreiche Anmeldung an Windows sein, die der Benutzer vor dem Starten der Anwendung durchgeführt hat. In diesem Fall verknüpft das entsprechende JAAS-Login-Modul das Subject unter anderem mit einem Exemplar der Klasse NTUserPrincipal, das die Windows-Benutzer-ID darstellt. Daneben stellt dieses Modul noch weitere Principals aus (z. B. für die Domäne und die Gruppen, denen der Benutzer angehört).
Die Schnittstelle zwischen JAAS und einem Programm, das JAAS benutzt, ist die Klasse LoginContext aus dem Paket javax.security.auth.login. Ein LoginContext benötigt zwei Informationen:Für die Verwendung von JAAS sind daher drei Schritte durchzuführen:
- Den Namen des Kontextes. Dieser Name identifiziert die Login-Modulkonfiguration, die für den Kontext verwendet werden soll. Diese Modulkonfiguration steht in einer Datei, die durch die System-Property java.security.auth.login.config bezeichnet wird.
- Ein Exemplar der Klasse CallbackHandler. Dieses Objekt dient dazu, Benutzerinteraktionen durchzuführen, die die Module benötigen. Eine solche Interaktion könnte die Abfrage eines Passworts sein. Da die Form solcher Abfragen stark von der Anwendung abhängt (z. B. Kommandozeile oder grafischer Dialog), sind sie nicht in den Modulen selbst kodiert, sondern werden über einen CallbackHandler abgewickelt.
Ein einfaches Beispiel für die Modulkonfiguration könnte so aussehen:
- Es ist eine Konfigurationsdatei zu erstellen, in der das gewünschte Authentifizierungsmodul eingetragen wird.
- Es muss eine Unterklasse von CallbackHandler erstellt werden, in der die Benutzerinteraktion durchgeführt wird.
- Es muss ein LoginContext erzeugt werden, mit dessen Methode login() das eigentliche Login durchgeführt werden kann.
Demo { // Für Windows: com.sun.security.auth.module.NTLoginModule required debug=false; // Für Unix: // com.sun.security.auth.module.UnixLoginModule required debug=false; };Ein Eintrag beginnt mit den Namen der Konfiguration, dem die gewünschten Module in geschweiften Klammern eingefasst folgen. Wichtig ist, dass jeder Eintrag und auch das Klammerpaar mit einem Semikolon abgeschlossen werden. Wie in dem Beispiel zu sehen ist, können den Modulen auch Parameter übergeben werden.
Das obige Beispiel bindet ein Login-Modul ein, das standardmäßig bei J2SE dabei ist, und Principals für die Windows- bzw. Unix-Benutzer-ID ausstellt, unter der das Programm ausgeführt wird. Diese beiden Module erfordern keine Benutzerinteraktion und somit auch keinen speziellen CallbackHandler, sondern erzeugen die Principals automatisch und reichen die Betriebssystem-Benutzerkennung an die Anwendung weiter. Auf diese Weise schaffen diese beiden Module auch eine Art Single-Sign-On mit dem Betriebssystem.
Damit eine Modul-Konfigurationsdatei aktiv wird, muss sie in der System-Property java.security.auth.login.config eingetragen werden. Hierzu kann der Pfad entweder als Parameter des Interpreters java mit der Option -D oder im Programm mit System.setProperty() gesetzt werden:System.setProperty("java.security.auth.login.config", "jaasdemo.conf");Falls in der Konfiguration kein Eintrag mit dem gewünschten Namen vorhanden ist, wird automatisch versucht, einen Eintrag mit dem Namen other zu finden. Existiert dieser auch nicht, kommt es zu einer Exception.
Wie erwähnt, benötigt weder das NTLoginModule noch das UnixLoginModule einen CallbackHandler, da sie nicht interaktiv arbeiten. Daher kann die Angabe eines CallbackHandler bei der Erzeugung des LoginContext entfallen (wodurch ein Default-CallbackHandler aktiv wird, dessen Klassenname in der System-Property auth.login.defaultCallbackHandler eingestellt werden kann). Das folgende Minimalprogramm zur Durchführung eines Logins zeigt nach dem Login die vom Modul vergebenen Principals an:// Login-Kontext für die Konfiguration "Demo" erzeugen try { loginContext = new LoginContext("Demo"); } catch (LoginException e) { System.err.println("login context creation failed: "+e.getMessage()); System.exit(1); } // Durchführung des Logins try { loginContext.login(); } catch (LoginException e) { System.out.println("authentication failed"); System.exit(1); } System.out.println("authentication succeeded"); // Die Principals ermitteln... Set principals = loginContext.getSubject().getPrincipals(); // ...und in einer Iteration ausgeben Iterator it = principals.iterator(); Principal p; while (it.hasNext()) { p = (Principal)it.next(); System.out.println(p); }Ein Beispiel für einen benutzerdefinierten CallbackHandler wird im nächsten Abschnitt gezeigt.
Neben den beiden genannten Modulen werden standardmäßig noch folgende Module mitgeliefert:
- Das JndiLoginModule für eine Authentifizierung gegenüber einem LDAP-Verzeichnis.
- Das Krb5LoginModule für die Anmeldung an einem Kerberos-System.
- Das KeyStoreLoginModule für eine Authentifizierung gegenüber einem Java Key Store.
Material zum Beispiel
- Quelltexte:
Bei JAAS können Login-Module auch kaskadiert, d. h. nacheinander ausgeführt werden. Das folgende Beispiel erweitert die vorhergehende Konfiguration um eine Anmeldung mit dem Modul MyLoginModule.Demo { com.sun.security.auth.module.NTLoginModule required debug=false; demo.MyLoginModule required debug=false; };Damit wird zunächst eine Anmeldung mit dem NTLoginModule und danach mit MyLoginModule durchgeführt.
Mit der Verkettung von Login-Modulen kommt das zweite Grundmerkmal von JAAS zum Tragen, dem von Datenbank-Transaktionen her bekannten Zweiphasen-Prinzip. In der ersten Phase werden die Login-Informationen ermittelt. Dies kann im einfachsten Fall durch eine Abfrage von Benutzername und Passwort erfolgen. Erst wenn alle erforderlichen Module diese erste Phase erfolgreich durchlaufen haben, beginnt die zweite Phase, in der dem Subject die Principals hinzugefügt werden. Daher wird bei JAAS zwischen dem Ergebnis eines einzelnen Moduls und dem Ergebnis der gesamten Kette unterschieden. Bei JAAS kann für jedes einzelne Login-Modul konfiguriert werden, ob die Principals nur dann gewährt werden, wenn die gesamte Kette erfolgreich war, oder ob es bereits genügt, wenn das Modul selbst abgeschlossen wurde.
Diese Einstellung erfolgt über Tags in den Einträgen der Modulkonfiguration. Die einzelnen Tags legen dabei fest, ob das betreffende Modul Erfolg haben muss, damit die Gesamtanmeldung noch als erfolgreicht gilt. Es kann auch definiert werden, ob nachfolgende Module überhaupt noch zur Ausführung kommen sollen. Die Tags sind im Einzelnen:
Tabelle 15.2: Tags für die Login-Module Tag Erfolg zwingend Ausführung folgender Module required ja immer requisite ja nur bei Erfolg sufficient nein nur bei Mißerfolg optional nein immer Module, die als sufficient oder optional definiert sind, beeinflussen das Gesamtergebnis nur, wenn keine required- oder requisite-Module konfiguriert sind. In diesem Fall muss mindestens eine sufficient- oder optional-Modul erfolgreich sein, damit der gesamte Anmeldevorgang als erfolreich gilt.
Die folgende Konfiguration erfordert die erfolgreiche Ausführung des NTLoginModule. Nur wenn dieses Erfolg hat, kann nachgelagert eine optionale Anmeldung mit MyLoginModule erfolgen.Demo { com.sun.security.auth.module.NTLoginModule requisite debug=false; demo.MyLoginModule optional debug=false; };
Eigene Login-Module können durch Implementierung des Interface LoginModule aus dem Paket javax.security.auth.spi erstellt werden. In der Regel wird man auch eine entsprechende Unterklasse von Principal entwickeln, um die von dem Modul erzeugte Benutzerkennung darzustellen. LoginModule definiert fünf Methoden, die in einem Modul implementiert werden müssen. Die Anwendung, in der das Modul eingebunden wird, braucht diese Methoden nie direkt aufzurufen. Die gesamte Steuerung übernimmt der LoginContext. Die Methoden sind im Einzelnen:
Nach der Implementierung kann das Modul über einen entsprechenden Eintrag in der JAAS-Konfigurationsdatei in eine Anwendung eingebunden werden, ohne diese neu zu übersetzen:UserFileDemo { de.dpunkt.security.jaas.UserFileLoginModule required userfile=userdb; };Das folgende Beispiel implementiert ein Login-Modul, das eine Authentifizierung gegen eine Benutzerdatei realisiert. Die Benutzer melden sich dabei mit einem Benutzernamen und einem Passwort an. Dieses steht in gehashter Form neben dem Benutzernamen in der Datei:duke:9d97f24439634d793f0b61d2559ae5c734b98288Die Datei wird dabei als Option in der Modulkonfiguration als Parameter userfile übergeben, der in der Methode initialize() ausgelesen wird. Alle anderen Parameter werden gespeichert. Darüber hinaus wird noch eine Hashtable initialisiert, in die die Dateieinträge eingelesen werden:public void initialize(Subject subject, CallbackHandler callbackHandler, Map sharedState, Map options) { this.subject = subject; this.callbackHandler = callbackHandler; this.sharedState = sharedState; this.options = options; debug = "true".equalsIgnoreCase((String)options.get("debug")); userFileName = (String)options.get("userfile"); userTable = new Hashtable(); }In der Methode login() wird die Datei in eine Hashtable eingelesen und die Eingaben abgefragt. Anschließend wird ein Hash über dem eingegebenen Passwort berechnet und mit dem Hash verglichen, der in der Benutzertabelle eingetragen ist:MessageDigest md; String pwdHash, calculatedPwdHashStr; byte[] calculatedPwdHash; try { md = MessageDigest.getInstance("SHA1"); } catch(NoSuchAlgorithmException e) { throw new LoginException("no SHA-1 implementation found"); } md.update(userID.getBytes()); md.update(String.valueOf(password).getBytes()); calculatedPwdHash = md.digest(); for (int i = 0; i < password.length; i++) password[i] = ' '; password = null; pwdHash = (String)userTable.get(userID); if (pwdHash == null) { succeeded = false; } else { calculatedPwdHashStr = getDigestAsHexString(calculatedPwdHash); if (debug) { System.out.println("\t\t[UserFileLoginModule]"); System.out.println("\t\t hash from file : "+pwdHash); System.out.println("\t\t calculated hash: "+calculatedPwdHashStr); } succeeded = pwdHash.equals(calculatedPwdHashStr); }Verläuft die gesamte Authentifizierung erfolgreich, wird dem Subject aus dem LoginContext ein Principal hinzugefügt:userFilePrincipal = new UserFilePrincipal(userID); if (!subject.getPrincipals().contains(userFilePrincipal)) { subject.getPrincipals().add(userFilePrincipal); if (debug) System.out.println("\t\t[UserFileLoginModule] : added principal"); }In der Methode logout() wird dieses Principal entsprechend wieder entzogen:public boolean logout() throws LoginException { subject.getPrincipals().remove(userFilePrincipal); ... return true; }Material zum Beispiel
Seit J2SE 1.4 besteht die Möglichkeit, die Vergabe von Berechtigungen neben der Codebase oder einer Signatur auch an bestimmte Principals zu knüpfen. Das heißt, dass Berechtigungen für Zugriffe aus der Sandbox heraus nicht grundsätzlich erteilt werden, sobald der Code von einem bestimmten Ort stammt, sondern nur dann, wenn sich ein bestimmter Benutzer angemeldet hat, oder genauer gesagt, über bestimmte Principals verfügt.
Auf diese Weise kann die Zuteilung von Berechtigungen an Applets oder Applikationen, die unter einem SecurityManager laufen, noch weiter flexibilisiert werden. Voraussetzung für diese benutzerbezogene Zuteilung von Berechtigungen ist zunächst ein Login mit JAAS, so wie es im vorhergehenden Abschnitt beschrieben wurde.
Für den zweiten Schritt, den Zugriff aus der Sandbox heraus, verfügt die Klasse Subject über die Methode doAsPrivileged(). Dieser Methode müssen drei Argumente übergeben werden:Die folgende Abbildung zeigt die nötigen Schritte noch einmal im Überblick:
- Ein Subject-Exemplar, mit dessen Berechtigungen die Aktion ausgeführt werden soll. Dieses Objekt muss beim Aufruf über alle für die Aktion erforderlichen Principals verfügen.
- Die auszuführende Aktion in Form eines Exemplars der Klasse PrivilegedAction. Falls die Aktion eine geprüfte Exception auslöst, muss stattdessen die Klasse PrivilegedExceptionAction verwendet werden.
- Schließlich kann ein AccessControlContext übergeben werden, in dem die Prüfung der Privilegien erfolgt. Dieser Parameter kann auf null gesetzt werden, wenn das Subject bereits über alle erforderlichen Berechtigungen verfügt.
Zunächst erfolgt eine Authentifizierung, d. h. ein Subject wird durch den Login-Prozess mit Principals versehen. Dieses Subject wird dann beim doPrivileged()-Aufruf angegeben. Die sicherheitskritische Operation löst schließlich Checks beim SecurityManager aus, die prüfen, ob die Policy die benötigten Berechtigungen für einen der Principals des Subjects gestattet.
Bevor die privilegierte Aktion durchgeführt werden kann, muss zunächst ein Login mit JAAS durchgeführt werden. Dieses Login erfolgt genauso wie im letzten Beispiel. Anschließend erfolgt der doAsPrivileged()-Aufruf.Subject subject = loginContext.getSubject(); PrivilegedExceptionAction action = new FileAccessAction(); try { subject.doAsPrivileged(subject, action, null); } catch(PrivilegedActionException e) { System.err.println("Caught exception "+e.getException()); }Diese Aktion versucht, eine Datei zu öffnen. In diesem Fall kann eine IOException ausgelöst werden. Da es sich hierbei um eine geprüfte Exception handelt, wird das Interface PrivilegedExceptionAction implementiert.class FileAccessAction implements PrivilegedExceptionAction { public Object run() throws IOException { new java.io.FileOutputStream("test.txt").close(); return null; } }Schließlich muss die Policy so definiert werden, dass die Aktion unter dem Principal gestattet ist. Darüber hinaus benötigt aber auch der Login-Vorgang und die privilegierte Ausführung selbst bestimmte Berechtigungen, wenn ein SecurityManager aktiv ist. Im Einzelnen sind in der Policy die folgenden Einträge nötig, die entweder global oder mit einer Codebase für die betreffenden Module vergeben werden können:// AuthorizationDemo: Setzen der Property permission java.util.PropertyPermission "java.security.auth.login.config", "read,write"; // AuthorizationDemo: Erzeugen des Login-Kontexts permission javax.security.auth.AuthPermission "createLoginContext.UserFileDemo";// Login-Modul: Hinzufügen und Entfernen von Principals permission javax.security.auth.AuthPermission "modifyPrincipals"; // AuthorizationDemo: Ausführen einer privilegierten Aktion permission javax.security.auth.AuthPermission "doAsPrivileged";Weiterhin muss die benötigte Berechtigung für den entsprechenden Principal erteilt werden:grant Principal de.dpunkt.security.jaas.UserFilePrincipal "duke" { permission java.io.FilePermission "*", "write"; };Material zum Beispiel
- Quelltexte: