Acegi Security-Framework für Spring
Wenn man mit Spring statt mit EJB arbeitet, ist ein Aspekt, welcher normalerweise komplett vom ApplikationServer abgedeckt wird, die Sicherheit. Hierzu gibt es die Standard-Lösung, die auch in vielen Artikeln und Büchern erwähnt und besprochen ist: Acegi
Zusammen mit Spring, lassen sich per AOP Interceptoren beim Zugriff auf Businessmethoden von SpringBeans dazwischen “weben”, die die Authentisierung des Aufrufers überwachen. Acegi benutzt hierzu einen SecurityContext, der auf unterschiedlichste Weise entstehen und befüllt werden kann.
In meinem konkreten Anwendungsfall wurde die Authentisierung einer WebApplikation mittels HTTP-Basic-Authentication realisiert und in der ersten zunächst scheinbar korrekten Konfiguration zeigten sich seltsame Effekte:
Neben den Zugriffen, die richtig authentisiert waren und demnach ohne weiteres ausgeführt wurden, konnte es passieren, dass manchmal auch ein nicht authentisierter Aufruf durchkam?
Wie sich heraus stellte, lebt der SecurityContext in einem ThreadLocal was im ApplikationenServer (hier Tomcat) mit dem jeweiligen WorkerThread assoziiert ist. Nun konnte es passieren, dass die SecurityContext eines vergangenen Requests “überlebt” hat und bei einem erneuten Zugriff wiederbenutzt wurde. So waren Zugriffe die zufällig mit dem “richtigen” WorkerThread ausgeführt wurden, falsch authentisiert.
Was hier fehlt (und in Acegi ist das nicht vorhanden) ist ein LifeCycle des SecurityContexts der sich am Request orientiert. Was Acegi bietet ist die Kontrolle des LifeCycles über eine Instance von “SecurityContextHolderStrategy” und eine solche “Strategy” für HTTP-Requests will ich hier einmal vorstellen.
Ein Listener für den Request
Der Schlüssel zum Request-Based-Lifecycle ist ein ContextListener, der das Erzeugen und Zerstören des Requests mitbekommt. Daran läßt sich der LifeCycle des SecurityContexts koppeln.
Im konkreten Fall ist eigentlich nicht viel mehr zu tun, als die Authentisierung des SecurityContexts am Ende eines jeden Requests zu löschen. Dafür sorgt dieser Listener:
package com.rinke.solutions.listener; import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import org.acegisecurity.context.SecurityContextHolder; /** * listener class that clears the security context of acegi at the end of the request. * @author sr */ public class RequestContextListener implements ServletRequestListener { public void requestDestroyed(ServletRequestEvent evt) { SecurityContextHolder.clearContext(); } public void requestInitialized(ServletRequestEvent evt) { } }
Dieser Listener wird in der web.xml gleich zu Beginn entsprechend eingetragen mehr braucht man nicht zu tun. So können die Effekte, die oben beschrieben wurden, nicht mehr auftreten.
Alternativ kann man den SecurityContext auf mit Hilfe eines Filters löschen, den man in die Request-Verarbeitung einklinkt. Ein solcher Filter macht nichts, ausser am Ende den SecurityContext im Finally-Block zu löschen:
package com.rinke.solutions.acegi.filter; import java.io.IOException; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import org.acegisecurity.context.SecurityContextHolder; /** * simple filter, that clears the acegi security context at the end of the request. * be sure to install it on every request, that is guarded by acegi, when using a * request based lifecycle of the security context. * @author sr * */ public class RequestFilter implements Filter { public void destroy() { } public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { try { chain.doFilter(req, res); } finally { SecurityContextHolder.clearContext(); } } public void init(FilterConfig conf) throws ServletException { } }
Bei der Filterlösung darf man allerdings nicht vergessen, dass der Filter auch ganz bestimmt auf alle Requests konfiguriert wird, die mit Acegi geschützt werden.
Ein komplettes Bespiel mit beiden Varianten kann man hier herunterladen. Es nutzt ein simples SpringServlet, welches wiederum eine SpringBean aufruft, deren eine Methode von Acegi geschützt wird, die genaue Konfiguration kann man im applicationContext.xml nachlesen. Ein TestRequest sollte auf …/test oder …/test?protected=true erfolgen, dann sieht man im letzteren Fall die Authenisierungsaufforderung.
Die echte Applikation schützt auf die gleiche Weise Zugriffe auf einen WebService, deren Delegates auch durch SpringBeans dargestellt werden.