Enterprise Eclipse RCP - Teil 3 - JAXenter

Security und Fehlerhandling

Enterprise Eclipse RCP – Teil 3

Stefan Reichert

Eclipse RCP etabliert sich zunehmend als Frontend für verteilte Anwendungen im Unternehmensbereich. Nachdem der zweite Teil der Reihe „Enterprise Eclipse RCP detailliert den Aspekt der Verteilung und das clientseitige Caching beleuchtet hat, beschäftigt sich der dritte Teil mit dem Thema Security und Fehlerhandling. Beide Bereiche sind stark mit dem Thema Verteilung bzw. Kommunikation verwoben, sodass sich Elemente der im Artikel beschriebenen Lösungen auf das im vorhergehenden Teil beschriebene Spring Remoting beziehen.

Security ist ein allgegenwärtiger Begriff bei der Entwicklung von Software. Dies gilt sowohl für Anwendungen für den privaten Gebrauch als auch für Unternehmensanwendungen. Dabei lässt sich der Begriff Security in unterschiedliche Bereiche unterteilen:

  • Authentifizierung
  • Sichere Kommunikation

  • Datensicherheit
  • Autorisation

Jeder dieser Aspekte muss im Rahmen einer Implementierung entsprechend den Anforderungen berücksichtigt werden. Für Authentifizierung und Autorisation bietet Eclipse RCP bereits Mechanismen, deren Verwendbarkeit in einer verteilten Umgebung jedoch überprüft werden muss.

Authentifizierung

Im Rahmen des Equinox-Security-Projekts bietet Eclipse RCP die Möglichkeit, für die Authentifizierung JAAS zu verwenden. Somit kann hier die Standard-Java-Login-Infrastruktur zum Einsatz kommen, typischerweise erfolgt die Authentifizierung dann gegen ein LDAP-System. Im Kontext einer verteilten Anwendung ist allerdings zu bedenken, dass eine durch den Client gesteuerte Authentifizierung gegen ein Fremdsystem zwar grundsätzlich möglich ist, architekturbedingt jedoch die Kommunikation mit anderen Systemen immer zentral durch die Serverseite erfolgen sollte. Diese stellt dafür üblicherweise einen Dienst bereit, den der Client verwenden kann. Entsprechend des im vorherigen Teil vorgestellten Spring Remotings würde die Serverseite einen Service anbieten, der vom Client zur Authentifizierung genutzt wird.

public interface ISecurityService { String login(String userId, String password); } 

Der Service liefert als Rückgabewert einen Token, der bei zukünftigen Requests mittels Piggybacking, also transparent im HTTP Request Header, vom Client mit übermittelt werden muss. Der Token kann auf beiden Seiten, Client- und Serverseite, aspektorientiert behandelt werden. Mit einem Aspekt um die vom Client verwendeten Remote-Services kann der Token auf der Serverseite überprüft werden. Der serverseitige Aspekt kann leicht über die Spring-Konfiguration deklariert werden.

<bean class="org.springframework.aop.framework.ProxyFactoryBean" id="fooRemoteService"> <property name="proxyInterfaces" value="com.demo.IFooRemoteService"/> <property name="target" ref="fooRemoteServiceTarget"/> <property name="interceptorNames"> <list> <value>securityAdvice</value> </list> </property> </bean> <bean class="com.foo.SecurityAdvice" id="securityAdvice"> <property name="contextProvider" ref="contextProvider"/> </bean> 

Der SecurityAdvice überprüft dabei den vom Client bereitgestellten Token, den er über die ContextProvider-Bean zur Verfügung gestellt bekommt.

public class SecurityAdvice implements MethodBeforeAdvice { private IContextProvider contextProvider; public void before(Method method, Object[] arguments, Object returnType) throws Throwable { ISessionService sessionService = (ISessionService) applicationContext.getBean("sessionService"); if (!sessionService.isValid(contextProvider.getContextSessionId())) { throw new SessionInvalidException("Session <"contextProvider.getContextSessionId() + "> is invalid."); } } } 

Ein HTTP-Filter hat die Informationen zuvor aus dem http-Request ausgelesen und dem ContextProvider über einen ThreadLocal zugänglich gemacht.

[ header = Seite 2: Sichere Kommunikation ]

Sichere Kommunikation

Bei der beschriebenen Methode wird der Token bei jedem Request mit übertragen und genügt, um auf die Dienste des Servers zurückzugreifen. Um diesen Mechanismus zu schützen, kann bei einer Kommunikation durch unsichere Netzwerke auch HTTPS als Protokoll verwendet werden. Spring bietet zu diesem Zweck den CommonsHttpInvokerRequestExecutor.

Datensicherheit

Neben der sicheren Übertragung von Daten ist auch eine sichere Ablage von Passwörtern oder anderen sensiblen Informationen direkt auf dem Client notwendig. Mit dem Equinox Secure Storage bietet Eclipse RCP eine ziemlich komfortable Out-of-the-Box-Lösung. Das Bundle org.eclipse.equinox.security stellt für das sichere Ablegen von Daten die Klasse ISecurePreferences zur Verfügung.

ISecurePreferences rootPreferences = SecurePreferencesFactory.getDefault(); ISecurePreferences fooPreferences = securePreferences.node("foo.preferences"); fooPreferences.put("the.key", "the.value", true); 

Die Verwendung fühlt sich dabei fast identisch zu dem „normalen“ IPreferenceStore an. Allerdings kann man, wie man im Beispiel sieht, eine Hierarchie mit Knoten aufbauen. Dies ist praktisch, daISecurePreferences, anders als das IPreferenceStore, nicht Bundle-bezogen existieren, sondern lediglich einmal pro Betriebssystembenutzer. Bundles können sich so einen eigenen Unterknoten anlegen, um Konflikte bei Schlüsselwerten zu vermeiden. Die Werte werden verschlüsselt im secure.storage, einer Textdatei, im Unterverzeichnis .eclipse/org.eclipse.equinox.security des Hauptverzeichnisses des Betriebssystemnutzers abgelegt. Für die Verschlüsselung wird ein so genannter PasswordProvider verwendet. Das Bundle org.eclipse.equinox.security liefert bereits einen Provider mit. Es können aber auch eigene Provider implementiert und über den Extension Point org.eclipse.equinox.security.secureStorage angemeldet werden. Sind mehrere Provider registriert, wird der Provider mit der höchsten Priorität für die Verschlüsselung verwendet.

Autorisation

Die Autorisation, also die Berechtigung eines Benutzers für bestimmte Aspekte der Anwendung, wird üblicherweise in zwei Bereiche getrennt. Die Berechtigung für Daten, also datengetriebene Autorisation, wird für gewöhnlich im Rahmen der Geschäftslogik abgebildet. Entsprechende serverseitige Abfragen beziehen die dafür notwendigen Kontextinformationen des Users automatisch mit ein. Die Berechtigung für einzelne Funktionen, also die funktionale Autorisation, ist dagegen auch für den Client von Bedeutung. Im Idealfall sollten Funktionen, für die ein Benutzer nicht autorisiert ist, nicht in der Oberfläche erscheinen. Dies setzt zwei Dinge voraus: Zum einen müssen die Autorisationsinformationen clientseitig vorliegen, was leicht im Zuge der Authentifizierung geschehen kann. Zum anderen muss der Client die vorliegenden Informationen auch tatsächlich auswerten.

Eclipse RCP bietet hierfür das Capability-Framework, die so genannten „Activities“. Mithilfe des Extension Points org.eclipse.ui.activities können mit Activity-Definitionen entweder einzelne UI-Elemente oder Gruppen von UI-Elementen referenziert werden. Diese Activities können dann sowohl deklarativ als auch programmatisch aktiviert bzw. deaktiviert werden. Die Deaktivierung einer Activity sorgt dafür, dass referenzierte UI-Elemente nicht angezeigt werden. In einem verteilten Kontext haben Activities leider einen kleinen Nachteil, da sie als Extension Point definiert werden. Rollenspezifische Activities müssen somit clientseitig deklarativ gepflegt werden. Rollen werden in verteilten Systems allerdings typischerweise serverseitig definiert. Damit sollte auch das Pflegen von Rollendefinitionen zentral an einer Stelle geschehen, also vorzugsweise auch auf der Serverseite.

Alternativ kann der Eclipse-Command- bzw. Handler-Mechanismus verwendet werden. Sowohl Menüeintrage als auch Funktionen werden typischerweise durch eine IHandler-Implementierung ausgeführt. Ein solcher IHandler definiert eine Methode isHandled(), die aussagt, ob der IHandler aktiv ist. An dieser Stelle kann die Autorisation eingebunden werden und die vergebenen Rechte des Benutzers entsprechend berücksichtigen. Dies kann relativ leicht über eine Vererbung implementiert werden, d. h. eine IHandler-Vaterklasse implementiert die entsprechende Prüflogik gegen die lokal im Cache vorliegenden Rechte. Alternativ kann dies auch aspektorientiert mithilfe eines AspectJ-Aspekts implementiert werden. Zu berücksichtigen ist bei dieser Variante, dass Menüeinträge lediglich deaktiviert erscheinen und nicht ausgeblendet werden.

Unabhängig von der Implementierung der Autorisation im Client sollte aus Sicherheitsgründen immer zusätzlich serverseitig die Autorisation des Benutzers für einen Funktionsaufruf überprüft werden. Zu empfehlen ist auch hier eine aspektorientierte Lösung, die analog zur Gültigkeit des übermittelten Tokens die Autorisation für die aufgerufene Funktion prüft.

Fehlerhandling

Ein Qualitätsmerkmal einer Software ist die Unterstützung des Benutzers im Fehlerfall. Fehlermeldungen, die dem Benutzer angezeigt werden, sollten in einer für ihn verständlichen Sprache verfasst sein. Dennoch müssen alle technischen Details festgehalten werden, die für eine weitere Bearbeitung notwendig sind. Ich möchte den Absatz „Fehlerhandling“ in zwei Bereiche gliedern:

  • Darstellung von Fehlern
  • Logging

Die Darstellung von Fehlern ist technisch gesehen unproblematisch. Mit dem ErrorDialog bietet JFace ein entsprechendes Widget. Hier können sowohl eine lesbare Fehlermeldung im Dialog als auch technische Details im Detailbereich dargestellt werden. Interessant ist eher die Frage, woher die dargestellten Informationen stammen. Fehlersituationen innerhalb des Clients sind dabei relativ gut zu kontrollieren, da an entsprechenden Stellen die passenden Fehlermeldungen generiert werden können. Anders ist es bei der Kommunikation zum Server. Hier können serverseitig beispielsweise technische Ausnahmen wie SQLExceptions oder JMSExceptions auftreten, die zunächst übersetzt werden müssen. Es empfiehlt sich, für die Fehlerbenachrichtigung eine eigene Exception-Klasse für Serviceaufrufe zu verwenden. Die Serverseite konvertiert dabei sämtliche Fehlermeldungen in die entsprechende Exception-Klasse und entkoppelt sie von eventuell abhängigen Bibliotheken. Dieser Punkt ist insofern wichtig, da sonst Exceptions von Bibliotheken, die clientseitig nicht vorhanden sind, nicht dargestellt werden können bzw. zuClassNotFoundExceptions bei der Deserialisierung der Response führen. Dieser Vorgang kann serverseitig beispielsweise durch den Spring-Interceptor-Mechanismus mit einem Advice umgesetzt werden, der für Methodenaufrufe deklariert wird.

<bean class="org.springframework.aop.framework.ProxyFactoryBean" id="fooRemoteService"> <property name="proxyInterfaces" value="com.demo.IFooRemoteService"/> <property name="target" ref="fooRemoteServiceTarget"/> <property name="interceptorNames"> <list> <value>securityAdvice</value> <value>exceptionAdvice</value> </list> </property> </bean> <bean class="com.foo.ExceptionAdvice" id="exceptionAdvice"/> 

Der Aspekt kann dann die Exception-Klasse für den Serviceaufruf mit zusätzlichen, zur jeweiligen Fehlersituation vorhandenen Informationen anreichern.

public class ServiceExceptionAdvice implements ThrowsAdvice { public void afterThrowing(Throwable throwable) throws Throwable { if (throwable instanceof ServiceException) { throw throwable; } // zusätzliche Informationen können hier hinzugefügt werden throw new ServiceException(throwable); } } 

[ header = Seite 3: Logging ]

Logging

Zum Abschluss möchte ich im Rahmen der Fehlerbehandlung noch auf das Thema „Logging“ eingehen. Auch hier bietet die Eclipse-Plattform eine integrierte Lösung. Vom Activator eines Plug-ins lässt sich mit der Methode getLog() auf das ILog des Plug-ins zugreifen. Das ILog hat eine log(IStatus)-Methode, mit der in die Eclipse-Logdatei geschrieben wird. Das IStatus-Objekt beinhaltet dabei sowohl den Text als auch das Log-Level. Die Verwendung der Methode ist so etwas sperrig, da das Erzeugen des IStatus-Objekts sehr umständlich ist. Einfacher ist eine Fassade, die die Erzeugung kapselt. Das Interface der Fassade ähnelt dabei dem von bekannten Logging-Frameworks wie Log4J.

public interface IPluginLogger { void info(String message, Throwable throwable); void info(String message); void error(String message); void error(Throwable throwable); void error(String message, Throwable throwable); } 

Die Implementierung delegiert dann die einzelnen Log-Methoden an eine einzige Methode, die das benötigte IStatus-Objekt erzeugt.

public final class PluginLogger implements IPluginLogger { private Plugin runtimePlugin; ... public final void info(String message, Throwable throwable) { log(IStatus.INFO, IStatus.OK, message, throwable); } ... private final void log(int severity, int code, String message, Throwable exception) { String symbolicName = runtimePlugin.getBundle().getSymbolicName(); IStatus status = new Status(severity, symbolicName, code, message, exception); runtimePlugin.getLog().log(status); } } 

So lässt sich das ILog wie ein gewöhnliches Logging-Interface verwenden. Was aber, wenn man serverseitig und clientseitig das gleiche Logging-Framework einsetzen möchte? Mithilfe einesILogListener, der bei der Platform angemeldet werden kann, können sämtliche Logs des ILogs direkt an das Logging-Framework delegiert werden.

/** * {@link ILogListener} that transfers the <i>Eclipse Log</i> to the * corresponding <i>Log4J Log</i>. public class LogTransfer implements ILogListener { /** The Log4J logger. */ private Logger logger = Logger.getLogger(LogTransfer.class); /** {@inheritDoc} */ public void logging(IStatus status, String plugin) { String message = "[" + plugin + "] " + status.getMessage(); Throwable exception = status.getException(); switch (status.getSeverity()) { case IStatus.CANCEL: logger.error(message, exception); break; case IStatus.ERROR: logger.error(message, exception); break; case IStatus.INFO: logger.info(message, exception); break; case IStatus.OK: logger.info(message, exception); break; case IStatus.WARNING: logger.warn(message, exception); break; default: logger.info(message, exception); break; } } } 

Ob das Logging-Framework direkt oder die zuvor beschriebene Fassade verwendet wird, bleibt dann Geschmackssache. Jedenfalls ist sichergestellt, dass sämtliche Logs, auch die Logs der Eclipse-Plattform, ihren Weg ins Logging-Framework finden. Typischerweise wird als externes Logging-Framework Log4J genutzt. Die Verwendung stellt im OSGi-Kontext allerdings zunächst ein kleines Problem dar. Die Log4J-Bibliothek sollte als eigenes Bundle eingebunden werden. Für die Konfiguration ist allerdings eine Konfigurationsdatei notwendig. Um Log4J diese Konfigurationsdatei im Klassenpfad zugänglich zu machen, kann leicht ein Fragment verwendet werden, das als Host das Log4J Bundle definiert.

In einer verteilten Anwendung ist das Logging im Produktivbetrieb ein wichtiges Instrument zur Identifizierung von Fehlern. Im Fehlerfall stehen die gewünschten Informationen jedoch in der Log-Datei der jeweiligen Installationen. Der Zugriff gestaltet sich teilweise schwierig und umständlich. Wünschenswert ist dann ein so genanntes End-to-End-Logging, die Log-Statements werden dabei zusätzlich an den Server transferiert. Das bedeutet selbstverständlich einen erheblichen zusätzlichen Datenverkehr, der sich allerdings über das Log-Level steuern lässt. Der Transfer sollte dabei auch „nur“ ein sekundäres Log darstellen, also die Log-Datei des Clients nicht ersetzen. Zusätzlich sollte der Transfer nebenläufig geschehen und die Clientfunktionen nicht behindern.

Geschrieben von
Stefan Reichert
Stefan Reichert
Stefan Reichert, Diplom-Wirtschaftsinformatiker (FH), arbeitet als Software Engineer bei Zühlke Engineering in Hamburg. Er beschäftigt sich seit mehreren Jahren mit verteilten Java-Enterprise-Anwendungen, serviceorientierten Architekturen, Eclipse und Eclipse RCP. Seit einiger Zeit begeistert ihn die Entwicklung von Apps mit Android. Darüber hinaus ist er Buchautor und verfasst regelmäßig Fachartikel. Kontakt: stefan[at]wickedshell.net, http://www.wickedshell.net/blog
Kommentare

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht.