Einfaches Beispiel mit Spring Security und Spring MVC

Alles neu im Spring Webbereich. Nachdem ich zum letzten Mal vor rund einem Jahr mit Spring MVC und damals noch Acegi Security programmiert hatte, musste ich feststellen, dass sich eine Menge geändert hat. Meine Aufgabe heute bestand darin, einen Adminstrationsbereich der mit Spring MVC umgesetzt ist, mit einer Autentifizierung zu versehen.

Die Dokumentation, in der die neuen Features von Spring Security beschrieben werden, ist leider an einigen Stellen nicht detailliert genug. Ich habe es mit ausprobieren dennoch hinbekommen und will es hier kurz beschreiben.

Zunächst die Konfiguration der Spring MVC Controller. Die Konfiguration in der web.xml Datei sieht wie folgt aus.



<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet>
<servlet-name>spring-controller</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>

<servlet-mapping>
<servlet-name>spring-controller</servlet-name>
<url-pattern>/admin/*</url-pattern>
</servlet-mapping>



Da der gewählte Servlet-Name spring-controller lautet, wird eine Konfigurationsdatei mit Namen spring-controller-servlet.xml im WEB-INF Verzeichnis benötigt. Diese sieht bei mir wie folgt aus.



<beans xmlns="http://www.springframework.org/schema/beans" xsi="http://www.w3.org/2001/XMLSchema-instance" context="http://www.springframework.org/schema/context" p="http://www.springframework.org/schema/p" schemalocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">

<!-- Enable autowiring -->
<context:component-scan package="com.mini.biz.servlets">

<!-- Enabale @Controller annotations -->
<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping">

<!-- Enabale @RequestMapping annotations -->
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">

<!-- View resolver -->
<bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView">
<property name="prefix" value="/WEB-INF/view/">
<property name="suffix" value=".jsp">
</property>

</property>
</property></bean></bean></bean></context:component-scan></beans>



Es kommen drei neue Spring 2.5. Elemente darin vor. Das context:component-scan Element sorgt dafür, dass das angegebene Package gescannt wird um das Autowiring der Dependencies durchzuführen. Die DefaultAnnotationHandlerMapping bean ermöglicht das Nutzen der @Controller Annotations. Die AnnotationMethodHandlerAdapter bean ermöglicht das Nutzen der @RequestMapping Annotations. Die viewResolver Bean ist im „oldschool“ Spring MVC Style deklariert. Hier gibt es meiner Meinung nach bisher keine andere Möglichkeit. Meine Requests müssen also wie folgt aussehen: /admin/* damit sie vom Spring DispatcherServlet verarbeitet werden (siehe web.xml). Alles weitere dahinter, z.B. /admin/show.do muss von einem Controller abgearbeitet werden. Die entsprechenden Annotationen im Controller sehen also so aus:



@Controller
@RequestMapping("/show.do")
public class AdminShowController {



In meinem zu schützenden Admin Bereich gibt es 3 Ausgabeseiten, die auf /show.do, /edit.do und /pdf.do gemappt sind. Für einen formularbasierten Schutz, wird ein Login Formular benötigt. Ich führe dafür einen neuen Controller ein.



@Controller
@RequestMapping("/anmelden.do")
public class AdminLoginController
{
@RequestMapping(method = RequestMethod.GET)
public String getDefaultForm()
{
return "login";
}

@RequestMapping(value = "/process.do", method = RequestMethod.POST)
public String onSubmit()
{
return "login";
}
}



Per default reagiert der Controller also auf /admin/anmelden.do. Ich wollte eigentlich login.do als Mapping verwenden, aber irgendwie scheint das in Spring MVC bereits ein reservierter Handler zu sein, den man nicht selbst noch einmal vergeben kann. Der AdminLoginController gibt login als View Name zurück. In meiner Webapplikation muss es also eine Datei /WEB-INF/view/login.jsp geben, die das Login Formular enthält.

Kommen wir nun zur Spring Security Konfiguration. Zunächst müssen diese beiden Elemente in der web.xml dazukommen.



<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/admin/*</url-pattern>
</filter-mapping>



Alles unterhalb von /admin/ durchläuft damit zusätzlich die springSecurityFilterChain. Für die Konfiguration der Security Beans lege ich eine separate XML Datei an, die ich in der applicationContext.xml Datei einbinde. Meine securityBeans.xml sieht wie folgt aus:



<b:beans xmlns="http://www.springframework.org/schema/security" b="http://www.springframework.org/schema/beans" xsi="http://www.w3.org/2001/XMLSchema-instance" schemalocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.1.xsd">

<http config="true">
<intercept-url pattern="/admin/anmelden.do*" filters="none">
<intercept-url pattern="/**" access="ROLE_ADMIN">
<form-login page="/admin/anmelden.do" url="/admin/process.do">
</form-login>

<authentication-provider>
<user-service>
<user name="bob" password="bob" authorities="ROLE_ADMIN">
</user>
</user-service>

</authentication-provider>
</intercept-url></intercept-url></http></b:beans>



Die auf der Spring Security Webseite angegebene Namespace Konfiguration könnt ihr nicht 1:1 übernehmen, da es sonst zu einer Exception mit Ausgabe „namespace Invalid content was found“ kommt. Übernehmt einfach meine Namespace deklaration wenn ihr ein separates File habt. Alternativ kann man es auch so machen.



<beans xmlns="http://www.springframework.org/schema/beans" security="http://www.springframework.org/schema/security" xsi="http://www.w3.org/2001/XMLSchema-instance" schemalocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-2.0.xsd">

<security:http config="true">
<security:intercept-url pattern="/admin/anmelden.do*" filters="none">
<security:intercept-url pattern="/**" access="ROLE_ADMIN">
<security:form-login page="/admin/anmelden.do" url="/admin/process.do">
</security:form-login>

<security:authentication-provider>
<security:user-service>
<security:user name="bob" password="bob" authorities="ROLE_ADMIN">
</security:user>
</security:user-service>

</security:authentication-provider>
</security:intercept-url></security:intercept-url></security:http></beans>



Was macht diese Konfiguration? Zunächst wird die URL des Login Formulars /admin/anmelden.do komplett aus der Sicherung herausgenommen. Das ist notwendig damit man das Login Formular sehen kann ohne sich vorher zu authentifizieren. Mit /** werden dann alle URL's unterhalb von /admin geschützt und der Rolle ROLE_ADMIN zugewiesen. Jetzt kommt mit dem form-login Element das Herzstück. login-page=“/admin/anmelden.do“ legt die (eigene) URL des Login Formulars fest. default-target-url="/admin/show.do" ist die URL, an die nach erfolgreichem Login weitergeleitet wird.

Wichtig ist das login-processing-url Attribut. Es muss sich von login-page und default-target-url unterscheiden. Ist login-processing-url = login-page, dann wird der AbstractProcessingFilter von Spring Security nicht durchlaufen, da das Login Formular ungeschützt ist. Ist login-processing-url = default-target-url funktioniert zwar der Login, bei der Weiterleitung an die target URL wird aber erneut nach den Username und Password Parametern geschaut, die dann natürlich nicht vorhanden sind. Ich habe mich für login-processing-url="/admin/process.do" entschieden und diese neue URL im AdminLoginController auf POST Requests gemapped (siehe oben).

Das security:authentication-provider Element definiert einen einzelnen rudimentären Benutzer für die Rolle Admin. Username bob mit Passwort bob. Hier bietet Spring Security angeklügelte andere Mechanismen. Ich denke für eine sehr einfache Webanwendung reicht die Verwendung eines md5 oder sha Hashs in der Art:



<authentication-provider>
<password-encoder hash="sha">
<user-service>
<user name="bob" password="4e7421b1b8765d8f9406d87e7cc6aa784c4ab97f" authorities="ROLE_ADMIN">
</user>
</user-service>
</password-encoder></authentication-provider>



Damit ist eigentlich alles bereits fertig. Die /WEB-INF/view/login.jsp Datei sieht vereinfacht so aus:



<form action="/stats/process.do" method="post">
Benutzername: <input name="j_username" type="text">
Passwort: <input name="j_password" type="password">
<input value="Login" type="submit">
</form>



Die beiden Eingabefelder müssen in der Standardkonfiguration j_username und j_password heissen.

Unit Tests mit Spring und asynchronem Verhalten

Wieder einmal ein Anwendungsfall aus dem aktuellen (privaten) Projekt. Wenn ein Benutzer seine Bestellung abschliesst, soll diese gespeichert, eine PDF-Rechnung generiert und eine Email mit Attachment versendet werden. Ich habe beschlossen, nach dem Speichern der Bestellung via Hibernate, dass Erstellen der Rechnung und den Email Versand in einen separaten Thread auszulagern, der asynchron ausgeführt wird. Dadurch kehrt die Webanwendung schneller wieder zum Benutzer zurück. PDF Erstellung und Email Versand dauern nämlich 3 bis 5 Sekunden. Unter Useability Gesichtspunkten also durchaus positiv.

Das Starten des Threads ist in eine Klasse OrderProcessor ausgelagert.



package com.mini.biz.order;

import com.mini.biz.tools.mail.EmailProcessor;
import com.mini.biz.fop.pdf.PdfGenerator;
import com.mini.biz.entities.cart.Cart;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.TaskExecutor;

/**
* The {@link OrderProcessor} handles further processing once an
* order has been saved in the system.
*/
public class OrderProcessor
{
@Autowired
private TaskExecutor _taskExecutor;

@Autowired
private PdfGenerator _pdfGenerator;

@Autowired
private EmailProcessor _emailProcessor;


public void processOrder(final Cart cart)
{
Runnable runnable = new Runnable()
{
public void run()
{
m_pdfGenerator.generate(cart);
m_emailProcessor.sentEmailForNewOrder(cart);
}
};
_taskExecutor.execute(runnable);
}
}



Auf die Details vom PdfGenerator und EmailProcessor gehe ich an dieser Stelle nicht weiter ein. Wichtig ist der TaskExecutor. Er ist wie folgt konfiguriert.



<bean id="taskExecutor" class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
<property name="corePoolSize" value="5" />
<property name="maxPoolSize" value="10" />
<property name="queueCapacity" value="25" />
</bean>



Leider nimmt der TaskExecutor nur Runnable Objekte entgegen. Ich persönlich finde das mit Java5 eingeführte Callable Interface schöner – aber gut.

Mit dem Spring Framework und TestNG lässt sich das alles wunderbar testen. Das eingesetzte Web Framework Apache Wicket lassen wir mal ausser Acht.



@Test
@ContextConfiguration(locations={"/applicationContext.xml"})
public class ApproveAndOrderPanelTest extends AbstractTestNGSpringContextTests
{



Hier wird im Spring 2.5 Stil der ApplicationContext referenziert. TestNG Tests müssen leider (im Gegensatz zu JUnit 4.x Tests) immer noch von AbstractTestNGSpringContextTests ableiten. Übrigens sollte man AbstractTestNGSpringContextTests nicht vor Spring 2.5.2 einsetzen, da dort ein wichtiger Bug behoben wurde, der auftritt wenn man die komplette TestNG Suite laufen lässt (AbstractTestNGSpringContextTests executes Spring default callbacks in any case (marked with "alwaysRun = true"). Die @Test Annotation gehört zu TestNG. Kommen wir zum Code der vor den Tests ausgeführt wird:



@BeforeClass
public void before()
{
// Not mentioned here: delete existing PDF File

// Start Wiser SMTP
_wiser = new Wiser();
_wiser.setPort(2525);
_wiser.start();

// Alter MailSenderBean
JavaMailSenderImpl mailSender = (JavaMailSenderImpl) _mailSender;
mailSender.setPort(2525);
mailSender.setHost("localhost");
}
}



Hier starte ich einen Dummy SMTP Server von den Subversion Jungs. Anschliessend „verbiege“ ich die MailSender Bean damit der Dummy SMTP Server verwendet wird. Achtung, wenn Ihr weitere Unit Tests innerhalb der Suite laufen lasst, denkt dran dass Spring Beans in der Regel Singletons sind. Das „Verbiegen“ einer Bean schlägt also auch auf Folgetests durch, wenn es nicht in einer tearDown Methode wieder rückgängig gemacht wird. Nun der eigentliche (start gekürzte) Test.



public void testPanel() throws InterruptedException
{
// not shown: set up mock Bestellung aka Cart

assertEquals(0, _wiser.getMessages().size());
File pdfFile = _pdfGenerator.getPdfFile(cart);
assertFalse(pdfFile.exists());

// not shown: use WicketTester and FormTester to submit HTML Form and trigger order processing

ThreadPoolTaskExecutor threadPoolTaskExecutor = (ThreadPoolTaskExecutor) _taskExecutor;
int activeThreads = threadPoolTaskExecutor.getActiveCount();
while (activeThreads > 0)
{
TimeUnit.SECONDS.sleep(3);
activeThreads = threadPoolTaskExecutor.getActiveCount();
}

pdfFile = _pdfGenerator.getPdfFile(cart);
assertTrue(pdfFile.exists());

assertEquals(1, _wiser.getMessages().size());
// not shown: check that email contains attachment
}



Zunächst überprüfe ich, dass keine PDF Datei existiert und keine Email in der Queue vorhanden ist. Dann wir, hier nicht aufgeführter Code ausgeführt um den OrderProcessor anzuwerfen, so dass dieser einen neuen Thread spawned. Der Thread läuft im Spring konfigurierten TaskExecutor, welcher in Wirklichkeit ein ThreadPoolTaskExecutor ist. Da der TestNG Test weiterläuft, muss ich warten bis alle Threads (in meinem Fall also genau ein einziger Thread) im ThreadPoolTaskExecutor beendet sind. Anschliessend überprüfe ich, ob ein PDF erzeugt und eine Email versendet wurde.

Bleibt nur noch das Aufräumen.



@AfterClass
public void after()
{
_wiser.stop();
}



So richtig mächtig ist das Ganze eigentlich erst unter dem Gesichtspunkt, dass es sich dank WicketTester und FormTester eigentlich gar nicht mehr um einen Unit Test, sondern um einen GUI-Test (Integrationstest) handelt – auch wenn dieser Code hier nicht aufgeführt ist. Apache Wicket wird mit Testklassen ausgeliefert, die Selenium oder Watir (von Layout Aspekten abgesehen) überflüssig machen.

Pojo to XML mit Spring, Castor und Hibernate

Ich baue gerade an eine Webanwendung, mit der es möglich sein soll, von einer Bestellung ein PDF Dokument zu erzeugen. Für die Generierung des PDF Dokuments habe ich Apache FOP als Technologie ausgewählt. In einem Schritt vor der Apache FOP wird zunächst die Bestellung, die auf Domain Ebene in die Klasse Cart gekapselt ist, in ein XML File umgewandelt. Das XML File wird dann mit einer zusätzlichen XSL Datei in XSL-FO umgewandelt und von Apache FOP weiterverarbeitet.

In diesem Posting soll es jedoch nicht um die PDF Generierung, sondern die Ûberführung der gespeicherten Cart Pojo's in XML Dateien gehen. Um möglichst wenig Arbeit mit der XML Erzeugung zu haben, verwende ich ein Marshalling Framework welches aus Java Objekten XML erstellt. Hier hat man die Auswahl zwischen verschiedenen Anbietern, z.B. Xstream, Jaxb, Xmlbeans, Castor etc. In einer Testphase habe ich verschiedene Frameworks ausprobiert und mich für Castor entschieden. Castor benötigt, wie in der Spring WS Dokumentation beschrieben, keine zusätzliche Konfiguration, Annotation whatsoever und kann sofort eingesetzt werden. Bereits während der Evaluierung des Marhalling Frameworks habe ich auf Spring WS gesetzt um gegen eine unabhängige API zu programmieren. Dadurch kann man im Idealfall das eingesetzte Framework schneller wechseln.

Diese Dependencies füge ich meiner Maven2 POM hinzu.



<dependency>
<groupId>org.codehaus.castor</groupId>
<artifactId>castor</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>javax.xml.stream</groupId>
<artifactId>stax-api</artifactId>
<version>1.0-2</version>
</dependency>
<dependency>
<groupId>org.springframework.ws</groupId>
<artifactId>spring-oxm</artifactId>
<version>1.5.2</version>
</dependency>



Stax ist eine Implementierung von JSR173. Hier kann bestimmt auch eine andere API verwendet werden. Spring-OXM beinhaltet Klassen zur angesprochenen API Abstrahierung. Diese verwende ich, um meine Cart Objekte in XML Dateien zu überführen.



package com.mini.biz.tools.oxm;

import com.mini.biz.entities.cart.Cart;
import org.apache.wicket.util.io.IOUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.oxm.Marshaller;

import javax.xml.transform.stream.StreamResult;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public class CartMarshaller
{
@Autowired
private Marshaller m_marshaller;

public void toXml(Cart cart)
{
// todo: determine file path based on Cart using Strategy pattern
File xmlFile = new File("whatsoever");

if (!xmlFile.exists())
{
toXml(cart, xmlFile);
}
}

public void toXml(Cart cart, File xmlFile)
{
if (cart != null)
{
FileOutputStream os = null;
try
{
os = new FileOutputStream(xmlFile);
m_marshaller.marshal(cart, new StreamResult(os));
}
catch (IOException e)
{
// todo: add logging
}
finally
{
IOUtils.closeQuietly(os);
}
}
}
}



An der Stelle, wo das (XML) File aus dem Cart generiert wird, verwende ich normalerweise eine Hilfsklasse, da ich diese „Logik“ noch an anderen Stelle benötige. Erwähnenswert ist noch die mit Spring 2.5 eingeführte @Autowired Annotation. Folgende schlanke Spring Konfiguration ist notwendig.



<bean id="cartMarshaller" class="com.mini.biz.tools.oxm.CartMarshaller" />
<bean id="castorMarshaller" class="org.springframework.oxm.castor.CastorMarshaller"/>




Spring WS wird mit Marshaller Klassen für alle bekannten Frameworks (inklusive der oben genannten) ausgeliefert.

So weit so gut. Es gibt nur ein Problem. Die Pojo's, in meinem Fall ein Cart, werden mit Hibernate persistiert. Hibernate verwendet einen Proxy Mechanismus um das Lazy Loading Pattern zu implementieren. Wenn ich meinen Cart aus der Datenbank lade und an den CartMarshaller ergebe, dann wird keine Cart Instanz in XML umgewandelt, sondern ein CGLIBLazyInitializer. Es gibt 2 Dinge die jetzt passieren können. Entweder ist das erzeugte XML totaler Schrott oder es kommt zu einer Exception beim Marshalling irgendwelcher DataSource Objekte.

Der von Castor selbst vorgeschlagene Ansatz, die Proxy Interfaces in einer castor.properties Datei anzugeben, kommt für mich nicht in Frage. Erstens hatte ich Castor gerade gewählt, weil keine zusätzliche Konfiguration notwendig ist und Zweitens ist der CGLIBLazyInitializer eine Klasse und kein Interface. Hier verwende ich einen kleinen Trick um Hibernate davon abzuhalten, einen Proxy einzusetzen. Meine simple Cart Klasse wird als final deklariert.



public final class Cart implements Serializable



Damit ist zwar das Lazy Loading von Hibernate ausgehebelt, aber es gibt keine Probleme beim Marshalling mit Castor.