Streifen machen schlank – Stripes, das Java Web Framework (2)
Update: Bitte den Artikel kommentieren, ob er hilfreich oder nicht hilfreich war
[Fortsetzung von Streifen machen schlank - Stripes, das Java Web Framework (1)]
Eine erste ActionBean: Login in eine Applikation
Für die Beispielanwendung verkneife ich mir mal das typische Hello World oder ähnliches. Das hat mir persönlich noch nie geholfen. Meist merke ich, wie passend ein Framework für ein Projekt ist, wenn ich damit beginne, grundsätzliche Funktionen wie Login/Logout, Paging, Ajax uvm. umzusetzen – und da ist’s meist schon zu spät, sich noch einmal umzuentscheiden.
Eine ActionBean ist ein Java Objekt, das die Daten eines Requests erhält und die Eingaben des Benutzers verarbeitet. Diese ActionBean entspricht, wenn man so will, der ActionForm und dem Action aus Struts kombiniert in einer einzigen Klasse. Allerdings muß die ActionBean nirgendwo konfiguriert werden (bzw. wir haben das durch den Parameter ActionResolver.packages bereits für alle ActionBeans getan).
Vorbereitung: Login und Security
Für ein typisches Login nehmen wir mal folgenden Ablauf an:
- Prüfen, ob der Benutzer bereits angemeldet ist
- Wenn ja, Seite anzeigen
- Wenn nicht, dann auf die LoginPage umleiten
- Nutzer gibt seine Daten ein
- Applikation prüft, ob gültig
- Wenn ja, dann weiter zur Standardansicht oder zu der zuletzt betrachteten Seite
- Wenn nein, Login-Seite mit Fehlern anzeigen
Da Stripes versucht (-und meiner Meinung nach gelingt das auch vortrefflich), ein leichtgewichtiges, intuitives und nach allen Seiten offenes Framework zu sein, ist kein Sicherheitskonzept implementiert.
Es bietet sich deshalb hier der Einfachheit halber an, einen ServletFilter zu implementieren, der prüft, ob der User angemeldet ist, die Weiterleitungen übernimmt und den User als Attribut in der Session speichert (Die Implementierung nimmer sich den SecurityFilter aus der Bugzooky Stripes Beispielanwendung zum Vorbild):
package personal.web.servlet;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.sourceforge.stripes.util.StringUtil;
import org.apache.log4j.Logger;
public class SecurityFilter implements Filter {
private static final Logger log = Logger.getLogger(SecurityFilter.class
.getName());
public void destroy() {
}
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
if (request.getSession().getAttribute("user") != null) {
filterChain.doFilter(request, response);
}
else if ( isPublicResource(request) ) {
filterChain.doFilter(request, response);
}
else {
if (log.isDebugEnabled()) {
log.debug(request.getServletPath() + " -> Redirecting to Login.");
}
String targetUrl = StringUtil.urlEncode(request.getServletPath());
if (request.getServletPath().startsWith("/View.action"))
targetUrl += "?" + request.getQueryString();
// Redirect the user to the login page, noting where they were coming from
response.sendRedirect(
request.getContextPath() + "/Login.jsp?targetUrl=" + targetUrl);
}
}
public void init(FilterConfig config) throws ServletException {
}
protected boolean isPublicResource(HttpServletRequest request) {
String resource = request.getServletPath();
return publicUrls.contains(resource)
|| (!resource.endsWith(".jsp") && !resource.endsWith(".action"))
|| (resource.startsWith("/errors/") || (resource.startsWith("/layout/")));
}
private static Set<String> publicUrls = new HashSet<String>();
static {
publicUrls.add("/Login.jsp");
publicUrls.add("/Login.action");
publicUrls.add("/Logout.jsp");
publicUrls.add("/Logout.action");
}
}
Und noch der nötige Eintrag in der web.xml:
<filter> <description>Handles Security</description> <filter-name>SecurityFilter</filter-name> <filter-class>de.binnichda.comp.stripes.web.SecurityFilter</filter-class> </filter> <filter-mapping> <filter-name>SecurityFilter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> </filter-mapping>
ActionBean und ActionBeanContext
Jede Stripes ActionBean implementiert das Interface ActionBean, das nur zwei Methoden verlangt, mit denen auf den sogenannten ActionBeanContext zugegriffen werden kann.
Der ActionBeanContext steht also jeder ActionBean zur Verfügung. Es bietet sich somit an, eine eigene, von ActionBeanContext abgeleitete Klasse zu entwickeln, die uns einen einfachen Zugriff auf den Context der Applikation und die Session bietet:
package de.binnichda.comp.stripes.web;
import net.sourceforge.stripes.action.ActionBeanContext;
import de.binnichda.comp.stripes.model.User;
public class ApplicationActionBeanContext extends ActionBeanContext {
/**
* Gibt den momentan angemeldeten User zurück, oder null wenn noch nicht
* angemeldet.
*/
public User getUser() {
return (User) getRequest().getSession().getAttribute("user");
}
/** Speichert den angemeldeten User */
public void setUser(User currentUser) {
getRequest().getSession().setAttribute("user", currentUser);
}
/** Meldet den User ab und erklärt die aktuelle Session auf ungültig */
public void logout() {
getRequest().getSession().invalidate();
}
// ...
}
Da diese Klasse von Stripes instantiert und in der ActionBean gesetzt wird, muss sie noch in web.xml konfiguriert werden:
<init-param>
<param-name>ActionBeanContext.Class</param-name>
<param-value>
de.binnichda.comp.stripes.web.ApplicationActionBeanContext
</param-value>
</init-param>
Um nun den eigenen Context ApplicationActionBeanContext verwenden zu können, sollten wir eine Basisklasse BaseActionBean erstellen, die ActionBean implementiert und eben diesen Context zurückliefert:
package de.binnichda.comp.stripes.web;
import net.sourceforge.stripes.action.ActionBean;
import net.sourceforge.stripes.action.ActionBeanContext;
public class BaseActionBean implements ActionBean {
public ApplicationActionBeanContext getContext() {
return this.context;
}
public void setContext(ActionBeanContext context) {
this.context = (ApplicationActionBeanContext) context;
}
private ApplicationActionBeanContext context;
}
Das Eingemachte: LoginActionBean und Login.jsp
Die Action Bean
Der Entwickler legt nun eine Klasse “LoginActionBean.java” im package de.binnichda.comp.stripes.weban. Der Name kann standardmässig nicht frei gewählt werden, denn Stripes erzeugt aus dem vollständigen Namen der Klasse eine URL.
Die ActionBean hat zwei Properties, nämlich username und password, auf die über Getter-/Settermethoden zugegriffen werden kann und eine Methode “login”, die automatisch von Stripes identifiziert und ausgeführt wird, wenn ein Request für diese ActionBean eintrifft und der User im Browser einen Absendebutton (oder -image) mit dem Namen “login” gedrückt hat. Die Methode Login gibt eine Resolution zurück, also wohin Stripes nach der Ausführung hin verweisen soll.
Ein wichtiger Aspekt fehlt allerdings noch: Wie kann gewährleistet werden, dass username und passwort überhaupt gesetzt werden? Und wie informiere ich den User darüber, dass seine Eingaben fehlerhaft sind?
In Stripes ist das ganz einfach: Es müssen nur die Felder mit der Annotation @Validate und der Art der Validierung gekennzeichnet werden, die von Stripes überprüft werden sollen. Im Beispiel wird nur geprüft, ob username und password überhaupt gesetzt sind (required=true):
package de.binnichda.comp.stripes.web;
import net.sourceforge.stripes.action.RedirectResolution;
import net.sourceforge.stripes.action.Resolution;
import net.sourceforge.stripes.validation.SimpleError;
import net.sourceforge.stripes.validation.Validate;
import net.sourceforge.stripes.validation.ValidationError;
import de.binnichda.comp.stripes.model.User;
public class LoginActionBean extends BaseActionBean {
public Resolution login() {
if (!username.equals("freund")) {
ValidationError error = new SimpleError("Unbekannter Benutzer {0}", username);
getContext().getValidationErrors().add("username", error);
return getContext().getSourcePageResolution();
}
if (!password.equals("geheim")) {
ValidationError error = new SimpleError("Falscher Benutzer oder Kennwort");
getContext().getValidationErrors().addGlobalError(error);
return getContext().getSourcePageResolution();
}
User user = new User();
user.setUsername(username);
user.setPassword(password);
// User in der Session Speichern
getContext().setUser(user);
return new RedirectResolution("/View.jsp");
}
public String getUsername() {
return username;
}
public String getPassword() {
return password;
}
public void setUsername(String username) {
this.username = username;
}
public void setPassword(String password) {
this.password = password;
}
@Validate(required=true)
private String username;
@Validate(required=true)
private String password;
}
Die View Login.jsp
Die zugehörige View Login.jsp wird als normale JSP mit einem Verweis auf die Stripes Tag Library angelegt. Das <stripes:form> Html Tag verweist auf die LoginActionBean. Dies kann entweder über den Namen der Klasse oder den Link /Login.action geschehen. Die Eingabefelder username und password greifen über die Getter/Setter – Metoden auf die entsprechenden Eigenschaften der ActionBean zu. Das <stripes:submit;> Tag ruft beim Absenden des Formulars die Methode in der ActionBean auf, die seinem Attribut name entspricht; in diesem Beispiel also die Methode “login”. Zur Fehlerausgabe dient das
<stripes:errors/> Tag:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ taglib prefix="stripes" uri="http://stripes.sourceforge.net/stripes.tld"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html>
<head><title>Login </title></head>
<body>
<h1>Bitte geben Sie Ihre Anmeldedaten ein</h1>
<stripes:form beanclass="de.binnichda.comp.stripes.web.LoginActionBean" focus="">
<table>
<tr>
<td>Benutzername</td>
<td><stripes:text name="username"/></td>
</tr>
<tr>
<td>Kennwort</td>
<td><stripes:text name="password"/></td>
</tr>
<tr>
<td colspan="2">
<stripes:submit name="login" value="Login"/>
</td>
</tr>
</table>
</stripes:form>
</body>
</html>
Um dann eine Seite nach dem Login anzeigen zu können, benötigen wir noch eine einfache JSP Seite View.jsp:
<%@ page language="java" contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<%@ page import="de.binnichda.comp.stripes.model.User" %>
<h1>Hallo <%=((User)session.getAttribute("user")).getUsername() %></h1>
Und die erste sehr einfache Stripes Applikation ist fertig.
Hier kann man sich das komplette Beispiel als .WAR Datei herunterladen
Wenn man diese Datei unter Tomcat 6 deployt, erreicht man die Anwendung unter http://localhost:8080/StripesBeispiel/.


