Beispiel: JBoss-spezifische Security
Inhalt:
Anlegen der Application
EJB-Projekt
persistence.xml
Datenbank vorbereiten
Konfiguration des Loginmoduls
Web-Projekt
Application Client-Projekt (Code)
Application Client-Projekt (Config)
Security-Domain für die ganze Anwendung
Ohne Annotations
Dieses Beispiel zeigt wie das Validieren eines Logins über ein eigenes Login-Modul
funktioniert, außerdem wird die JavaEE-Security gezeigt. Das Beispiel ist mit WildFly 8.1.0 Final entwickelt worden.
Aufbau des Beispiels:
- Es gibt zwei Rollen in dieser Anwendung, "administrator" und "kunde".
- Die Benutzer werden in einer Datenbanktabelle "BENUTZER"
abgelegt, die über die Spalten ID, LOGIN und PASSWORT verfügen.
- Auf die Tabelle "BENUTZER" greift eine gleichnamige EJB zu.
- Eine Session-Bean im EJB-Projekt enthält Methoden auf die nur Kunden bzw. Administratoren zugreifen dürfen.
- Zum Test dienen eine Webanwendung und ein Application-Client. Bei beiden muss der User sich anmelden, die Anmeldeinformationen
werden bis zum EJB-Projekt weitergeben.
Hier gibt es die Enterprise Application zum Download: Security.ear. Die Importanleitung findet
man im Stateless-Beispiel.
WildFly 9/10:
In WildFly 9 begann der Umstieg auf "Elytron", trotzdem funktionieren alle in diesem Beispiel beschriebenen Schritte weiterhin problemlos.
Anlegen der Application
Ein leeres "EAR Application Project" mit dem Namen "Security" erstellen. In Variante 1 werden keine Deployment-Deskriptoren benötigt (außer "web.xml").
Zu erzeugende Module zufügen. Dieses Beispiel benötigt ein EJB-, ein Application Client- und ein Webprojekt.
EJB-Projekt
Wir legen eine Entitiy-Bean "BenutzerBean" im Package de.fhw.komponentenarchitekturen.knauf.security
an, mit den Datenbankfeldern "ID", "LOGIN", "NAME" und "PASSWORT".
Hier ist es wichtig, die Tabellen- und Spaltennamen explizit zu deklarieren, da wir diese Spaltennamen im Login-Modul benutzen müssen,
und uns deshalb nicht auf die Generierungsregeln des JBoss verlassen sollten!
Der Code sieht so aus:
@Entity ()
@Table(name="BENUTZER")
public class BenutzerBean implements Serializable
{
private static final long serialVersionUID = 1L;
private Integer intId;
private String strLogin;
private String strPasswort;
private Collection<RolleBean> rollen;
@Id
@Column(name="ID")
@GeneratedValue
public Integer getId()
{
return this.intId;
}
public void setId (Integer int_Id)
{
this.intId = int_Id;
}
@Column(name="LOGIN")
public String getLogin()
{
return this.strLogin;
}
public void setLogin (String str_Login)
{
this.strLogin = str_Login;
}
@Column(name="PASSWORT")
public String getPasswort()
{
return this.strPasswort;
}
public void setPasswort(String str_Passwort)
{
this.strPasswort = str_Passwort;
}
@ManyToMany()
@JoinTable (name="BENUTZER_ROLLE",
joinColumns={@JoinColumn(name="BENUTZER_ID") },
inverseJoinColumns={@JoinColumn(name="ROLLEN_ID") })
public Collection<RolleBean> getRollen()
{
return this.rollen;
}
public void setRollen (Collection<RolleBean> rollen)
{
this.rollen = rollen;
}
}
Die Klasse hat eine (unidirektionale) ManyToMany-Beziehung zur RolleBean. Da wir die Rollen per SQL anlegen und sie nirgends im Programm
selbst verwendet werden müssen wir hier uns hier keine Gedanken über cascadeType
oder fetchType
machen.
Wichtig ist bei der Relationship, dass wir hier den Namen der Relationstabelle sowie deren Spalten explizit über die Annotation @JoinTable
definieren.
Jetzt wird die RolleBean
zugefügt:
@Entity ()
@Table(name="ROLLE")
public class RolleBean implements Serializable
{
private static final long serialVersionUID = 1L;
private Integer intId;
private String strRolle;
@Id
@Column(name="ID")
@GeneratedValue
public Integer getId()
{
return this.intId;
}
public void setId (Integer int_Id)
{
this.intId = int_Id;
}
@Column(name="ROLLE")
public String getRolle()
{
return this.strRolle;
}
public void setRolle(String str_Rolle)
{
this.strRolle = str_Rolle;
}
}
Jetzt fügen wir eine Stateless Session Bean SecuredBean
im Package de.fhw.swtvertiefung.knauf.security
zu.
@Stateless
@DeclareRoles(value={"administrator", "kunde"})
@SecurityDomain(value="knaufsecurity")
public class SecuredBean implements Secured
{
private static final Logger logger = Logger.getLogger (SecuredBean.class.getName() );
@PersistenceContext(unitName="securityPersistenceUnit")
private EntityManager entityManager = null;
@Resource()
private SessionContext sessionContext = null;
@RolesAllowed(value={"administrator"} )
public void forAdminOnly()
{
logger.info("forAdminOnly");
}
@RolesAllowed(value={"kunde"} )
public void forKundeOnly()
{
logger.info("forKundeOnly");
}
@RolesAllowed(value={"administrator", "kunde"} )
public void forBoth()
{
logger.info("forBoth");
logger.info ("Caller: '" + this.sessionContext.getCallerPrincipal().getName() +"'");
logger.info ("Caller in Rolle 'kunde' ? " + this.sessionContext.isCallerInRole("kunde"));
logger.info ("Caller in Rolle 'administrator' ? " + this.sessionContext.isCallerInRole("administrator"));
//Jetzt einen Datenbankzugriff versuchen:
Query query =this.entityManager.createQuery("select o from BenutzerBean o");
List<BenutzerBean> listBenutzer = query.getResultList();
Iterator<BenutzerBean> iteratorBenutzer = listBenutzer.iterator();
while (iteratorBenutzer.hasNext() == true)
{
BenutzerBean benutzerAktuell = iteratorBenutzer.next();
logger.info("Benutzer-Login: '" + benutzerAktuell.getLogin() + "'");
}
}
}
Die Bean implementiert das Remote-Interface "SecuredRemote", das die Deklaration der drei Methoden enthält.
Die Methode "forBoth" liest alle Benutzer ein und gibt diese aus (als Test ob unsere BenutzerBean korrekt deklariert ist).
Um das Beispiel übersichtlicher zu halten wird die Query hier direkt angegeben, ohne sie als "NamedQuery" zu deklarieren.
Mittels der Annotation @DeclaredRoles
wird festgelegt welche Rollen in der Bean vorkommen. Da ich im
Beispiel zwei Rollen habe wird eine Array-Syntax verwendet. Das Beispiel hat übrigens auch ohne diese Annotation funktioniert.
Jede Methode enthält in einer Annotation @javax.annotation.security.RolesAllowed
ein String-Array der erlaubten Rollen.
Es gibt eine JBoss-spezifische Besonderheit: Über die Annotation @org.jboss.ejb3.annotation.SecurityDomain
wird festgelegt dass für Zugriffe auf unsere Bean die Security Domain "knaufsecurity" verwendet werden soll.
ACHTUNG:
Es gibt eine gleichnamige Annotation "org.jboss.security.annotation.SecurityDomain",
diese dürfen wir nicht verwenden. Tun wir es doch ist unsere Bean absolut ungesichert, es gibt beim Deploy allerdings keine Fehlermeldungen.
Nur zur Laufzeit erhalten wir eine Meldung "JBAS013323: Invalid User"
Um in einer SessionBean Informationen über den aktuellen Benutzer zu erhalten, muss man auf den javax.ejb.SessionContext
zugreifen. Diesen kann man sich mittels der Annotation @javax.annotation.Resource
injizieren lassen. Die Methode getCallerPrincipal()
liefert Informationen über den User. Mittels isCallerInRole(rolle)
kann man abfragen, ob der User eine bestimmte Rolle hat.
persistence.xml
Für Entity Beans muss eine Persistence Unit deklariert werden. Dies geschieht über eine Datei "persistence.xml" im Unterverzeichnis
"META-INF" des EJB-Projekts.
Sie hat diesen Inhalt:
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
version="2.0">
<persistence-unit name="securityPersistenceUnit">
<jta-data-source>java:jboss/datasources/ExampleDS</jta-data-source>
<properties>
<!-- Setzen dieser Property aktiviert das automatische Tabellen-Generieren und Löschen beim Deploy! -->
<property name="hibernate.hbm2ddl.auto" value="create-drop" />
<!-- SQL-Logging einschalten: -->
<property name="hibernate.show_sql" value="true"></property>
</properties>
</persistence-unit>
</persistence>
Datenbank vorbereiten
In der Datenbank müssen nach jedem Deploy die Benutzer und die Rollen angelegt werden:
insert into rolle (id, rolle) values (1, 'administrator');
insert into rolle (id, rolle) values (2, 'kunde');
insert into rolle (id, rolle) values (3, 'gast');
insert into benutzer (id, login, passwort) values (1, 'admin', 'admin');
insert into benutzer (id, login, passwort) values (2, 'kunde', 'kunde');
insert into benutzer (id, login, passwort) values (3, 'gast', 'gast');
insert into benutzer_rolle (benutzer_id, rollen_id) values (1, 1);
insert into benutzer_rolle (benutzer_id, rollen_id) values (2, 2);
insert into benutzer_rolle (benutzer_id, rollen_id) values (3, 3);
Das ganze geht aber auch einfacher: JBoss sucht beim Deploy in der EJB-JAR-Datei nach einer Datei namens "import.sql" (auf der obersten Ebene,
im Projekt also im Verzeichnis "ejbModule" abzulegen!) und führt diese aus.
In WildFly 8 und 9 sieht man keine Statusmeldungen über die Ausführung dieses Scripts, erst ab WildFly 10.0 Final wird diese Meldung ausgegeben:
12:26:05,696 INFO [org.hibernate.tool.hbm2ddl.SchemaExport] (ServerService Thread Pool -- 62) HHH000476: Executing import script '/import.sql'
Für WildFly 8 und 9 kann sich etwas näher herantasten:
Gemäß Anleitung für das
Logging von SQL-Parametern definiert man einen neuen Trace-Logger in "standalone.xml",
und filtert für diesen die Kategorie "org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl".
Danach sieht man in meinem Beispiel diese Logausgaben, die sich auf "import.sql" beziehen:
21:00:13,825 TRACE [org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl] (ServerService Thread Pool -- 70) trying via [new URL("/import.sql")]
21:00:13,826 TRACE [org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl] (ServerService Thread Pool -- 70) trying via [ClassLoader.getResourceAsStream("/import.sql")]
Dass die Datei "import.sql" gefunden wurde, erkennt man daran, dass es im zweiten Schritt aufhört. Gibt es die Datei nicht im Projekt, geht es
nämlich bis zu einer "nicht gefunden"-Meldung weiter:
21:02:55,379 TRACE [org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl] (ServerService Thread Pool -- 77) trying via [new URL("/import.sql")]
21:02:55,379 TRACE [org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl] (ServerService Thread Pool -- 77) trying via [ClassLoader.getResourceAsStream("/import.sql")]
21:02:55,383 TRACE [org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl] (ServerService Thread Pool -- 77) trying via [new URL("import.sql")]
21:02:55,383 TRACE [org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl] (ServerService Thread Pool -- 77) trying via [ClassLoader.getResourceAsStream("import.sql")]
21:02:55,385 DEBUG [org.hibernate.tool.hbm2ddl.SchemaExport] (ServerService Thread Pool -- 77) Import file not found: /import.sql
Aber beruhigend: wenn wir Fehler in "import.sql" haben, dann werden die uns auf jeden Fall als "ERROR" auf der Konsole geloggt.
Konfiguration des Loginmoduls
In JBoss 7 und WildFly 8 erfolgt die Konfiguration des Login-Moduls über die Server-Konfiguration in "standalone.xml" (bzw. "domain.xml").
Im Abschnitt <subsystem xmlns="urn:jboss:domain:security:1.2">
wird eine security-domain
definiert:
<security-domain name="knaufsecurity" cache-type="default">
<authentication>
<login-module code="Database" flag="required">
<module-option name="dsJndiName" value="java:jboss/datasources/ExampleDS"/>
<module-option name="principalsQuery" value="SELECT PASSWORT FROM BENUTZER WHERE LOGIN=?"/>
<module-option name="rolesQuery" value="SELECT R.ROLLE, 'Roles' FROM ROLLE AS R, BENUTZER_ROLLE AS BR, BENUTZER AS B WHERE B.LOGIN=? AND BR.ROLLEN_ID = R.ID AND BR.BENUTZER_ID = B.ID"/>
</login-module>
</authentication>
</security-domain>
Das Attribut "code" des "login-module" gibt an, welche Implementierung verwendet werden soll. Eine Übersicht der verfügbaren Login-Module findet man
hier.
Wir verwenden hier ein Login-Modul, das Benutzername und Passwort gegen Datenbanktabellen verifiziert. Diese Datenbanktabellen entsprechen
unseren EJBs.
Jedes Login-Modul erfordert eine eigene Konfiguration. Für das "Database"-Modul sind dies:
- "dsJndiName": Für den Datenbankzugriff wird eine DataSource verwendet, die über den hier angegebenen JNDI-Namen gefunden wird.
Das muss natürlich identisch sein mit der DataSource, über die die EJBs gespeichert werden.
- "principalsQuery": dient zum Prüfen der Kombination "Benutzername/Passwort": der Login wird als erster Parameter in die Query übergeben,
zurück kommt das Passwort - das Login-Modul prüft, ob es dem eingegebenen Passwort entspricht.
- "rolesQuery": liefert die Benutzerrollen für den per Parameter in die Query übergebenen Login. Das Ergebnis der Query muss eine Tabelle mit zwei Spalten
sein, wobei die erste Spalte den Namen der Benutzerrolle enthält, die zweite Spalte muss den Wert "Roles" enthalten.
In früheren Versionen konnte man das innerhalb des EJB-Moduls über eine Datei, deren Name auf "...-jboss-beans.xml" endet, erledigen. Dies klappt
in den neueren Versionen nicht mehr.
Security Domain über CLI anlegen
Hierzu ist folgende Sequenz von Befehlen nötig (jeweils mit den zu erwartenden Ausgaben, im letzten Fall habe ich der Lesbarkeit zuliebe Zeilumbrüche
eingefügt, die natürlich entfernt werden müssen):
[standalone@localhost:9990 /] /subsystem=security/security-domain=knaufsecurity:add(cache-type=default)
{"outcome" => "success"}
[standalone@localhost:9990 /] /subsystem=security/security-domain=knaufsecurity/authentication=classic:add
{
"outcome" => "success",
"response-headers" => {
"operation-requires-reload" => true,
"process-state" => "reload-required"
}
}
[standalone@localhost:9990 /] /subsystem=security/security-domain=knaufsecurity/authentication=classic/login-module=Database:add
(
flag=required,
code=Database,
module-options=
[
dsJndiName => "java:jboss/datasources/ExampleDS",
principalsQuery => "SELECT PASSWORT FROM BENUTZER WHERE LOGIN=?",
rolesQuery => "SELECT R.ROLLE, 'Roles' FROM ROLLE AS R, BENUTZER_ROLLE AS BR, BENUTZER AS B WHERE B.LOGIN=? AND BR.ROLLEN_ID = R.ID AND BR.BENUTZER_ID = B.ID"
]
)
{
"outcome" => "success",
"response-headers" => {"process-state" => "reload-required"}
}
[standalone@localhost:9990 /] reload
Beim "code"-Attribut hätte man auch die Langform "org.jboss.security.auth.spi.DatabaseServerLoginModule" verwenden können.
Um die Security-Domain wieder loszuwerden:
[standalone@localhost:9990 /] /subsystem=security/security-domain=knaufsecurity:remove
{
"outcome" => "success",
"response-headers" => {
"operation-requires-reload" => true,
"process-state" => "reload-required"
}
}
[standalone@localhost:9990 /] reload
Diese Schritte kam man sich auch in einer Scriptdatei speichern. Sie wird ausgeführt über den Befehl "jboss-cli.bat --file=pfad_zum_script\meinscript.cli"
Wichtig: als erster Befehl muss "connect" in der Scriptdatei stehen, oder man muss "jboss-cli.bat" mit dem Befehl "--connect" aufrufen.
Fehler in den Queries des Login-Moduls (oder sonstige Konfigurationsfehler) werden uns leider nicht in der Konsole angezeigt. Falls der Login ohne erkennbare Ursache fehlschlägt, muss man
das Logging für die Security-Schicht einschalten. Das Vorgehen ist ähnlich wie das Vorgehen beim Aktivieren des SQL-Parameter-Loggings:
In "%WILDFY_HOME\standalone\configuration\standalone.xml" sucht man den Bereich subsystem xmlns="urn:jboss:domain:logging:1.2". Hier wird ein
neuer ConsoleHandler deklariert, der auf dem Level "Trace" steht:
<console-handler name="CONSOLE.SECURITY">
<level name="TRACE"/>
<formatter>
<pattern-formatter pattern="%K{level}%d{HH:mm:ss,SSS} %-5p [%c] (%t) %s%E%n"/>
</formatter>
</console-handler>
Alle Ausgaben des Packages org.jboss.security
sollen auf diesen Appender loggen:
<logger category="org.jboss.security">
<level name="TRACE"/>
<handlers>
<handler name="CONSOLE.SECURITY"/>
</handlers>
</logger>
Das Log enthält jetzt leider sehr viele Ausgaben, aber dafür auch die Queries des Login-Moduls, und vor allem eventuell intern auftretende Exceptions.
Falls man nur die Ausgaben des Loginmoduls haben will (und sonst keinerlei Security-Ausgaben), so reicht auch:
<logger category="org.jboss.security.auth.spi.DatabaseServerLoginModule">
<level name="TRACE"/>
<handlers>
<handler name="CONSOLE.SECURITY"/>
</handlers>
</logger>
Quelle: http://www.jboss.org/community/docs/DOC-12198
Web-Projekt
Das Web-Projekt muss das EJB-Projekt referenzieren.
"web.xml" sieht so aus:
<?xml version="1.0" encoding="UTF-8"?>
<web-app id="WebApp_ID" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0">
<display-name>SecurityWeb</display-name>
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
<welcome-file>default.html</welcome-file>
<welcome-file>default.htm</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<security-constraint>
<display-name>Security Constraint für SecurityWeb</display-name>
<web-resource-collection>
<web-resource-name>Alles</web-resource-name>
<url-pattern>/*</url-pattern>
<http-method>GET</http-method>
<http-method>POST</http-method>
</web-resource-collection>
<auth-constraint>
<description>Administrator und Kunde dürfen auf diese Seite zugreifen</description>
<role-name>administrator</role-name>
<role-name>kunde</role-name>
</auth-constraint>
</security-constraint>
<login-config>
<auth-method>FORM</auth-method>
<form-login-config>
<form-login-page>/login.jsp</form-login-page>
<form-error-page>/error.jsp</form-error-page>
</form-login-config>
</login-config>
<security-role>
<description>Administrator im Web</description>
<role-name>administrator</role-name>
</security-role>
<security-role>
<description>Kunde im Web</description>
<role-name>kunde</role-name>
</security-role>
<ejb-ref>
<ejb-ref-name>ejb/Secured</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<remote>de.fhw.komponentenarchitekturen.knauf.security.Secured</remote>
</ejb-ref>
</web-app>
Im Element "security-constraint" wird festgelegt welche Resourcen unserer Anwendung gesichert sein sollen. Ich
habe hier alles gesperrt.
"login-config" gibt an dass wir ein eigenes Login-Formular "login.jsp" verwenden wollen. Bei fehlgeschlagenen Logins soll eine
Fehlerseite "error.jsp" aufgerufen werden.
Schließlich werden die beiden Rollen deklariert die in der Anwendung vorkommen können. "role-name" verweist
Es werden die Security-Rollen deklariert die in der Webanwendung verwendet werden.
Zu dem Verweis auf die Secured-EJB muss nicht mehr gesagt werden.
"jboss-web.xml" enthält ebenfalls eine Neuerung (die Deklaration der Security Domain):
<?xml version="1.0" encoding="UTF-8"?>
<jboss-web xmlns="http://www.jboss.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-web_7_0.xsd"
version="7.0">
<context-root>SecurityWeb</context-root>
<security-domain>knaufsecurity</security-domain>
<!-- EJB References -->
<ejb-ref>
<ejb-ref-name>ejb/Secured</ejb-ref-name>
<jndi-name>java:global/Security/SecurityEJB/SecuredBean!de.fhw.komponentenarchitekturen.knauf.security.SecuredRemote</jndi-name>
</ejb-ref>
</jboss-web>
Jetzt werden noch drei JSP-Dateien zugefügt: "index.jsp" ruft die drei Methoden der SecuredBean
auf.
"error.jsp" dient als Fehlerseite bei Anmeldefehlern. Und "login.jsp" ist unser Login-Formular. Die Action j_security_check ist im JSP-Standard festgelegt und definiert ein festes Ziel für die Login-Verarbeitung:
<form method="post" ACTION="j_security_check">
Login: <input type="text" name="j_username" /> <br>
Passwort: <input type="password" name="j_password" /> <br>
<input type="submit" name="login" value="Login">
</form>
Der Logout-Button in "index.jsp" muss hier zu einer selbstgebauten Seite.
<FORM METHOD=POST ACTION="logout.jsp" NAME="logout">
<input type="submit" name="logout" value="Logout">
</FORM>
Die Seite "logout.jsp" sieht im Kern so aus:
<%
//Session beenden:
session.invalidate();
%>
<P>Sie wurden abgemeldet ! </P>
<a href="index.jsp">Zurück zur Startseite</a>
Unsere schicke Anwendung ist jetzt unter http://localhost:8080/SecurityWeb
erreichbar und sollte uns mit dem Login-Formular beglücken.
Application Client-Projekt (Code)
Das Application Client Project muss das EJB-JAR referenzieren.
Im folgenden wird EJB-Injection verwendet.
Die Klasse de.fhw.komponentenarchitekturen.knauf.security.SecurityClient
wird zugefügt und in "Manifest.mf" als "Main-Class" eingetragen.
In der Main-Methode geschehen die Aufrufe der SecuredBean
. Diese wird per Injection gesetzt:
public class SecurityClient
{
@EJB()
public static SecuredRemote secured;
Eine Klasse muss implementiert werden die die Security-Informationen von der Client-Anwendung anfordert
und an den Server übergibt. Hierzu muss das Interface javax.security.auth.callback.CallbackHandler
implementiert werden. Dessen Methode handle (Callback[] callbacks)
wird vom Server aufgerufen,
das übergebene Array von Callback
s beschreibt die angeforderten Informationen (die in den
entsprechenden Callback geschrieben werden).
Der Code sieht so aus (eine Hilfsmethode inputString
) liest die Eingabe von System.in
):
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException
{
//Über alle übergebenen Callbacks iterieren:
for (int intIndexCallback = 0; intIndexCallback < callbacks.length; intIndexCallback++)
{
//NameCallback: Login übergeben.
if (callbacks[intIndexCallback] instanceof NameCallback)
{
NameCallback nameCallback = (NameCallback) callbacks[intIndexCallback];
nameCallback.setName(this.inputString("Login eingeben: "));
}
//PasswordCallback: Login übergeben.
else if (callbacks[intIndexCallback] instanceof PasswordCallback)
{
PasswordCallback passwordCallback = (PasswordCallback) callbacks[intIndexCallback];
passwordCallback.setPassword (this.inputString("Password eingeben: ").toCharArray() );
}
else
{
throw new UnsupportedCallbackException (callbacks[intIndexCallback], "Nicht unterstützer Callback !");
}
}
}
private String inputString(String title) throws IOException
{
System.out.print (title);
InputStreamReader inputStreamReader = new InputStreamReader(System.in, "ISO-8859-1");
BufferedReader bufferedReader = new BufferedReader (inputStreamReader);
return bufferedReader.readLine();
}
Obiges Einlesen von Usereingaben funktioniert mit WildFly 8.1.0 nicht, da dort
System.out, System.error und System.in
ersetzt wurden.
Die Ausgabestreams leiten auf einen Konsolen-Logger um.
System.in
wurde durch einen
NullInputStream
ersetzt.
Unter
https://issues.jboss.org/browse/WFLY-3737 findet sich mein Bugreport dazu - mal
schauen, was herauskommt.
Workaround:
System.console()
verwenden:
private String inputString(String title) throws IOException
{
System.out.print (title);
BufferedReader bufferedReader = new BufferedReader (System.console().reader());
String strInput = bufferedReader.readLine();
return strInput;
}
Die Vorbereitungen sind getroffen, jetzt können wir den Client erstellen. Die Initialisierung sieht so aus:
//Login initialisieren:
SecurityClientCallbackHandler callbackHandler = new SecurityClientCallbackHandler();
LoginContext loginContext = new LoginContext ("knaufclientsecurity", callbackHandler);
loginContext.login();
Wichtig ist hier die Initialisierung des LoginContexts, dem eine Instanz unseres CallbackHandlers übergeben wird.
Der Name des LoginContexts scheint egal zu sein. In JBoss 6 und früher war hier eine Datei "auth.conf" nötig, und der Name des LoginContext musste
in "auth.conf" enthalten sein.
Danach können wir die gesicherten Bean-Methoden aufrufen:
secured.forAdminOnly();
secured.forKundeOnly();
secured.forBoth();
Application Client-Projekt (Config)
Außerdem sind auf Serverseite einige Änderungen nötig, damit sich der Application Client anmelden kann, sprich dass eine Zuordnung des
Anmeldeversuchs einer fremden Komponente über die Standard-Remotekanäle zu einer serverseitigen Security Domain erfolgen kann und
Authentifizierung (Benutzer/Passwort prüfen) und Autorisierung (Benutzerrollen festlegen) erfolgen kann.
Das Konzept ist hier:
- Es wird ein "Security Realm" deklariert. Dieses definiert, wie die Authentifizierung erfolgen soll. Wir verwenden hier JAAS
("Java Authentication and Authorization Service") und leiten weiter auf eine Security Domain. Security Realms haben wir bisher indirekt genutzt:
in "standalone.xml" sind die Realms "ManagementRealm" (Administrativer Zugriff, z.B. über Webkonsole oder CLI von extern) und "ApplicationRealm"
(Default-Userverwaltung für Anwendungen, genutzt z.B. im Message Driven Bean auf WildFly 8-Beispiel.
- Die Security Domain kennen wir bereits aus dem Webzugriff. Sie deklariert das LoginModule und kümmert sich darum, wie Benutzername und
Passwort validiert werden und die Benutzerrollen zugeordnet werden.
- Jetzt kommt es an den unangenehmen Teil: wir müssen WildFly sagen, dass Remotezugriffe nicht auf dem "ApplicationRealm" arbeiten sollen, sondern
das oben definierte Security Realm verwenden soll. Dies sorgt leider dafür, dass alle Remote-Zugriffe auf den Server gegen dieses Realm arbeiten müssen!
Siehe auch: https://docs.jboss.org/author/display/WFLY8/Security+Realms
Die Schritte im Detail (erfolgen alle in "standalone.xml")
- Schritt 1: Deklaration des Security Realm:
<management>
<security-realms>
<security-realm name="ManagementRealm">
...
</security-realm>
<security-realm name="ApplicationRealm">
...
</security-realm>
<security-realm name="KnaufRealm">
<authentication>
<jaas name="knaufsecurity"/>
</authentication>
</security-realm>
</security-realms>...
- Schritt 2: Das Login-Modul wurde oben schon konfiguriert.
- Schritt 3: wir schalten die Remote-Zugriffe auf unser Login-Modul um. Dazu suchen wir diese Stelle:
<subsystem xmlns="urn:jboss:domain:remoting:2.0">
<endpoint worker="default"/>
<http-connector name="http-remoting-connector" connector-ref="default" security-realm="ApplicationRealm"/>
</subsystem>
Hier ändern wir das Attribut "security-realm" auf "KnaufRealm" um.
Anmerkung: in JBoss 7.x hätte man den "remoting-connector" ändern müssen, erst mit WildFly 8 wurde auf HTTP-Upgrade umgestellt:
<subsystem xmlns="urn:jboss:domain:remoting:1.1">
<connector name="remoting-connector" socket-binding="remoting" security-realm="ApplicationRealm"/>
</subsystem>
Diese Config stammt von https://community.jboss.org/thread/234591
Für das Problem, dass man den gesamten Remote-Zugriff auf ein eigenes Security Realm und
eine eigene Security Domain umstellen muss, gibt es einen JIRA-Featurerequest:
https://issues.jboss.org/browse/WFLY-959
Dort ist als Workaround angegeben:
Until a complete solution is available the quick starts already contain an
example showing how interceptors can be used to change the identity used for EJB calls instead of mandating the identity of the connection.
Dies könnte sich eventuell auf diesen Quickstart beziehen:
https://github.com/wildfly/quickstart/tree/master/ejb-security-interceptors
Fehlerdiagnose: falls man clientseitige Security-Probleme hat, kann man in "%WILDFLY_HOME%\appclient\configuration\appclient.xml" die gleichen Logging-Einträge
vornehmen, die ich oben schon für "standalone.xml" angegeben hatte.
Konfiguration per CLI
Schritt 1: SecurityRealm anlegen:
[standalone@localhost:9990 /] /core-service=management/security-realm=KnaufRealm:add
{"outcome" => "success"}
[standalone@localhost:9990 /] /core-service=management/security-realm=KnaufRealm/authentication=jaas:add(name="knaufsecurity")
{
"outcome" => "success",
"response-headers" => {
"operation-requires-reload" => true,
"process-state" => "reload-required"
}
}
[standalone@localhost:9990 /] reload
Schritt 2: Connector anpassen:
[standalone@localhost:9990 /] /subsystem=remoting/http-connector=http-remoting-connector:write-attribute(name=security-realm,value="KnaufRealm")
{
"outcome" => "success",
"response-headers" => {
"operation-requires-reload" => true,
"process-state" => "reload-required"
}
}
[standalone@localhost:9990 /] reload
Hier ändern wir den Wert eines bestehenden Attributs und nutzen deshalb die Operation "write-attribute".
Um diese Änderungen rückgäng zu machen, sind folgenden Schritte nötig:
[standalone@localhost:9990 /] /subsystem=remoting/http-connector=http-remoting-connector:write-attribute(name=security-realm,value="ApplicationRealm")
{
"outcome" => "success",
"response-headers" => {
"operation-requires-reload" => true,
"process-state" => "reload-required"
}
}
[standalone@localhost:9990 /] /core-service=management/security-realm=KnaufRealm:remove
{
"outcome" => "success",
"response-headers" => {
"operation-requires-reload" => true,
"process-state" => "reload-required"
}
}
Security-Domain für die ganze Anwendung
Alternativ zu der Deklaration der Security-Domain in Web- und EJB-Projekt können wir diese auch in einer Datei
"jboss-app.xml" im Projekt "Security" (also das Projekt aus dem die EAR-Datei erzeugt wird),
Unterverzeichnis "EarContent\META-INF" angeben. Die Datei könnte so aussehen:
<?xml version="1.0" encoding="UTF-8"?>
<jboss-app xmlns="http://www.jboss.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-app_7_0.xsd"
version="7.0">
<security-domain>knaufsecurity</security-domain>
</jboss-app>
Ohne Annotations
ejb-jar.xml
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar id="ejb-jar_ID" version="3.1" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/ejb-jar_3_1.xsd">
<display-name>SecurityEJB</display-name>
<enterprise-beans>
<session>
<description>
<![CDATA[Die Methoden dieser Session-Bean sind für unterschiedliche Rollen zugelassen.]]>
</description>
<display-name>SecuredBean</display-name>
<ejb-name>SecuredBean</ejb-name>
<business-remote>de.fhw.komponentenarchitekturen.knauf.security.SecuredRemote</business-remote>
<ejb-class>de.fhw.komponentenarchitekturen.knauf.security.SecuredBean</ejb-class>
<session-type>Stateless</session-type>
<transaction-type>Container</transaction-type>
<!--EntityManager-Injection -->
<persistence-context-ref>
<persistence-context-ref-name>
SecurityPersistenceUnitRef
</persistence-context-ref-name>
<persistence-unit-name>securityPersistenceUnit</persistence-unit-name>
<injection-target>
<injection-target-class>
de.fhw.komponentenarchitekturen.knauf.security.SecuredBean
</injection-target-class>
<injection-target-name>entityManager</injection-target-name>
</injection-target>
</persistence-context-ref>
<!--SessionContext-Injection -->
<resource-env-ref>
<resource-env-ref-name>EgalWasHierSteht</resource-env-ref-name>
<resource-env-ref-type>javax.ejb.SessionContext</resource-env-ref-type>
<mapped-name>java:comp/EJBContext</mapped-name>
<injection-target>
<injection-target-class>de.fhw.komponentenarchitekturen.knauf.security.SecuredBean</injection-target-class>
<injection-target-name>sessionContext</injection-target-name>
</injection-target>
</resource-env-ref>
</session>
</enterprise-beans>
<assembly-descriptor>
<security-role>
<description>
<![CDATA[Rolle "Kunde"]]>
</description>
<role-name>kunde</role-name>
</security-role>
<security-role>
<description>
<![CDATA[Rolle "Administrator"]]>
</description>
<role-name>administrator</role-name>
</security-role>
<method-permission>
<role-name>administrator</role-name>
<method>
<description>
<![CDATA[Diese Methode darf nur vom Admin aufgerufen werden !]]>
</description>
<ejb-name>SecuredBean</ejb-name>
<method-intf>Remote</method-intf>
<method-name>forAdminOnly</method-name>
<method-params></method-params>
</method>
</method-permission>
<method-permission>
<role-name>kunde</role-name>
<method>
<description>
<![CDATA[Diese Methode darf nur vom Kunden aufgerufen werden !]]>
</description>
<ejb-name>SecuredBean</ejb-name>
<method-intf>Remote</method-intf>
<method-name>forKundeOnly</method-name>
<method-params></method-params>
</method>
</method-permission>
<method-permission>
<role-name>administrator</role-name>
<role-name>kunde</role-name>
<method>
<description>
<![CDATA[Diese Methode darf von Admin und Kunde aufgerufen werden !]]>
</description>
<ejb-name>SecuredBean</ejb-name>
<method-intf>Remote</method-intf>
<method-name>forBoth</method-name>
<method-params></method-params>
</method>
</method-permission>
</assembly-descriptor>
</ejb-jar>
Neu in diesem Beispiel:
- Deklaration der im Deployment Deskriptor verwendeten Rollen durch ein Element "security-role" (entspricht
der Annotation
@DeclaredRoles
in der Bean).
- Festlegen der Methodenberechtigungen durch Elemente "method-permission" (entspricht
der Annotation
@RolesAllowed
in der Bean). Hier muss man extrem vorsichtig
sein da bei einem Tippfehler z.B. im Bean-Namen keine Fehlermeldungen kommen.
- Injection des SessionContexts: hier gibt es drei Besonderheiten:
a) das XML-Pflichtelement <resource-env-ref-name>
kann hier einen beliebigen Wert haben, denn da wir uns den SessionContext nicht per JNDI-Lookup selbst holen wollen, ist es egal
unter welchem Namen er im JNDI zur Verfügung gestellt wird.
b) Der SessionContext steht automatisch im Environment Naming Context der Bean unter dem Namen java:comp/EJBContext
zur Verfügung (auch wenn er nicht im JNDI-View auftaucht), auf diesen Namen verweist unsere <resource-env-ref>
mittels des Elements <mapped-name>
.
c) Die Angabe des Elements <resource-env-ref-type>
mit dem Datentyps des Elements (javax.ejb.SessionContext)
ist optional, aber kann nicht schaden ;-).
"orm.xml" sieht so aus:
<?xml version="1.0" encoding="UTF-8"?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_2_0.xsd"
version="2.0">
<entity class="de.fhw.komponentenarchitekturen.knauf.security.BenutzerBean" access="PROPERTY"
metadata-complete="true">
<table name="BENUTZER"></table>
<attributes>
<id name="id">
<column name="ID" />
<generated-value/>
</id>
<basic name="login">
<column name="LOGIN" />
</basic>
<basic name="passwort">
<column name="PASSWORT" />
</basic>
<many-to-many name="rollen" target-entity="de.fhw.komponentenarchitekturen.knauf.security.RolleBean">
<join-table name="BENUTZER_ROLLE">
<join-column name="BENUTZER_ID"/>
<inverse-join-column name="ROLLEN_ID"/>
</join-table>
</many-to-many>
</attributes>
</entity>
<entity class="de.fhw.komponentenarchitekturen.knauf.security.RolleBean" access="PROPERTY"
metadata-complete="true">
<table name="ROLLE"></table>
<attributes>
<id name="id">
<column name="ID" />
<generated-value/>
</id>
<basic name="rolle">
<column name="ROLLE" />
</basic>
</attributes>
</entity>
</entity-mappings>
Einzige Besonderheit ist die Deklaration des Join-Tables.
In einer Datei "META-INF\jboss-ejb3.xml" deklarieren wir die Security Domain:
<?xml version="1.1" encoding="UTF-8"?>
<jboss:ejb-jar xmlns:jboss="http://www.jboss.com/xml/ns/javaee"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee http://www.jboss.org/j2ee/schema/jboss-ejb3-2_0.xsd http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/ejb-jar_3_1.xsd"
version="3.1"
xmlns:s="urn:security:1.1"
impl-version="2.0">
<assembly-descriptor>
<s:security>
<!-- Even wildcard * is supported -->
<!-- <ejb-name>MyBean</ejb-name>-->
<ejb-name>*</ejb-name>
<s:security-domain>knaufsecurity</s:security-domain>
<s:run-as-principal></s:run-as-principal>
<s:missing-method-permissions-deny-access>false</s:missing-method-permissions-deny-access>
</s:security>
</assembly-descriptor>
</jboss:ejb-jar>
Man beachte die relativ verwirrende Deklaration der Namespaces für das Subelement "security".
Das Element "ejb-name" muss angegeben werden. Ich habe hier den Stern als Wildcard für alle EJBs verwendet, ich hätte aber auch "SecuredBean" eintragen können.
Die Elemente "run-as-principal" und "missing-method-permissions-deny-access" habe ich nur angegeben, weil die XSD sie als Pflichtfelder erfordert - es geht auch ohne.
Bei ersterem habe ich nichts zur Bedeutung gefunden, das zweite Element gibt das Verhalten des Servers an, wenn auf der gesicherten EJB eine Methode
keine "@RolesAllowed"-Annotation (bzw. "method-permission" in ejb-jar.xml) definiert hat: "true" verweigert den Zugriff, "false" erlaubt ihn (Default).
Achtung: Aktuell wird man einige Validierungsfehler erhalten. Siehe https://issues.jboss.org/browse/WFLY-3189
Siehe https://docs.jboss.org/author/display/WFLY8/Securing+EJBs und
https://docs.jboss.org/author/display/WFLY8/jboss-ejb3.xml+Reference
Anmerkung:
In früheren Versionen (JBoss 6) erfolgte diese Deklaration in einer Datei "jboss.xml". Diese wird von WildFly 8 nicht mehr unterstützt.
Im Client nutze ich ebenfalls Injection. Deshalb benötigen wir hier die Datei "application-client.xml"
<?xml version="1.0" encoding="UTF-8"?>
<application-client id="Application-client_ID" version="6" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/application-client_6.xsd">
<display-name>SecurityClient</display-name>
<ejb-ref>
<ejb-ref-name>ejb/Secured</ejb-ref-name>
<ejb-ref-type>Session</ejb-ref-type>
<remote>de.fhw.komponentenarchitekturen.knauf.security.SecuredRemote</remote>
<injection-target>
<injection-target-class>de.fhw.komponentenarchitekturen.knauf.security.SecurityClient</injection-target-class>
<injection-target-name>secured</injection-target-name>
</injection-target>
</ejb-ref>
</application-client>
Außerdem muss diese EJB-Referenz an einen JNDI-Namen gebunden werden, und das geht über "jboss-client.xml":
<?xml version="1.0" encoding="UTF-8"?>
<jboss-client xmlns="http://www.jboss.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.jboss.com/xml/ns/javaee
http://www.jboss.org/j2ee/schema/jboss-client_6_0.xsd"
version="6.0">
<jndi-name>SecurityClient</jndi-name>
<!-- Connect the declared EJB reference to the JNDI-Name of the EJB: -->
<ejb-ref>
<ejb-ref-name>ejb/Secured</ejb-ref-name>
<jndi-name>java:global/Security/SecurityEJB/SecuredBean!de.fhw.komponentenarchitekturen.knauf.security.SecuredRemote</jndi-name>
</ejb-ref>
</jboss-client>
Beide Dateien enthalten keine Spezialitäten im Vergleich zu früheren Beispielen.
Die modifizierte Version des Projekts gibt es hier: SecurityNoAnnotation.ear.
ACHTUNG: Dieses Projekt kann nicht neben dem obigen Security-Beispiel auf den gleichen Server deployed werden
Stand 27.09.2017
Historie:
21.08.2014: Erstellt aus 2009er-Beispiel, angepasst an WildFly 8.1
10.11.2015: Hinweise zu WildFly 10, Logging für Verarbeitung von "import.sql" weiter analysiert
14.12.2016: Logging für Verarbeitung von "import.sql" klappt unter WildFly 10.0 Final
27.09.2017: Link auf WildFly11-Beispiel, Feinschliff