Beispiel: JBoss-spezifische Security
Inhalt:
Anlegen der Application
EJB-Projekt
persistence.xml
Datenbank vorbereiten
Konfiguration des Loginmoduls
Web-Projekt
Application Client-Projekt
Security-Domain für die ganze Anwendung
"unauthenticatedIdentity"
Ohne Annotations
Dieses Beispiel zeigt wie das Validieren eines Logins über ein eigenes Login-Modul
funktioniert, außerdem wird die J2EE-Security gezeigt.
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.
Anlegen der Application
Ein leeres "EAR Application Project" mit dem Namen "Security" erstellen. Dabei die Option "Generate Deployment Descriptor" setzen.
Zu erzeugende Module zufügen. Dieses Beispiel benötigt ein EJB-, ein Application Client- und ein Webprojekt.
Dabei jeweils die Option "Generate Deployment Descriptor" setzen.
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 "java:/jaas/knaufsecurity" verwendet werden soll.
Anmerkung:
In JBoss 4.0 und 4.2 lag die Annotation in einem anderen Package: @org.jboss.annotation.security.SecurityDomain
ACHTUNG:
Es gibt eine gleichnamige Annotation "org.jboss.aspects.security.SecurityDomain",
diese dürfen wir nicht verwenden. Tun wir es doch ist unsere Bean absolut ungesichert, es gibt allerdings keine Fehlermeldungen.
Anmerkung:
In JBoss 5.0 ist hier auch die Langform der Security-Domain-Angabe als java:/jaas/knaufsecurity
möglich.
In JBoss 4.2 musste im EJB-Projekt zwingend die Kurzform angegeben werden (im Gegensatz zu Web- oder Application-Projekt, die die Langform verlangten)
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.
Nach einem Deploy auf den Server findet man die Security Domaina im "java:"-Namespace wieder:
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_1_0.xsd"
version="1.0">
<persistence-unit name="securityPersistenceUnit">
<jta-data-source>java:/DefaultDS</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.
Das führt zu dieser Ausgabe auf der Konsole:
22:54:52,371 INFO [SchemaExport] Running hbm2ddl schema export
22:54:52,371 INFO [SchemaExport] exporting generated schema to database
22:54:52,371 INFO [SchemaExport] Executing import script: /import.sql
22:54:52,371 INFO [SchemaExport] schema export complete
Konfiguration des Loginmoduls
In JBoss 5 kam eine neue Möglichkeit der Security-Konfiguration hinzu, die die neu eingeführte Bean-Struktur des Servers verwendet. Hier wird
eine Datei, deren Name auf "...-jboss-beans.xml" endet (Beginn des Dateinamens kann frei vergeben werden) z.B. in "META-INF" des EJB-Projekts angelegt.
Quelle: http://server.dzone.com/articles/security-features-jboss-510
Ihr Inhalt sieht so aus:
<?xml version="1.0" encoding="UTF-8"?>
<deployment xmlns="urn:jboss:bean-deployer:2.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="urn:jboss:bean-deployer:2.0 bean-deployer_2_0.xsd">
<application-policy xmlns="urn:jboss:security-beans:1.0" xsi:schemaLocation="urn:jboss:security-beans:1.0 security-beans_1_0.xsd"
name="knaufsecurity">
<authentication>
<login-module code="org.jboss.security.auth.spi.DatabaseServerLoginModule" flag="required">
<module-option name="unauthenticatedIdentity">
gast
</module-option>
<!-- Auf KEINEN FAll Leerzeichen! -->
<module-option name="dsJndiName">java:/DefaultDS</module-option>
<module-option name="principalsQuery">
SELECT PASSWORT FROM BENUTZER WHERE LOGIN=?
</module-option>
<module-option name="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
</module-option>
</login-module>
</authentication>
</application-policy>
</deployment>
Leider liefert uns JBoss beim erfolgreichen Deploy keine Statusinformationen hierüber in Konsole oder Server-Log, es hilft also nur ausprobieren.
Hinweis: die oben eingetragenen "schemaLocations" stimmen nicht, da die Dateien nicht auf der JBoss-Webseite zu finden sind. Ich hoffe das geschieht bald,
und hoffentlich werden auch die Validierungsfehler korrigiert, die die aktuellen xsds (zu finden in "lib\jboss-kernel.jar") enthalten.
Wichtig ist, dass wir auf keinen Fall Leerzeichen vor der "module-option" "dsJndiName" haben! Dies führt nämlich
zu solchen Fehlern:
javax.security.auth.login.LoginException: Error looking up DataSource from:
java:/DefaultDS
at org.jboss.security.auth.spi.DatabaseServerLoginModule.getUsersPassword(DatabaseServerLoginModule.java:194)
at org.jboss.security.auth.spi.UsernamePasswordLoginModule.login(UsernamePasswordLoginModule.java:206)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.lang.reflect.Method.invoke(Unknown Source)
...
Caused by: javax.naming.NameNotFoundException:
java: not bound
at org.jnp.server.NamingServer.getBinding(NamingServer.java:771)
at org.jnp.server.NamingServer.getBinding(NamingServer.java:779)
at org.jnp.server.NamingServer.getObject(NamingServer.java:785)
at org.jnp.server.NamingServer.lookup(NamingServer.java:396)
at org.jnp.interfaces.NamingContext.lookup(NamingContext.java:722)
at org.jnp.interfaces.NamingContext.lookup(NamingContext.java:682)
at javax.naming.InitialContext.lookup(Unknown Source)
at org.jboss.security.auth.spi.DatabaseServerLoginModule.getUsersPassword(DatabaseServerLoginModule.java:172)
... 30 more
JBoss trimmt die Werte also nicht (siehe dazu
https://jira.jboss.org/jira/browse/SECURITY-421 )
JBoss 4.2 und früher:
In früheren Versionen geschah die Konfiguration der Security-Domain über die Datei "\server\default\conf\login-config.xml". Dort werden verwendetes Login-Modul sowie dessen Konfiguration
eingetragen.
Dies sieht in unserem Fall so aus (der Eintrag wird vor dem schließenden "policy"-Element eingefügt):
<application-policy name="knaufsecurity">
<authentication>
<login-module
code="org.jboss.security.auth.spi.DatabaseServerLoginModule"
flag="required">
<module-option name="unauthenticatedIdentity">
gast
</module-option>
<module-option name="dsJndiName">
java:/DefaultDS
</module-option>
<module-option name="principalsQuery">
SELECT PASSWORT FROM BENUTZER WHERE LOGIN=?
</module-option>
<module-option name="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
</module-option>
</login-module>
</authentication>
</application-policy>
Nachteil dieser Methode: der Server musste nach jeder Änderung neu gestartet werden.
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 "server/default/conf/jboss-log4j.xml" wird ein neuer Appender definiert, dessen Loglevel auf "TRACE" gestellt wird:
<appender name="CONSOLE.SECURITY" class="org.apache.log4j.ConsoleAppender">
<errorHandler class="org.jboss.logging.util.OnlyOnceErrorHandler"/>
<param name="Target" value="System.out"/>
<param name="Threshold" value="TRACE"/>
<layout class="org.apache.log4j.PatternLayout">
<param name="ConversionPattern" value="%d{ABSOLUTE} %-5p [%c{1}] %m%n"/>
</layout>
</appender>
Alle Ausgaben des Packages org.jboss.security
sollen auf diesen Appender loggen:
<category name="org.jboss.security">
<priority value="TRACE"/>
<appender-ref ref="CONSOLE.SECURITY"/>
</category>
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:
<category name="org.jboss.security.auth.spi.DatabaseServerLoginModule">
<priority value="TRACE"/>
<appender-ref ref="CONSOLE.SECURITY"/>
</category>
Quelle: http://www.jboss.org/community/docs/DOC-12198
Anmerkung: leider klappt es nicht, "log4j.xml" als Teil der Webanwendung dynamisch mitzudeployen. Zumindest ist es mit Programmierarbeit verbunden:
http://www.jboss.org/community/wiki/Log4jRepositorySelector
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_2_5.xsd"
version="2.5">
<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"?>
<!DOCTYPE jboss-web PUBLIC
"-//JBoss//DTD Web Application 5.0//EN"
"http://www.jboss.org/j2ee/dtd/jboss-web_5_0.dtd">
<jboss-web>
<security-domain>knaufsecurity</security-domain>
<context-root>SecurityWeb</context-root>
<!-- EJB References -->
<ejb-ref>
<ejb-ref-name>ejb/Secured</ejb-ref-name>
<jndi-name>Security/SecuredBean/remote</jndi-name>
</ejb-ref>
</jboss-web>
In JBoss 4.2 musste die Security Domain im Web-Projekt in Langform angegeben werden:
<security-domain>java:/jaas/knaufsecurity</security-domain>
Der Präfix "java:/jaas/" wird in JBoss 5.0 unterstützt, ist aber optional.
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 und entspricht haargenau
dem WebSphere-Beispiel:
<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 führen da es im Gegensatz zum
WebSphere keine Standard-Logout-Aktion gibt.
<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>
JBoss 4.2:
Nach einem Deploy auf den Server sah im JBoss 4.2 der Environment Naming Context der Webanwendung so aus:
Man erkennt die Security Domain. Leider stellt JBoss 5.0 den ENC der Webanwendung nicht dar, vielleicht kommt das noch.
Unsere schicke Anwendung ist jetzt unter http://localhost:8080/SecurityWeb
erreichbar und sollte uns mit dem Login-Formular beglücken.
Application Client-Projekt
Das Application Client Project muss das EJB-JAR referenzieren.
Im folgenden werde ich keine EJB-Injection verwenden, da dies den Client-Startvorgang noch komplizierter machen würde. Es funktioniert aber genauso
wie in den bisherigen Beispielen.
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
.
In "application-client.xml" muss eine EJB-Referenz auf SecuredBean
eingetragen werden:
<?xml version="1.0" encoding="UTF-8"?>
<application-client id="Application-client_ID" version="5" 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_5.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>
</ejb-ref>
</application-client>
Damit die EJB-Referenz funktioniert muss "jboss-client.xml" mit diesem Inhalt zugefügt werden:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jboss-client PUBLIC
"-//JBoss//DTD Application Client 5.0//EN"
"http://www.jboss.org/j2ee/dtd/jboss-client_5_0.dtd" >
<jboss-client>
<jndi-name>SecurityClient</jndi-name>
<ejb-ref>
<ejb-ref-name>ejb/Secured</ejb-ref-name>
<jndi-name>Security/SecuredBean/remote</jndi-name>
</ejb-ref>
</jboss-client>
Das Kernproblem des ApplicationClient ist es, die Security-Informationen zu erfassen und zum Server zu bringen.
Zuerst einmal muss eine Anmeldekonfiguration angelegt werden. Dazu legen wir im Unterverzeichnis "appClientModule/META-INF"
eine Datei "auth.conf" mit diesem Inhalt an:
knaufclientsecurity {
// jBoss LoginModule
org.jboss.security.ClientLoginModule required
;
};
Mit dieser Datei wird festgelegt dass für die Anwendung eine SecurityDomäne "knaufclientsecurity"
existiert, als LoginModul dient org.jboss.security.ClientLoginModule
.
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 ungefähr so aus (in der Implementation des Beispiels wird allerdings der Login vom User eingegeben):
public class SecurityClientCallbackHandler implements CallbackHandler
{
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( "ADMIN" );
}
//PasswordCallback: Login übergeben.
else if (callbacks[intIndexCallback] instanceof PasswordCallback)
{
PasswordCallback passwordCallback = (PasswordCallback) callbacks[intIndexCallback];
passwordCallback.setPassword ("ADMIN".toCharArray() );
}
else
{
throw new UnsupportedCallbackException (callbacks[intIndexCallback], "Nicht unterstützer Callback !");
}
}
}
}
Die Vorbereitungen sind getroffen, jetzt können wir den Client erstellen. Die Initialisierung sieht so aus:
Properties props = new Properties();
props.setProperty(Context.INITIAL_CONTEXT_FACTORY, "org.jnp.interfaces.NamingContextFactory");
props.setProperty(Context.URL_PKG_PREFIXES, "org.jboss.naming.client");
props.setProperty(Context.PROVIDER_URL, "jnp://localhost:1099");
props.setProperty("j2ee.clientName", "SecurityClient");
InitialContext initialContext = new InitialContext(props);
//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.
Als Name des LoginContexts muss der gleiche Wert angegeben sein der auch in "auth.conf" steckt !
Ab jetzt können wir die Beans aufrufen.
Beim Start des Clients ist zu beachten dass wir die Datei "auth.conf" in einem Java-Parameter
java.security.auth.login.config
übergeben müssen. Deshalb muss für den Client eine "Run Configuration"
angelegt werden.
Im Menü "Run" den Punkt "Run Configurations..." wählen. Links unter "Java Application" eine neue namens "SecurityClient" zufügen.
Auf der Karteikarte "Main" wird de.fhw.komponentenarchitekturen.knauf.security.SecurityClient
als "Main class" gewählt.
Auf der Karteikarte "Arguments" wird das "VM Argument"
-Djava.security.auth.login.config=appClientModule/META-INF/auth.conf
eingetragen:
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"?>
<!DOCTYPE jboss-app
PUBLIC "-//JBoss//DTD J2EE Application 5.0//EN"
"http://www.jboss.org/j2ee/dtd/jboss-app_5_0.dtd">
<jboss-app>
<security-domain>knaufsecurity</security-domain>
</jboss-app>
Vor JBoss 5.0 funktionierte das übrigens nicht.
Die Security Domain ist auch hier in Langform angebbar:
<security-domain>java:/jaas/knaufsecurity</security-domain>
"unauthenticatedIdentity"
Im Eintrag von "login-config.xml" steckt diese Zeile:
...
<module-option name="unauthenticatedIdentity">
gast
</module-option>
...
Hiermit kann ein Login deklariert werden, der beim Zugriff auf die EJBs ohne vorhergehenden Login verwendet wird. In diesem Fall
wird der Login "gast" verwendet, dessen Rollen werden dann vom Loginmodul ermittelt (in diesem Fall heißt die Rolle ebenfalls "gast").
Aus einem Application Client heraus kann man die "unauthenticatedIdentity" scheinbar erzwingen, indem man sich mit User/Passwort = null
einloggt:
AppCallbackHandler callbackHandler = new AppCallbackHandler(null, null);
LoginContext loginContext = new LoginContext ("knaufclientsecurity", callbackHandler);
loginContext.login();
Der Klasse org.jboss.security.auth.callback.AppCallbackHandler
werden Username und Passwort übergeben, in diesem Fall zweimal NULL.
Aber auch ohne irgendeinen Login funktioniert die "unauthenticatedIdentity", da es im Application Client wohl keine Prüfung auf vorhandenen Login gibt.
Mein obiges Beispiel wird allerdings keinen Methodenaufruf der Sessionbean zulassen, da keine Methode für die Rolle "gast" freigegeben ist.
Im Webclient habe ich bisher keinen Weg gefunden, einen leeren Login anzugeben und dadurch per Default die "unauthenticatedIdentity" zu bekommen.
Leere Felder im Loginformular werden scheinbar als Leerstrings behandelt, und das führt natürlich zu einem verweigerten Login, da kein leerer User vorhanden ist.
Der einzige Lösungsansatz wäre ein programmatischer Login über die Klasse org.jboss.web.tomcat.security.login.WebAuthentication
, die einen programmatischen Login
über ein natürlich ungesichertes Servlet erlaubt (siehe http://www.jboss.org/community/docs/DOC-12656):
WebAuthentication webauth = new WebAuthentication();
webauth.login("admin", "admin");
log("User Principal=" + request.getUserPrincipal());
log("isUserInRole(Authorized User)=" + request.isUserInRole("administrator"));
request.getRequestDispatcher("../index.jsp").forward(request, response);
Das Beispielservlet leitet nach erfolgtem Login weiter zur Hauptseite.
Allerdings klappt dies auch bei erfolgreichem Login nicht wie gewünscht, beim nächsten
Seitenzugriff (auf eine eigentlich rechtigte Seite) erhielt ich wiederum die Loginseite angeboten.
Der Gast-Login wurde mir ebenfalls nicht wie gewünscht zugewiesen, so dass ich keine nur für "gast" freigegebenen Webseiten hätte besuchen können.
Ein schmutziger Workaround wäre, auf der Loginseite ein zusätzliches Formular mit einem einzigen "Als Gast anmelden"-Button und Gast-Username-/Passwort in Hidden Fields einzubauen:
Klicken Sie hier, um sich als Gast anzumelden:
<form method="post" ACTION="j_security_check">
<input type="hidden" name="j_username" value="gast"/>
<input type="hidden" name="j_password" value="gast"/>
<input type="submit" name="login" value="Login">
</form>
Ohne Annotations
ejb-jar.xml
<?xml version="1.0" encoding="UTF-8"?>
<ejb-jar id="ejb-jar_ID" version="3.0" 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_0.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_1_0.xsd"
version="1.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.
"jboss.xml" sieht so aus:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE jboss PUBLIC
"-//JBoss//DTD JBOSS 5.0//EN"
"http://www.jboss.org/j2ee/dtd/jboss_5_0.dtd">
<jboss>
<security-domain>knaufsecurity</security-domain>
</jboss>
Die modifizierte Version des Projekts gibt es hier: SecurityNoAnnotation.ear.
ACHTUNG: Dieses Projekt kann nicht neben dem obigen Security-Beispiel existieren !
Stand 26.06.2009
Historie:
30.11.2008: Erstellt aus Vorjahresbeispiel, angepaßt an JBoss 5.0
03.12.2008: Abschnitt "unauthenticatedIdentity"
15.12.2008: Abschnitt "unauthenticatedIdentity" überarbeitet, JBoss5-Änderungen beim "java:/jaas/"-Präfix bei Security Domains, "logout" im Client.
25.06.2009: Konfiguration der Security Domain auf "...-jboss-beans.xml" umgestellt, Deployfehler im annotationfreien Beispiel mit JBoss 5.1.0 wegen fehlendem Element "res-type"
26.06.2009: Annotationfreies Beispiel: Falsche Injection des SessionContext: statt "resource-ref" sollte besser "resource-env-ref" verwendet werden.
Tippfehler in orm.xml bei Property "passwort" korrigiert.