HttpUnit ist eine Testkomponente, die zwar in die Jahre gekommen ist, aber keineswegs ihre Aktualität verloren hat. Sie besticht durch ihre Einfachheit und erlaubt das Testen von einfachen Webseiten, komplexeren Pageflows oder Web Services. In einem Szenario, in dem ein System mit Central Authentication Service (CAS) geschützt ist, muss man mittels HttpUnit zunächst eine CAS-Authentifikation implementieren. Davon handelt dieser Beitrag.
Installation und Verwendung
Für die Verwendung von HttpUnit reicht es, eine entsprechende Jar-Datei in den Projekt-Classpath zu legen. Wenn man Apache Ivy verwendet, kann die folgende Abhängigkeitsdefinition Abhilfe schaffen:
<dependency org="httpunit" name="httpunit" rev="1.7" /> <dependency org="rhino" name="js" rev="1.7R2" transitive="false" />
Bei Apache Maven ist entsprechend
<dependency> <groupId>httpunit</groupId> <artifactId>httpunit</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>rhino</groupId> <artifactId>js</artifactId> <version>1.7R2</version> </dependency>
einzutragen.
Grundidee von HttpUnit
Die Grundidee bei der Verwendung von HttpUnit ist recht simpel: Mittels einer WebConversation erzeugt man ein WebRequest-Objekt, mit dessen Hilfe eine HTTP-Anfrage an eine URL verschickt wird. Ein daraus resultierendes WebResponse-Objekt repräsentiert die HTTP-Antwort und ermöglicht somit einen einfachen Zugriff auf die Elemente von HTML.
WebConversation wc = new WebConversation(); WebRequest req = new GetMethodWebRequest("http://some.acme.com/"); WebResponse resp = wc.getResponse(req); assertNotNull(resp);
CAS Login
Central Authentication Service (CAS) ist eine De-facto-Standardlösung im Bereich von Single-Sign-On (SSO). Für die Anbindung im Web-Container wird üblicherweise ein HttpServletFilter installiert, der sicherstellt, dass der Benutzer eingeloggt ist. Ist das nicht der Fall, leitet er den Benutzer auf die Anmeldedialoge des CAS Servers um. Nach dem Login wird der Benutzer auf die ursprüngliche Seite zurückgeleitet, der Filter verifiziert die CAS-Tickets und lässt den Benutzer durch. Zusammengefasst kann die Kommunikation so dargestellt werden:
- HTTP-Anfrage an die Ziel-URL
- HTTP-Redirect an den CAS Login-Dialog
- HTTP-Post für den CAS Login
- HTTP-Redirect an die Ziel-URL mit einem CAS-Ticket
- HTTP-Anfrage an die Ziel-URL mit einem CAS-Ticket
- HTTP-Redirect an die Ziel-URL mit einem CAS-Cookie
- HTTP-Anfrage an die Ziel-URL mit einem CAS-Cookie
Login mit HttpUnit
Für die Simulation von CAS Login müssen nun Anweisungen für HttpUnit geschrieben werden. HttpUnit unterstützt grundsätzlich ein Http-Redirect. Es kann also auf eine Http-Redirect-Antwort reagieren und schickt automatisch eine Anfrage an das Umleitungsziel. Leider merkt sich HttpUnit beim eingeschalteten Auto-Redirect-Modus bereits besuchte URLs und bricht ab, sobald auf eine URL zum zweiten Mal umgeleitet wird. CAS verwendet aber gerade diese Methode, um auf die Ursprungs-URL umzuleiten. So bleibt nichts anderes übrig, als den Auto-Redirect-Modus auszuschalten und den Http-Redirect selbst zu implementieren. Dazu muss als erstes die WebConversation im HttpUnit konfiguriert werden:
WebConversation wc = new WebConversation(); // setting some client properties. ClientProperties clientProperties = wc.getClientProperties(); clientProperties.setAutoRedirect(false); clientProperties.setAcceptCookies(true);
Der folgende Ablauf ist in Form eines JUnit-Tests implementiert. Zunächst schicken wir eine Anfrage an die Ziel-URL und erwarten einen Http-Redirect zum CAS Server. Um dies zu erkennen, wird die Tatsache genutzt, dass der CAS-Filter bei der Umleitung auf die CAS-Formulare die Ursprungs-URL in einem bestimmten Format als Parameter übergibt.
String sourceUrl = "..."; String sourceUrlEncoded = URLEncoder.encode(sourceUrl, "UTF-8"); // used for CAS redirect detection String ssoUrlSuffix = "?service=" + sourceUrlEncoded + "&security=high"; WebRequest req = new GetMethodWebRequest(sourceUrl); WebResponse resp = wc.getResponse(req); assertEquals("Expected redirect to SSO", HttpServletResponse.SC_MOVED_TEMPORARILY, resp.getResponseCode()); String ssoRedirectionTarget = resp.getHeaderField(LOCATION_HEADER); StringAssert.assertContains("Expected redirect to SSO", ssoUrlSuffix, ssoRedirectionTarget); String ssoUrl = ssoRedirectionTarget.substring(0, ssoRedirectionTarget.indexOf(ssoUrlSuffix)); assertEquals("Expect redirect to SSO", ssoUrl + ssoUrlSuffix, ssoRedirectionTarget);
Wenn die Umleitung auf den CAS Server erfolgt ist, kann das CAS-Formular zum Login verwendet werden. Dazu werden die Parameter gefüllt, die den Namen der Formularfelder entsprechen:
req = new GetMethodWebRequest(ssoRedirectionTarget); resp = wc.getResponse(req); assertEquals("SSO not responding properly", HttpServletResponse.SC_OK, resp.getResponseCode()); WebForm loginForm = resp.getForms()[0]; loginForm.setParameter(HtmlConstants.USERNAME, username); loginForm.setParameter(HtmlConstants.PASSWORD, password); resp = loginForm.submit();
Nach dem CAS Login erwarten wir einen Http-Redirect an die Ursprungs-URL mit einem CAS-Ticket, das als Parameter übertragen wird.
// if the following assert fails, the user could not login. assertEquals("Expect redirect to Target", HttpServletResponse.SC_MOVED_TEMPORARILY, resp.getResponseCode()); String redirectLocation = resp.getHeaderField(LOCATION_HEADER); req = new GetMethodWebRequest(redirectLocation); resp = wc.getResponse(req); redirectLocation = resp.getHeaderField(LOCATION_HEADER); StringAssert.assertStartsWith("Expected redirect to source with ticket", sourceUrl, redirectLocation); StringAssert.assertContains("Expected redirect to source with ticket", "&ticket=", redirectLocation); req = new GetMethodWebRequest(redirectLocation); resp = wc.getResponse(req); redirectLocation = resp.getHeaderField(LOCATION_HEADER); StringAssert.assertStartsWith("Expected redirect to source without ticket", sourceUrl, redirectLocation); StringAssert.assertNotContains("Expected redirect to source without ticket", "&ticket=", redirectLocation);
Dieses Ticket wird vom CAS-Filter validiert und führt im Erfolgsfall zum Http-Redirect auf die Ursprungsseite mit einem gesetzten Http-Cookie.
String cookie = resp.getHeaderField("Set-Cookie"); // Return to the source req = new GetMethodWebRequest(redirectLocation); if (cookie != null) { this.casCookie = cookie.substring(0, cookie.indexOf(";")); req.setHeaderField(COOKIE_HEADER, this.casCookie); } resp = wc.getResponse(req);
Hilfsclient
Wird CAS Server als SSO-Mechanismus verwendet, muss jeder Request nach dem Login den CAS-Cookie gesetzt haben, um vom CAS-Filter durchgelassen zu werden. Dazu kann die Login-Methode in einem zustandsbehafteten Client implementiert werden, der für die Vorbereitung aller Anfragen zuständig ist. Alternativ dazu kann dieser Client auch die fachlichen Methoden anbieten, aus denen die zu testende Webkonversation zusammengestellt werden kann.
Fazit
HttpUnit ist ein altes, aber mächtiges Werkzeug, mit dem auch die Autorisation gegen ein CAS implementiert werden kann. Auch wenn dazu auf den Auto-Redirect-Modus verzichtet werden muss, kann dies mittels weniger Zeilen Code kompensiert werden. Mit einem einfachen Hilfsclient, der den Zugriff auf die WebConversation kapselt, kann das gewünschte Session-Verhalten implementiert werden.