Skip to content

19. Sichern des Zugriffs auf einen Webdienst mit Spring Security

19.1. Die Rolle von Spring Security in einer Webanwendung

Betrachten wir Spring Security im Kontext der Entwicklung einer Webanwendung. Meistens basiert diese auf einer mehrschichtigen Architektur wie der folgenden:

  • Die Schicht [Spring Security] gewährt nur autorisierten Benutzern Zugriff auf die Schicht [web].

19.2. Ein Tutorial zu Spring Security

Wir werden erneut ein Spring-Handbuch importieren, indem wir die folgenden Schritte 1 bis 3 ausführen:

  

Das Projekt besteht aus folgenden Elementen:

  • Im Ordner „[templates]“ befinden sich die Projektdateien „HTML“;
  • [Application]: ist die ausführbare Klasse des Projekts;
  • [MvcConfig]: ist die Spring-Konfigurationsklasse MVC;
  • [WebSecurityConfig]: ist die Spring-Security-Konfigurationsklasse;

19.2.1. Maven-Konfiguration

Das Projekt [3] ist ein Maven-Projekt. Sehen wir uns die Datei [pom.xml] an, um die Abhängigkeiten zu ermitteln:


<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.springframework</groupId>
    <artifactId>gs-securing-web</artifactId>
    <version>0.1.0</version>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.2.3.RELEASE</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <!-- tag::security[] -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- end::security[] -->
    </dependencies>

    <properties>
        <start-class>hello.Application</start-class>
    </properties>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>
  • Zeilen 10–14: Das Projekt ist ein Spring-Boot-Projekt;
  • Zeilen 17–20: Abhängigkeit vom Framework [Thymeleaf];
  • Zeilen 22–25: Abhängigkeit vom Spring Security-Framework;

19.2.2. Die Thymeleaf-Ansichten

  

Die Ansicht [home.html] sieht wie folgt aus:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <h1>Welcome!</h1>

    <p>
        Click <a th:href="@{/hello}">here</a> to see a greeting.
    </p>
</body>
</html>
  • Zeile 12: Das Attribut [th:href="@{/hello}"] generiert das Attribut [href] des Tags <a>. Der Wert [@{/hello}] generiert den Pfad [<context>/hello], wobei [context] der Kontext der Webanwendung ist;

Der generierte Code HTML lautet wie folgt:


<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example</title>
    </head>
    <body>
        <h1>Welcome!</h1>

        <p>
            Click
            <a href="/hello">here</a>
            to see a greeting.
        </p>
    </body>
</html>

Die Ansicht [hello.html] sieht wie folgt aus:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Hello World!</title>
</head>
<body>
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
    </form>
</body>
</html>
  • Zeile 9: Das Attribut [th:inline="text"] generiert den Text des Tags <h1>. Dieser Text enthält einen $-Ausdruck, der ausgewertet werden muss. Das Element [[${#httpServletRequest.remoteUser}]] ist der Wert des Attributs [RemoteUser] der aktuellen Abfrage HTTP. Es handelt sich dabei um den Namen des angemeldeten Benutzers;
  • Zeile 10: ein Formular HTML. Das Attribut [th:action="@{/logout}"] generiert das Attribut [action] des Tags [form]. Der Wert [@{/logout}] generiert den Pfad [<context>/logout], wobei [context] der Kontext der Webanwendung ist;

Der generierte Code HTML lautet wie folgt:


<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1>Hello user!</h1>
        <form method="post" action="/logout">
            <input type="submit" value="Sign Out" />
            <input type="hidden" name="_csrf" value="b152e5b9-d1a4-4492-b89d-b733fe521c91" />
        </form>
    </body>
</html>
  • Zeile 8: die Übersetzung von „Hello [categorie[2]]!“;
  • Zeile 9: die Übersetzung von @{/logout};
  • Zeile 11: ein verstecktes Feld mit dem Namen (Attribut name) _csrf;

Die letzte Ansicht [login.html] sieht wie folgt aus:

  

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:th="http://www.thymeleaf.org"
    xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
<title>Spring Security Example</title>
</head>
<body>
    <div th:if="${param.error}">Invalid username and password.</div>
    <div th:if="${param.logout}">You have been logged out.</div>
    <form th:action="@{/login}" method="post">
        <div>
            <label> User Name : <input type="text" name="username" />
            </label>
        </div>
        <div>
            <label> Password: <input type="password" name="password" />
            </label>
        </div>
        <div>
            <input type="submit" value="Sign In" />
        </div>
    </form>
</body>
</html>
  • Zeile 9: Das Attribut [th:if="${param.error}"] sorgt dafür, dass das Tag <div> nur dann generiert wird, wenn das URL, das die Anmeldeseite anzeigt, den Parameter [error] (http://context/login?error) enthält;
  • Zeile 10: Das Attribut [th:if="${param.logout}"] bewirkt, dass das Tag <div> nur dann generiert wird, wenn das URL, das die Anmeldeseite anzeigt, den Parameter [logout] (http://context/login?logout) enthält;
  • Zeilen 11–23: ein Formular HTML;
  • Zeile 11: Das Formular wird an URL [<context>/login] gesendet, wobei <context> der Kontext der Webanwendung ist;
  • Zeile 13: ein Eingabefeld mit dem Namen [username];
  • Zeile 17: ein Eingabefeld mit dem Namen [password];

Der generierte Code HTML lautet wie folgt:


<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example </title>
    </head>
    <body>

        <div>
            You have been logged out.
        </div>
        <form method="post" action="/login">
            <div>
                <label>
                    User Name :
                    <input type="text" name="username" />
                </label>
            </div>
            <div>
                <label>
                    Password:
                    <input type="password" name="password" />
                </label>
            </div>
            <div>
                <input type="submit" value="Sign In" />
            </div>
            <input type="hidden" name="_csrf" value="ef809b0a-88b4-4db9-bc53-342216b77632" />
        </form>
    </body>
</html>

In Zeile 28 ist zu beachten, dass Thymeleaf ein verstecktes Feld mit dem Namen [_csrf] hinzugefügt hat.

19.2.3. Spring-Konfiguration MVC

  

Die Klasse [MvcConfig] konfiguriert das Spring-Framework MVC:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class MvcConfig extends WebMvcConfigurerAdapter {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }

}
  • Zeile 7: Die Annotation [@Configuration] macht die Klasse [MvcConfig] zu einer Konfigurationsklasse;
  • Zeile 8: Die Klasse [MvcConfig] erweitert die Klasse [WebMvcConfigurerAdapter], um bestimmte Methoden neu zu definieren;
  • Zeile 10: Neudefinition einer Methode der übergeordneten Klasse;
  • Zeilen 11–16: Die Methode [addViewControllers] ermöglicht es, URL mit Ansichten HTML zu verknüpfen. Dabei werden folgende Verknüpfungen hergestellt:
URL
Ansicht
/, /home
/templates/home.html
/hello
/templates/hello.html
/login
/templates/login.html

Die Endung [html] und der Ordner [templates] sind die von Thymeleaf verwendeten Standardwerte. Sie können über die Konfiguration geändert werden. Der Ordner [templates] muss sich im Stammverzeichnis des Classpaths des Projekts befinden:

Oberhalb von [1] befinden sich die Ordner [java] und [resources], die beide Quellordner (source folders) sind. Das bedeutet, dass sich ihr Inhalt im Stammverzeichnis des Classpaths des Projekts befindet. In [2] befinden sich die Ordner [hello] und [templates] also im Stammverzeichnis des Classpaths.

19.2.4. Konfiguration von Spring Security

  

Die Klasse [WebSecurityConfig] konfiguriert das Spring Security-Framework:


package hello;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;

@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests().antMatchers("/", "/home").permitAll().anyRequest().authenticated();
        http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }
}
  • Zeile 9: Die Annotation [@Configuration] macht die Klasse [WebSecurityConfig] zu einer Konfigurationsklasse;
  • Zeile 10: Die Annotation [@EnableWebSecurity] macht die Klasse [WebSecurityConfig] zu einer Spring-Security-Konfigurationsklasse;
  • Zeile 11: Die Klasse [WebSecurity] erweitert die Klasse [WebSecurityConfigurerAdapter], um bestimmte Methoden neu zu definieren;
  • Zeile 12: Neudefinition einer Methode der übergeordneten Klasse;
  • Zeilen 13–16: Die Methode [configure(HttpSecurity http)] wird neu definiert, um die Zugriffsrechte auf die verschiedenen URL der Anwendung festzulegen;
  • Zeile 14: Die Methode [http.authorizeRequests()] ermöglicht es, URL mit Zugriffsrechten zu verknüpfen. Dabei werden folgende Verknüpfungen hergestellt:
URL
Regel
Code
/, /home
Zugriff ohne Authentifizierung

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
Zugriff nur nach Authentifizierung
http.anyRequest().authenticated();
  • Zeile 15: Legt die Authentifizierungsmethode fest. Die Authentifizierung erfolgt über ein Formular (URL, [/login]), das für alle zugänglich ist ([http.formLogin().loginPage("/login").permitAll()]). Die Abmeldung (Logout) ist ebenfalls für alle zugänglich;
  • Zeilen 19–21: definieren die Methode [configure(AuthenticationManagerBuilder auth)] neu, die die Benutzer verwaltet;
  • Zeile 20: Die Authentifizierung erfolgt mit fest definierten Benutzern [auth.inMemoryAuthentication()]. Ein Benutzer wird hier mit dem Login [user], dem Passwort [password] und der Rolle [USER] definiert. Benutzern mit derselben Rolle können die gleichen Rechte gewährt werden;

19.2.5. Ausführbare Klasse

  

Die Klasse [Application] lautet wie folgt:


package hello;

import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {

    public static void main(String[] args) throws Throwable {
        SpringApplication.run(Application.class, args);
    }

}
  • Zeile 8: Die Annotation [@EnableAutoConfiguration] weist Spring Boot (Zeile 3) an, die Konfiguration vorzunehmen, die der Entwickler nicht explizit vorgenommen hat;
  • Zeile 9: macht die Klasse [Application] zu einer Spring-Konfigurationsklasse;
  • Zeile 10: Fordert das Durchsuchen des Verzeichnisses der Klasse [Application] an, um nach Spring-Komponenten zu suchen. Die beiden Klassen [MvcConfig] und [WebSecurityConfig] werden somit erkannt, da sie die Annotation [@Configuration] tragen;
  • Zeile 13: Die Methode [main] der ausführbaren Klasse;
  • Zeile 14: Die statische Methode [SpringApplication.run] wird mit der Konfigurationsklasse [Application] als Parameter ausgeführt. Wir sind diesem Prozess bereits begegnet und wissen, dass der in den Maven-Abhängigkeiten des Projekts enthaltene eingebettete Tomcat-Server gestartet und das Projekt darauf bereitgestellt wird. Wir haben gesehen, dass vier URL von [/, /home, /login, /hello] verwaltet wurden und dass einige durch Zugriffsrechte geschützt waren.

19.2.6. Tests der Anwendung

Beginnen wir damit, die URL [/] abzufragen, die eine der vier akzeptierten URL ist. Sie ist mit der Ansicht [/templates/home.html] verknüpft:

 

Die angeforderte URL [/] ist für alle zugänglich. Deshalb haben wir sie erhalten. Der Link [here] lautet wie folgt:

Click <a href="/hello">here</a> to see a greeting.

Die URL [/hello] wird angefordert, sobald man auf den Link klickt. Diese ist geschützt:

URL
Regel
Code
/, /home
Zugriff ohne Authentifizierung

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
Zugriff nur nach Authentifizierung
http.anyRequest().authenticated();

Um darauf zugreifen zu können, muss man authentifiziert sein. Spring Security leitet den Browser des Clients dann auf die Authentifizierungsseite weiter. Gemäß der angezeigten Konfiguration handelt es sich dabei um die Seite URL [/login]. Diese ist für alle zugänglich:


http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();

Wir erhalten also [1]:

Der Quellcode der erhaltenen Seite lautet wie folgt:

<!DOCTYPE html>

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
...
    <form method="post" action="/login">
...
       <input type="hidden" name="_csrf" value="87bea06a-a177-459d-b279-c6068a7ad3eb" />
   </form>
</body>
</html>
  • In Zeile 7 erscheint ein verstecktes Feld, das auf der ursprünglichen Seite [login.html] nicht vorhanden ist. Es wurde von Thymeleaf hinzugefügt. Dieser Code mit der Bezeichnung CSRF (Cross-Site-Request-Forgery) dient dazu, eine Sicherheitslücke zu schließen. Dieses Token muss zusammen mit der Authentifizierung an Spring Security zurückgesendet werden, damit diese akzeptiert wird;

Wir erinnern uns, dass von Spring Security nur der Benutzer „user/password“ erkannt wird. Wenn wir in [2] etwas anderes eingeben, erhalten wir dieselbe Seite mit einer Fehlermeldung in [3]. Spring Security hat den Browser auf die Seite URL [http://localhost:8080/login?error] umgeleitet. Das Vorhandensein des Parameters [error] hat die Anzeige des Tags ausgelöst:


<div th:if="${param.error}">Invalid username and password.</div>

Geben wir nun die erwarteten Werte für „user“ und „password“ ein: [4]:

  • in [4] melden wir uns an;
  • in [5] leitet uns Spring Security zu URL und [/hello] weiter, da dies die URL ist, die wir angefordert hatten, als wir zur Anmeldeseite weitergeleitet wurden. Die Identität des Benutzers wurde in der folgenden Zeile von [hello.html] angezeigt:
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

Auf der Seite [5] wird das folgende Formular angezeigt:


    <form th:action="@{/logout}" method="post">
        <input type="submit" value="Sign Out" />
</form>

Wenn man auf die Schaltfläche [Sign Out] klickt, wird ein POST auf der Seite URL [/logout] erstellt. Diese ist ebenso wie die Dateien „URL“ und „[/login]“ für alle zugänglich:


http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();

In unserer Zuordnung URL / Ansichten haben wir für die URL und [/logout] nichts definiert. Was wird passieren? Probieren wir es aus:

  • in [6] klicken wir auf die Schaltfläche [Sign Out];
  • in [7] sehen wir, dass wir zu URL [http://localhost:8080/login?logout] weitergeleitet wurden. Diese Weiterleitung wurde von Spring Security angefordert. Das Vorhandensein des Parameters [logout] in URL hat dazu geführt, dass die folgende Zeile in der Ansicht angezeigt wurde:

<div th:if="${param.logout}">You have been logged out.</div>

19.2.7. Fazit

Im vorangegangenen Beispiel hätten wir die Webanwendung zunächst schreiben und anschließend sichern können. Spring Security ist nicht aufdringlich. Man kann die Sicherheit einer bereits geschriebenen Webanwendung nachträglich einrichten. Außerdem haben wir Folgendes festgestellt:

  • Es ist möglich, eine Authentifizierungsseite zu definieren;
  • Die Authentifizierung muss mit dem von Spring Security ausgegebenen Token CSRF erfolgen;
  • Wenn die Authentifizierung fehlschlägt, wird man zur Authentifizierungsseite weitergeleitet, wobei zusätzlich ein „error“-Parameter im Token „URL“ enthalten ist;
  • Wenn die Authentifizierung erfolgreich ist, wird man nach der Authentifizierung auf die angeforderte Seite weitergeleitet. Wenn die Authentifizierungsseite direkt aufgerufen wird, ohne eine Zwischenseite zu durchlaufen, leitet Spring Security uns zu den URL und [/] weiter (dieser Fall wurde nicht vorgestellt);
  • die Abmeldung erfolgt durch Aufruf der Seite URL [/logout] mit einem POST. Spring Security leitet uns dann mit dem Parameter „logout“ im URL auf die Authentifizierungsseite weiter;

All diese Schlussfolgerungen basieren auf dem Standardverhalten von Spring Security. Dieses Verhalten lässt sich durch Konfiguration ändern, indem bestimmte Methoden der Klasse [WebSecurityConfigurerAdapter] neu definiert werden.

Das vorherige Tutorial wird uns im weiteren Verlauf kaum helfen. Wir werden nämlich Folgendes verwenden:

  • eine Datenbank zum Speichern der Benutzer, ihrer Passwörter und ihrer Rollen;
  • eine Authentifizierung über den Header HTTP;

Es gibt relativ wenige Tutorials für das, was wir hier umsetzen wollen. Die Lösung, die vorgeschlagen wird, ist eine Zusammenstellung von Code, der hier und da gefunden wurde.