Spring Security mit einer Webanwendung (Tutorial)

Dieser Beitrag ist Teil einer Serie über die Einführung in das Spring Framework und beschreibt den Einsatz von Spring Security in einer Konsolen- und Webanwendung.

Die Struktur des Projektes

Dieses Beispiel baut auf dem vorherigem Beispiel Spring mit einer Webanwendung mit JPA und Validierung in der Version 2.x auf. Die verwendeten Frameworks und Werkzeuge sind hier beschrieben. In diesem Beispiel werden die folgenden Technologien des Spring Frameworks vorgestellt:

  • Die Einführung von Sicherheit (Security) in eine Spring Anwendung.
  • Die Authentifizierung per Kommandozeile und per Webseite.
  • Die Autorisierung von Services mit Annotationen.
  • Das sichere Session-Management (Session Fixation Protection).

In der folgenden Bildergalerie sind die benötigten Bibliotheken für jedes einzelne Eclipse Projekt dargestellt.

Die Literaturempfehlungen für dieses Beispiel

Die Erweiterungen des Projektes test-spring-jpa

Wir beginnen mit der XML Konfiguration für das Projekt test-spring-jpa. Hier wird der zentrale AuthentificationManager definiert. Er verwaltet verschiedene AuthenticationProvider. Der Provider authentifiziert (Überprüfen der Benutzerkennung und Benutzerkennwortes) und autorisiert (Ermitteln der Berechtigungen) den Benutzer über seinen UserDetailsService.

  • In der Zeile 7 wird der Namespace für Spring Security hinzugefügt.
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:sec="http://www.springframework.org/schema/security"
    xsi:schemaLocation="
  • In den Zeilen 15 und 16 wird das Schema für Spring Security angegeben.
        http://www.springframework.org/schema/security
            http://www.springframework.org/schema/security/spring-security.xsd
  • Ab der Zeile 32 bis 43 wird der Authentifizierungsmanager mit dem Provider definiert. Hier werden die Authentifizierungsdaten und die Zugriffsrechte (Rollen) definiert. Die Kennwörter werden hier in diesem Beispiel im Klartext abgelegt.
  • In der Zeile 47 wird der Import der speziellen XML Konfiguration für das Projekt test-spring-jpa angegeben. Diese Konfiguration gilt nur für den ApplicationContext in dem sie geladen wird.
    <!-- Den Authentifikationsmanager definieren -->
    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:password-encoder hash="plaintext" />
            <sec:user-service>
                <sec:user name="admin" password="admin"
                    authorities="ROLE_USER, ROLE_ADMIN" />
                <sec:user name="frank" password="frank"
                    authorities="ROLE_USER" />
            </sec:user-service>
        </sec:authentication-provider>
    </sec:authentication-manager>

    <!-- Die projektspezifischen Konfigurationen laden -->
    <import resource="classpath:/META-INF/spring/db.xml" />
    <import resource="classpath:/META-INF/spring/security.xml" />

Jetzt die neue XML Konfiguration für die Spring Security im Projekt test-spring-jpa.

  • In der Zeile 18 wird die Security definiert. Hier werden die Annotationen (@PreAuthorize, @PostAuthorize, @PreFilter und @PostFilter) für die Security eingeschaltet. Diese können nicht mit der Annotation @Secured kombiniert werden.
<?xml version="1.0" encoding="UTF-8"?>

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

    <description>
        Dieses ist die zentrale Konfiguration für die Security.
    </description>

    <!-- Die Annotations-basierende Security einschalten -->
    <sec:global-method-security mode="aspectj"
        pre-post-annotations="enabled"
        proxy-target-class="true" />

</beans>

Die Klasse Starter muss um eine Authentifizierung des Benutzers erweitert werden.

  • Die neuen Imports sind nicht dargestellt.
  • Ab der Zeile 34 werden die Benutzerinformationen (Kennung und Kennwort) für die Authentifizierung abfragt.
  • In der Zeile 49 wird die Authentifizierung und die Autorisierung am AuthenticationManager durchgeführt.
  • In der Zeile 50 wird bei erfolgreicher Authentifizierung die Autorisierung im SecurityContext über den SecurityContextHolder am aktuellen Thread per ThreadLocal gebunden und steht so der Anwendung zu Verfügung.
        try {
            // Einlesen der Authentisierungsdaten
            BufferedReader in =
                new BufferedReader(new InputStreamReader(System.in));
            System.out.println("Please enter your username:");
            String name = in.readLine();
            System.out.println("Please enter your password:");
            String password = in.readLine();

            // Authentifizierung des Benutzers
            AuthenticationManager authenticationManager =
                ctx.getBean(BeanIds.AUTHENTICATION_MANAGER,
                    AuthenticationManager.class);
            Authentication request =
                new UsernamePasswordAuthenticationToken(name, password);
            Authentication result = authenticationManager.authenticate(request);
            SecurityContextHolder.getContext().setAuthentication(result);
        } catch (Exception exception) {
            logger.error("Fehler bei der Authentifizierung", exception);
            return;
        }

Der Service StandardDrivers wird nun um die Autorisierung erweitert.

  • In der Zeile 22 wird für den Zugriff auf alle Methoden dieses Services die Benutzerrolle USER verlangt.
/**
 * Die Standard-Implementierung des Services {@link Drivers}.
 * @author Frank W. Rahn
 */
@Service("drivers")
@Transactional
@PreAuthorize("hasRole('ROLE_USER')")
public class StandardDrivers implements Drivers {
  • In der Zeile 53 wird zusätzlich für das Erzeugen eines Fahrers die Benutzerrolle ADMIN verlangt.
    @Override
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public Long createDriver(String name, String firstname) {
        Driver driver = new Driver();
        driver.setName(name);
        driver.setFirstname(firstname);

        return create(driver);
    }
  • In der Zeile 67 wird zusätzlich für das Erzeugen eines Fahrers die Benutzerrolle ADMIN verlangt.
    @Override
    @PreAuthorize("hasRole('ROLE_ADMIN')")
    public Long create(Driver driver) {

Die Erweiterungen des Projektes test-spring-web

Ab hier beginnen wir mit den Erweiterungen am Deployment Discriptors der Webanwendung.

  • Ab der Zeile 44 wird ein Servlet Filter für die Security definiert. Dieser Filter wirkt auf alle Requests an diesen Servlet Context.
  • In der Zeile 52 wird der Name der Spring Bean definiert, welche die Kette der vordefinierten Servlet Filter für die Spring Security kennt. Typischerweise wird an Stelle des Init-Parameters der Filtername verwendet. Dieser Name muss dann springSecurityFilterChain lauten.
    <filter>
        <description>Der Filter für die Security</description>
        <filter-name>securityFilter</filter-name>
        <filter-class>
            org.springframework.web.filter.DelegatingFilterProxy
        </filter-class>
        <init-param>
            <param-name>targetBeanName</param-name>
            <param-value>springSecurityFilterChain</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>securityFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
  • Ab der Zeile 77 wird ein Listener für das Abmelden des Benutzers definiert, wenn die HTTP Session ausläuft.
    <listener>
        <description>
            Dieser Listener leitet die Event des HttpSessionListener an den
            ApplicationContext weiter.
        </description>
        <listener-class>
            org.springframework.security.web.session.HttpSessionEventPublisher
        </listener-class>
    </listener>

Nun die Änderung an der XML Konfiguration der fachlichen Spring Beans. Diese Basiert auf den Änderungen für das Projekt test-spring-jpa in diesem Artikel.

  • Nicht dargestellt sind die Definitionen für den Namespace von Spring Security.
  • Ab der Zeile 32 die Definitionen für den Authentifizierungsmanager, wie für das Projekt test-spring-jpa.
    <!-- Den Authentifikationsmanager definieren -->
    <sec:authentication-manager>
        <sec:authentication-provider>
            <sec:password-encoder hash="plaintext" />
            <sec:user-service>
                <sec:user name="admin" password="admin"
                    authorities="ROLE_USER, ROLE_ADMIN" />
                <sec:user name="frank" password="frank"
                    authorities="ROLE_USER" />
            </sec:user-service>
        </sec:authentication-provider>
    </sec:authentication-manager>
  • Ab der Zeile 46 werden die Definitionen für die spezielle Spring Security des HTTP Zugriffs hinzugefügt.
  • In der Zeile 48 wird der Zugriff auf die Anmeldemaske für alle Benutzer freigegeben.
  • In der Zeile 55 werden alle Seiten dieser Webanwendung für den Zugriff eines authentifizierten Benutzers mit der Rolle USER beschränkt. Der Ausdruck hasRole() kann verwendet werden, da dieses in der Zeile 46 eingeschaltet wurde.
  • In der Zeile 56 wird die Seite definiert, die im Fehlerfall aufgerufen wird. In diesem Fall wird wieder die Anmeldemaske angezeigt. Zusätzlich wird durch den Parameter login_error=true eine Fehlermeldung zum vorliegendem Fehlerfall ausgegeben.
  • In der Zeile 59 wird der Link für die Abmeldung definiert. Wenn dieser Link aufgerufen wird, wird der authentifizierte Benutzer vom System abgemeldet und auf die Anmeldemaske weitergeleitet.
  • In der Zeile 63 wird die Seite definiert, die beim Timeout der Session aufgerufen wird. In diesem Fall auch wieder die Anmeldemaske. Durch den Parameter timeout=true wird eine entsprechende Fehlermeldung ausgegeben.
  • In der Zeile 64 wird die Session Fixation Protection über eine Migration der Session definiert. Dazu werden die Attribute aus der aktuellen Session gesichert und diese Session invalidiert (verworfen). Danach wird eine neue Session erzeugt und die Attribute in diese Session kopiert. Die ursprüngliche Session kann durch einen Angreifer nicht mehr verwendet werden.
  • In der Zeile 67 wird die Anzahl der Anmeldungen pro Benutzer auf eine Session beschränkt. D. H. falls der Benutzer sich von einem anderem Arbeitsplatz anmelden möchte, erhält er eine Fehlermeldung.
    <!-- Konfigurieren der HTTP Security -->
    <sec:http auto-config="true" use-expressions="true">
        <!-- Zugriff auf /login für alle erlauben -->
        <sec:intercept-url pattern="/login" access="permitAll" />

        <!-- Zugriff auf /** auf Benutzer mit der
            Rolle ROLE_USER einschränken -->
        <sec:intercept-url pattern="/**" access="hasRole('ROLE_USER')" />

        <!-- Formular-basiertes Login konfigurieren -->
        <sec:form-login login-page="/login"
            authentication-failure-url="/login?login_error=true" />

        <!-- Logout Seite definieren -->
        <sec:logout logout-url="/logout" />

        <!-- Session Timeout Seite setzen und
            Session Fixation Attack Protection einschalten -->
        <sec:session-management invalid-session-url="/login?timeout=true"
            session-fixation-protection="migrateSession">

            <!-- Maxmale Anzahl von Session per User (Doppelanmeldung) -->
            <sec:concurrency-control max-sessions="1"
                error-if-maximum-exceeded="true" />
        </sec:session-management>
    </sec:http>
  • In der Zeile 74 wird der Import der speziellen XML Konfiguration für die Security für das Projekt test-spring-web hinzugefügt.
    <!-- Die projektspezifischen Konfigurationen laden -->
    <import resource="classpath:/META-INF/spring/db.xml" />
    <import resource="classpath:/META-INF/spring/security.xml" />
    <import resource="classpath:/de/rahn/validation/validation.xml" />

Im DriversController wird über eine Erweiterung des Logings, ab der Zeile 45, der angemeldete Benutzer ausgegeben.

    @ModelAttribute("driver")
    public Driver handleRequest(@RequestParam(required = false) Long id) {
        Authentication authentication =
            SecurityContextHolder.getContext().getAuthentication();
        Object principal =
            authentication == null ? null : authentication.getPrincipal();

        logger
            .info(
                "Die Methode DriversController.handleRequest() wurde aufgerufen. id={}, user={}",
                id, principal);

        if (id != null) {

Für das Anmelden muss ein neuer Controller erstellt werden.

package de.rahn.web.spring;

import static org.slf4j.LoggerFactory.getLogger;

import org.slf4j.Logger;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * Der Controller für das Login und Logout.
 * @author Frank W. Rahn
 */
@Controller
public class LoginLogoutController {

    private static final Logger logger = getLogger(LoginLogoutController.class);

    /**
     * Die Methode für das Login.
     */
    @RequestMapping("/login")
    public void login() {
        logger.info("Benutzer hat die Anmeldeseite aufgerufen.");
    }

}

Für das Abmelden muss ein zusätzlicher Link in die Startseite aufgenommen werden.

  • In der Zeile 2 wird die JavaServer Pages Standard Tag Library hinzugefügt.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
  • Ab der Zeile 18 wird der zusätzliche Link aufgebaut und ausgegeben.
        <c:url var="logoutUrl" value="/logout" />
            <li><a href="${logoutUrl}">Abmelden</a></li>

Zum Schluss noch die Formular-basierende Anmeldemaske für die Webanwendung.

  • In der Zeile 11 wird der Fokus auf das erste Eingabefelde für die Benutzerkennung gesetzt.
  • In den Zeilen 13 bis 19 wird die Fehlermeldung der Anmeldung behandelt.
  • In den Zeilen 20 bis 26 wird die Meldung für das Ablaufen des Timeouts ausgegeben.
  • In den Zeilen 28 bis 35 befindet sich das Formular für die Eingaben von Benutzerkennung und dem Kennwort. Die Eingaben werden durch das Standard-Servlet j_spring_security_check verarbeitet.
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ page import="org.springframework.security.web.WebAttributes" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title>Anmeldung | Frank W. Rahn</title>
    </head>
    <body onload="document.login.j_username.focus();">
        <h1>Anmeldung</h1>
        <c:if test="${not empty param.login_error}">
            <fieldset>
                <legend>Fehler bei Login</legend>
                <div style="color: red;">${SPRING_SECURITY_LAST_EXCEPTION.message}</div>
            </fieldset>
            <br/>
        </c:if>
        <c:if test="${not empty param.timeout}">
            <fieldset>
                <legend>Timeout</legend>
                <div style="color: red;">Ihre Benutzersitzung ist abgelaufen oder wurde beendet.</div>
            </fieldset>
            <br/>
        </c:if>
        <c:url var="url" value="j_spring_security_check" />
        <form action="${url}" method="post">
            <fieldset>
                <p><label for="j_username">Benutzerkennung: </label><input type="text" id="j_username" name="j_username" /></p>
                <p><label for="j_password">Kennwort: </label><input type="password" id="j_password" name="j_password" /></p>
                <input name="submit" type="submit" value="Anmelden" />
                <input name="reset" type="reset" value="Zurücksetzen" />
            </fieldset>
        </form>
    </body>
</html>

Die Masken

In folgender Bildgalerie werden die Screenshots der Masken der Webanwendung im Browser dargestellt.

Der Quellcode und Download des Beispiels

Den Quellcode ansehen bei GitHub:
Spring mit JPA und Hibernate
Spring mit einer einfachen Webanwendung

Der Download einer ZIP-Datei von GitHub:
Spring mit JPA und Hibernate
Spring mit einer einfachen Webanwendung

Update am 28.09.2012

Es wurden folgende Änderungen an allen Projekten vorgenommen.

  1. Die Projekte wurden aus meinem lokalen Apache Subversion Repository in meine öffentlichen GitHub Repositories verschoben.
  2. Die folgenden typischen Anpassungen an Git wurden an allen Projekten durchgeführt.
    • .directory gelöscht
    • .gitignore hinzugefügt
    • README.md hinzugefügt
    • COPYRIGHT.md hinzugefügt
    • In jedem Repository wurde für jeden Beitrag der Serie ein Branch angelegt.
      • develop-spring-an-einem-einfachen-beispiel
      • develop-spring-mit-aop
      • develop-spring-mit-jpa-und-hibernate
      • develop-spring-mit-einer-einfachen-webanwendung
      • develop-spring-mit-einer-webanwendung-mit-jpa-und-validierung
      • develop-spring-security-mit-einer-webanwendung
      • develop-spring-mit-restful-webservice

Updates von 06.10.2012 bis zum 14.10.2012

Es wurden folgende Änderungen an allen Projekten vorgenommen.

  1. Aktualisierung des OpenJDK auf die Version 1.7.0.
  2. Aktualisierung der Entwicklungsumgebung Eclipse auf die Version 4.2.1.
    Die benötigten Plugins aus dem Eclipse Marketplace:
  3. Aktualisierung der Datei pom.xml:
    • Anpassungen an die OpenJDK Version
    • GitHub Einträge (SCM und URL) hinzugefügt
    • Aktualisierung der Libraries auf aktuellerer Versionen (z. B. Spring Version 3.1.2.RELEASE, Hibernate Version 4.1.7.Final, JUnit Version 4.10, mockito Version 1.9.0, …)
      Die genaueren Versionen bitte aus den jeweiligen pom.xml auf GitHub entnehmen.
Frank Rahn
Letzte Artikel von Frank Rahn (Alle anzeigen)
0 Kommentare

Hinterlasse einen Kommentar

An der Diskussion beteiligen?
Hinterlasse uns deinen Kommentar!

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Ihre E-Mail-Adresse wird nicht veröffentlicht. Ihr Kommentar wird verschlüsselt an meinen Server gesendet. Erforderliche Felder sind mit * markiert.

Weitere Informationen und Widerrufshinweise finden Sie in meiner Datenschutzerklärung.