Im folgenden Artikel soll die Verwendung von Spring-Security in der aktuellen Version 3.1.3.RELEASE an einem Spring Web MVC Projekt erläutert werden. Hierzu wird die Web-Applikation aus dem Artikel (http://martinzimmermann1979.wordpress.com/2012/05/23/in-10-minuten-ein-spring-web-mvc-projekt-erstellen/) um einen Security Layer erweitert. Es gibt zwei Web-Ressourcen, jeweils eine zum Anzeigen und eine zum Hinzufügen von Benutzern. Beide Ressourcen sollen abgesichert werden, wobei zum Anzeigen die Rolle User ausreicht, während für die Neuanlage Administrations-Rechte vorausgesetzt werden. Im ersten Schritt werden die Benutzer inklusive ihrer Rollen in der Spring-Konfiguration hinterlegt. Später soll das User-Model aus dem vorangegangenen Artikel um Login, Passwort und Rollen erweitert werden, so dass dieses für den Autorisierungs-Prozess verwendet werden kann.
Aber nun zur Umsetzung. Zunächst müssen die Spring-Security Libraries in die bereits existierende pom.xml eingetragen werden:
<properties>
<spring.security.version>3.1.3.RELEASE</spring.security.version>
</properties><dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>${spring.security.version}</version>
</dependency><dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring.security.version}</version>
</dependency><dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring.security.version}</version>
</dependency>
Um eine Web-Applikation mit Spring-Security abzusichern, werden diverse Servlet-Filter benötigt. Damit die Anzahl der Filter-Definitionen in der web.xml nicht überhandnimmt, wird lediglich ein DelegatingFilterProxy eingetragen, der wiederrum auf die anderen Filter delegiert. Wie bei Servlet-Filtern üblich, muss ein URL-Pattern definiert werden, welches für die gesamte Web-Applikation gelten soll:
<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>/*</url-pattern>
</filter-mapping>
Sofern noch nicht vorhanden, muss die web.xml zusätzlich um den ContextLoaderListener erweitert werden, welcher für das Hoch- und Runterfahren von Springs-WebApplicationContext zuständig ist:
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
Sobald der ContextLoaderListener registriert ist, reicht allerdings die Spring-Konfigurations-Datei myWebApp-Servlet.xml aus dem vorangegangenen Beispiel nicht mehr aus. Es wird zusätzlich die Datei applicationContext.xml benötigt. Beide liegen im WEB-INF-Verzeichnis.
Versucht man die Web-Applikation zu starten, wird es dennoch zwangsläufig zu einem Fehler kommen:
SCHWERWIEGEND: Exception starting filter springSecurityFilterChain
org.springframework.beans.factory.NoSuchBeanDefinitionException: No bean named ’springSecurityFilterChain‘ is defined
at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:529)
at org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1095)
Der ContextLoaderListener lädt automatisch die Konfiguration unter applicationContext.xml und das Dispatcher-Servlet mit dem Servlet-Namen myWebApp erwartet die Datei myWebApp-servlet.xml, nur für den springSecurityFilterChain sind beide Konfigurations-Dateien unbekannt. Daher müssen sie explizit als Kontext-Parameter angegeben werden:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
WEB-INFapplicationContext.xml
WEB-INFmyWebApp-servlet.xml
</param-value>
</context-param>
Werfen wir nun einen Blick auf die Servlet-Filter, die der DelegatingFilterProxy standardmäßig verwaltet:
- SecurityContextPersistenceFilter
- LogoutFilter
- UsernamePasswordAuthenticationFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- AnonymousAuthenticationFilter
- SessionManagementFilter
- ExceptionTranslationFilter
- FilterSecurityInterceptor
Bei einem Request werden die Filter der Reihe nach Durchlaufen und nach dessen Vollendung in umgekehrter Reihenfolge erneut passiert (1->2->…->9->8->..->1).
Auf ein paar dieser Filter lohnt sich ein genauerer Blick.
SecurityContextPersistenceFilter
Dies ist der erste Filter der ausgeführt werden muss. Sofern der Benutzer bereits authentifiziert ist, lädt er den SecurityContext inklusive dem Authentication-Objekt des Benutzers aus dem SecurityContextRepository – standardmäßig wird das HttpSessionSecurityContextRepository verwendet – und legt es im SecurityContextHolder ab. Hier wird von den anderen Filtern ein gültiger SecurityContext erwartet. Ist der Benutzer noch nicht eingeloggt, so wird von einem folgenden Filter der Authentifizierungs-Prozess gestartet. Nach Abschluss des Requests wird der SecurityContextHolder wieder freigegeben und die Authentifizierungsdaten im SecurityContextRepository gespeichert.
LogoutFilter
Nach einem Logout werden die registrierten LogoutHandler ausgeführt. Ohne weitere Konfiguration sind dies der TokenBasedRememberMeServices und der SecurityContextLogoutHandler. Nach dem Logout findet ein Redirect auf die logoutSuccessUrl statt. Per Default reagiert der Filter auf die URL /j_spring_security_logout.
UsernamePasswordAuthenticationFilter
Konnte der SecurityContextPersistenceFilter keinen authentifizierten Benutzer ausmachen, kommt der UsernamePasswordAuthenticationFilter zum Einsatz. Sofern nicht anders definiert, reagiert der Filter auf die URL /j_spring_security_check und erwartet vom Login-Formular die Input-Felder j_username und j_password.
ExceptionTranslationFilter
Behandelt alle AccessDeniedException (403 – FORBIDDEN) und AuthenticationException (401 UNAUTHORIZED) die in der Filter-Kette geworfen werden. Hierbei findet eine Umwandlung der Java-Exception in einen entsprechenden HTTP-Status statt.
FilterSecurityInterceptor
Überprüft ob ein Request Zugriff auf eine geschützte Ressource verlangt. Mit intercept-urls aus dem Spring Security Namespace lassen sich Kombinationen aus URL-Pattern und Rollen definieren. Die hier angegebenen Rollen sind Voraussetzung, um auf die jeweilige URL zugreifen zu können. Bei der Reihenfolge der intercept-urls ist zu beachten, dass das erste matchende Pattern zum Einsatz kommt. Allgemeine Pattern sollten daher ans Ende gestellt werden.
Auch wenn sich die Beschreibung anders liest, so ist die Spring-Konfiguration für dieses Verhalten recht übersichtlich. Es wird ein http-Tag benötigt, welches die erwähnten intercept-urls beinhaltet. Die Ressource users zum Anzeigen der Benutzerliste ist für Benutzer mit den Rollen USER_ROLE und ADMIN_ROLE zulässig. Zum Anlegen neuer Benutzer wird ein POST-Request auf die Ressource user ausgeführt. Hierfür ist die Rolle ADMIN_ROLE Voraussetzung. Andere Web-Ressourcen sollen keine Berechtigungen benötigen. Das Verhalten lässt sich mit drei intercept-urls abbilden. Zur Authentifizierung wird einfachheitshalber Basic Authentication verwendet, welches über das Tag http-basic definiert wird. Später soll eine eigene Login-Seite zum Einsatz kommen.
<security:http use-expressions=“true“>
<security:intercept-url pattern=“/users“ access=“hasAnyRole(‚USER_ROLE‘,’ADMIN_ROLE‘)“ />
<security:intercept-url pattern=“/user“ access=“hasAnyRole(‚ADMIN_ROLE‘)“ />
<security:intercept-url pattern=“/*“ />
<security:http-basic />
</security:http>
Kommen wir nun zum AuthenticationManager, der für die Authentifizierung mit Benutzername und Passwort zuständig ist. Als Rückgabewert dient ein Authentication-Objekt, welches einen Authentifizierungs-Token und die Rollen des Benutzers beinhaltet. Als AuthenticationManager kann der ProviderManager von Spring verwendet werden, der die Authentifizierung an AuthenticationProvider delegiert. Jeder Provider ist hierbei für einen bestimmten Authentifizierungs-Mechanismus zuständig. Kann ein Provider den Benutzer authentifizieren, liefert er das Authentication-Objekt zurück. Andernfalls kommt der nächste Provider zum Einsatz. Ohne weitere Konfiguration wird der DaoAuthenticationProvider verwendet, der einen UserDetailService besitzt. Für den schnellen Einsatz wird ein InMemoryUserDetailsManager verwendet, der ausschließlich in der Spring-Konfiguration definiert wird. Später wird eine eigene Implementierung erstellt, welche auf das bestehende User-Model aus dem vorangegangenen Artikel aufbaut.
<security:authentication-manager alias=“authenticationManager“>
<security:authentication-provider>
<security:user-service id=“userDetailsService“>
<security:user name=“user“ password=“123456″ authorities=“USER_ROLE“ />
<security:user name=“admin“ password=“123456″ authorities=“ADMIN_ROLE“ />
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
Möchte man die zu schützenden URLs nicht alle einzeln in der Spring-Konfiguration angeben, sondern die benötigten Rollen lieber bequem per Annotation über der entsprechenden Controller-Methode definieren, müssen ein paar Anpassungen vorgenommen werden. Zunächst werden die intercept-urls wieder aus der Spring-Konfiguration entfernt:
<security:http use-expressions=“true“>
<security:http-basic />
</security:http>
Anschließend muss die Annotation PreAuthorize aktiviert werden:
<security:global-method-security pre-post-annotations=“enabled“/>
Außerdem wird cglib als Maven-Dependency hinzugefügt:
<dependency>
<groupId>cglib</groupId>
<artifactId>cglib-nodep</artifactId>
<version>2.2.2</version>
</dependency>
Im Controller muss lediglich die Annotation PreAuthorize mit den Rollen USER_ROLE und ADMIN_ROLE ergänzt werden, damit die Funktionsweise der bisherigen Spring-Konfiguration entspricht:
@RequestMapping(„/users“)
@PreAuthorize(„hasAnyRole(‚USER_ROLE‘,’ADMIN_ROLE‘)“)
public ModelAndView getUsers() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName(„users“);
modelAndView.addObject(„users“, userService.getUsers());
return modelAndView;
}
Die addUser-Methode wird analog dazu abgesichert, nur dass hier ausschließlich für die Rolle ADMIN_ROLE Zugriff gewährt wird.
Sicherlich benötigt jede Web-Applikation eine Navigation, die im besten Fall nur Links auf Ressourcen enthält, für die der angemeldete Benutzer auch die Autorisierung besitzt. Im Folgenden wird dies in einer JSP mit der Spring-Security-Tag-Library realisiert. Diese muss zunächst als Maven-Dependency eingetragen werden:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>${spring.security.version}</version>
</dependency>
Die Navigation wird in der nav.jsp abgelegt und erhält um jeden Link ein authorize-Element. Besitzt der Benutzer die im access-Feld definierte Rolle, so wird ihm der Link angezeigt:
<%@ taglib prefix=“sec“ uri=“http://www.springframework.org/security/tags“ %>
<sec:authorize access=“hasAnyRole(‚USER_ROLE‘,’ADMIN_ROLE‘)“>
<p><a href=“users“>User</a></p>
</sec:authorize>
<sec:authorize access=“hasAnyRole(‚ADMIN_ROLE‘)“>
<p><a href=“user“>Admin</a></p>
</sec:authorize>
Bislang war das Passwort unverschlüsselt in der Spring-Konfiguration hinterlegt. Um dies zu ändern, wird dem DaoAuthenticationProvider ein PasswordEncoder zugewiesen, wobei als Verschlüsselungsalgorithmus sha verwendet wird. Nun müssen natürlich auch die Passwörter entsprechend angepasst werden:
<security:authentication-manager alias=“authenticationManager“>
<security:authentication-provider>
<security:password-encoder hash=“sha“/>
<security:user-service id=“userDetailsService“>
<security:user name=“user“ password=“7c4a8d09ca3762af61e59520943dc26494f8941b“ authorities=“USER_ROLE“ />
<security:user name=“admin“ password=“7c4a8d09ca3762af61e59520943dc26494f8941b“ authorities=“ADMIN_ROLE“ />
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
Wie anfangs erwähnt, soll ein eigener UserDetailsService zum Tragen kommen, der auf das bestehende User-Model aufbaut. Hierfür muss der User das Interface UserDetails implementieren, welches weitere Getter-Methoden voraussetzt. Zusätzlich zu den bestehenden Attributen forename und lastname werden folgende Eigenschaften ergänzt:
public class User implements UserDetails {
private String forename;
private String lastname;
// the following attributes are new and required by spring security
private String password;
private String username;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
private boolean enabled;
private Collection<SimpleGrantedAuthority> authorities = new ArrayList<SimpleGrantedAuthority>();// getter and setter
}
Als Implementierung für die GrantedAuthority-Collection wird SimpleGrantedAuthority verwendet. Hier können die Benutzer-Rollen per String hinzugefügt werden:
authorities.add(new SimpleGrantedAuthority(„USER_ROLE“));
Nach der Erweiterung des Benutzers wird nun der UserDetailsService implementiert. Dieser besitzt lediglich die Methode loadUserByUsername, welche entweder den gefundenen User zurück gibt oder eine UsernameNotFoundException wirft:
public class UserDetailsServiceImpl implements UserDetailsService {
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = loadUser(username);
if (user == null) {
throw new UsernameNotFoundException(„No user with name “ + username + “ found“);
} else {
return user;
}
}// add loadUser-method
}
Der UserDetailsService wird in der Spring-Konfiguration registriert und dem DaoAuthenticationProvider übergeben:
<bean id=“userDetailsService“ class=“de.mz.spring.security.service.UserDetailsServiceImpl“/>
<security:authentication-manager alias=“authenticationManager“>
<security:authentication-provider user-service-ref=“userDetailsService“>
<security:password-encoder hash=“sha“/>
</security:authentication-provider>
</security:authentication-manager>
Da der Authentifizierungs-Dialog eines Browser nicht grade die eleganteste Lösung ist, soll nun noch eine Login-Seite zum Einsatz kommen, wofür die Spring-Konfiguration um ein form-login-Element erweitert wird:
<security:http use-expressions=“true“>
<security:form-login />
</security:http>
Ohne weitere Angaben, findet die Standardseite von Spring Verwendung. Soll eine eigene Login-Seite angezeigt werden, lässt sich der entsprechende Link über das Attribut login-page setzten:
<security:http use-expressions=“true“>
<security:form-login login-page=“/login“/>
</security:http>
Analog zu den bislang implementierten Controllern, muss auch für die login-URL ein passendes RequestMapping zum Einsatz kommen. Über den ViewName wird auf die entsprechende JSP verwiesen, wobei das Mapping zwischen ViewName und JSP im vorangegangenen Artikel mit Hilfe des ViewResolvers definiert wurde:
@RequestMapping(„/login“)
public ModelAndView login() {
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName(„login“);
return modelAndView;
}
Nun wird die Login-Seite in der WEB-INF/login.jsp erstellt. Sofern nicht anders angegeben, muss das Login-Formular die Felder j_username und j_password enthalten und die Daten an die URL j_spring_security_check senden. Anschließend kommt der UsernamePasswordAuthenticationFilter zum Einsatz, der sich um die Authentifizierung des Benutzers kümmert:
<%@taglib uri=“http://java.sun.com/jsp/jstl/core“ prefix=“c“%>
<html>
<head>
<title>Login</title>
</head>
<body onload=’document.f.j_username.focus();‘>
<h3>Login</h3>
<form name=“f“ action=“/de.mz.spring.security/j_spring_security_check“ method=“POST“>
<table>
<tr>
<td>User:</td>
<td><input type=“text“ name=“j_username“ /></td>
</tr>
<tr>
<td>Password:</td>
<td><input type=“password“ name=“j_password“ /></td>
</tr>
<tr>
<td colspan=“2″><input name=“submit“ type=“submit“ value=“Login“ /></td>
</tr>
</table>
</form>
</body>
</html>
Die eigene Login-Seite ist hiermit verfügbar. Im nächsten Schritt muss es dem Benutzer möglich gemacht werden, sich ausloggen zu können. Hierfür muss die Spring-Konfiguration ein weiteres Mal angepasst und um das logout-Element ergänzt werden:
<security:http use-expressions=“true“>
<security:form-login login-page=“/login“/>
<security:logout logout-success-url=“/login“ />
</security:http>
Es ist zu beachten, dass das logout-success-url-Attribut auf eine öffentlich zugängliche Seite – wie hier die Login-Seite – verweisen muss. Andernfalls erhält der Benutzer bei jedem Ausloggen eine 401 – Unauthorized – Meldung vom Server, da er auf eine geschützte Ressource zugreifen möchte, aber nicht länger authentifiziert ist.
Der Vollständigkeit halber wird der Logout-Link in die Navigation aufgenommen, wobei er ausschließlich authentifizierten Benutzern angezeigt werden soll:
<sec:authorize access=“hasAnyRole(‚USER_ROLE‘,’ADMIN_ROLE‘)“>
<a href=“/de.mz.spring.security/j_spring_security_logout“>Logout</a>
</sec:authorize>
Hiermit ist die Web-Applikation vollständig abgesichert. Im Vergleich zu den Vorgängerversionen hat sich einiges getan. So wurden unter Spring-ACEGI die Servlet-Filter noch nicht automatisch initialisiert, sondern mussten explizit in der Spring Konfiguration angegeben werden. Für jede Web-Applikation waren mindestens vier Filter zu konfigurieren:
- IntegrationFilter (jetzt SecurityContextPersistenceFilter)
- AuthenticationProcessingFilter (jetzt UsernamePasswordAuthenticationFilter)
- ExceptionTranslationFilter
- FilterSecurityInterceptor
Es ist äußerst hilfreich, dass der Convention over Configuration Ansatz bei Spring Security so konsequent durchgesetzt wurde. Auch den Spring Security Namespace gab es früher nicht, so dass abzusichernde URLs im FilterSecurityInterceptor definiert werden mussten, statt bequem per intercept-url. Mit Spring Security 3.x lässt sich eine Web-Applikation schnell absichern, ohne dass man sich zwangsläufig mit allen Aspekten des Frameworks vertraut machen muss. Für eine einfache Absicherung sind nur noch wenige Zeilen XML-Konfiguration nötig, aber auch komplexe Szenarien lassen sich mit Spring Security problemlos abbilden. Eine Entscheidende Rolle spielen hierbei die vielen Erweiterungsmöglichkeiten des komplexen Frameworks.