Skip to content

16. [Cours]: Den Zugriff auf einen Webdienst mit Spring Security sichern

Stichworte: mehrschichtige Architektur, Spring, Dependency Injection, Webdienst / gesicherter jSON, Client / Server

16.1. Support

 

Die Projekte dieses Kapitels befinden sich im Ordner [support / chap-16]. Das Skript SQL dient dazu, die für die Tests erforderliche Datenbank zu erstellen.

16.2. Die Rolle von Spring Security in einer Webanwendung

Betrachten wir die Rolle von Spring Security bei 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].

16.3. Ein Tutorial zu Spring Security

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

  

Das Projekt besteht aus folgenden Elementen:

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

16.3.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;

16.3.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 [[${#httpServletRequest.remoteUser}]]!“;
  • Zeile 9: die Übersetzung von @{/logout};
  • Zeile 11: ein verstecktes Feld mit dem Namen (Attribut name) _csrf;

Die 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}"] bewirkt, 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 das 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.

16.3.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.

16.3.4. Spring Security-Konfiguration

  

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;

16.3.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 Instanzen von URL von [/, /home, /login, /hello] verwaltet wurden und dass einige davon durch Zugriffsrechte geschützt waren.

16.3.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
Nur für authentifizierte Benutzer
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 nur der Benutzer user/password von Spring Security erkannt wird. Wenn wir etwas anderes in [2] 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;
  • bei [5] leitet uns Spring Security zu URL [/hello] weiter, da dies die Seite 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>

Die Seite [5] zeigt das folgende Formular an:


    <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];
  • zu [7]: Wir sehen, dass wir zu URL [http://localhost:8080/login?logout] weitergeleitet wurden. Diese Weiterleitung wurde von Spring Security veranlasst. 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>

16.3.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 zu der Seite weitergeleitet, die zum Zeitpunkt der Authentifizierung angefordert wurde. 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 vorgeschlagene Lösung ist eine Zusammenstellung von Code, der hier und da gefunden wurde.

16.4. Einrichtung der Sicherheit für den Webdienst / JSON der Produkte

16.4.1. Die Datenbank

Die Datenbank [dbintrospringdata] wird erweitert, um Benutzer, deren Passwörter und Rollen zu berücksichtigen. Es kommen drei neue Tabellen hinzu:

Image

Tabelle [USERS]: die Benutzer

  • ID: Primärschlüssel;
  • VERSION: Spalte für die Versionsverwaltung der Zeile;
  • IDENTITY: eine beschreibende Kennung des Benutzers;
  • LOGIN: der Benutzername des Benutzers;
  • PASSWORD: sein Passwort;

In der Tabelle USERS werden Passwörter nicht im Klartext gespeichert:

 

Der Algorithmus, der die Passwörter verschlüsselt, ist der Algorithmus BCRYPT.

Tabelle [ROLES]: die Rollen

  • ID: Primärschlüssel;
  • VERSION: Versionsspalte der Zeile;
  • NAME: Name der Rolle. Standardmäßig erwartet Spring Security Namen in der Form ROLE_XX, zum Beispiel ROLE_ADMIN oder ROLE_GUEST;
 

Tabelle [USERS_ROLES]: Verknüpfungstabelle USERS / ROLES

Ein Benutzer kann mehrere Rollen haben, eine Rolle kann mehrere Benutzer umfassen. Es handelt sich um eine Mehr-zu-Mehr-Beziehung, die durch die Tabelle [USERS_ROLES] abgebildet wird.

  • ID: Primärschlüssel;
  • VERSION: Versionsspalte der Zeile;
  • USER_ID: ID eines Benutzers;
  • ROLE_ID: ID einer Rolle;
 

16.4.2. Das Eclipse-Projekt

Wir erstellen das folgende Eclipse-Projekt:

1
  

  • in [1]: das neue Projekt mit den folgenden Paketen:
    • [spring.security.entities]: enthält die Entitäten JPA, die den drei neuen Tabellen der Datenbank entsprechen;
    • [spring.security.repositories]: enthält die Spring-Data-[repositories]-Komponenten, die den drei neuen Tabellen zugeordnet sind;
    • [spring.security.dao]: enthält einen Dienst, der auf den [repositories] basiert;
    • [spring.security.config]: enthält die Projektkonfiguration, insbesondere die Konfiguration für den sicheren Zugriff auf den Webdienst;
    • [spring.security.boot]: enthält die Startklasse für den gesicherten Webdienst;

16.4.3. Die Maven-Konfiguration

Das neue Projekt ist ein Maven-Projekt, das durch die folgende Datei „[pom.xml]“ konfiguriert wird:


<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>istia.st.spring.security</groupId>
    <artifactId>intro-spring-security-server-01</artifactId>
    <version>0.0.1-SNAPSHOT</version>

    <name>intro-spring-security-server-01</name>
    <description>démo spring security</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <java.version>1.8</java.version>
    </properties>

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

    <dependencies>
        <dependency>
            <groupId>istia.st.webjson</groupId>
            <artifactId>intro-server-webjson-01</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
        <!-- Spring Security -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <!-- Spring-Protokolle -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot</artifactId>
        </dependency>
        <!-- Spring Boot-Test -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <!-- Plugins -->
    <build>
        <plugins>
            <plugin>
                <artifactId>maven-assembly-plugin</artifactId>
                <configuration>
                    <descriptorRefs>
                        <descriptorRef>jar-with-dependencies</descriptorRef>
                    </descriptorRefs>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>2.18.1</version>
            </plugin>
        </plugins>
    </build>

</project>
  • Zeilen 23–27: Hier wird die bestehende Konfiguration mit dem untersuchten Webdienst-Archiv / JSON übernommen;
  • Zeilen 29–32: Die Abhängigkeit, die die Spring-Security-Klassen bereitstellt;
  • Zeilen 34–37: die Logging-Bibliothek;
  • Zeilen 39–42: die Bibliothek zur Verwendung der Spring-Boot-Annotationen;
  • Zeilen 44–48: die für die Tests erforderliche Bibliothek;

16.4.4. Die neuen Entitäten [JPA]

Die Schicht JPA definiert drei neue Entitäten:

  

Die Klasse [User] ist das Abbild der Tabelle [USERS]:


package spring.security.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

import spring.data.entities.AbstractEntity;

@Entity
@Table(name = "USERS")
public class User extends AbstractEntity {

    // Eigenschaften
    @Column(name = "NAME")
    private String name;
    @Column(name = "LOGIN")
    private String login;
    @Column(name = "PASSWORD")
    private String password;

    // Konstruktor
    public User() {
    }

    public User(String name, String login, String password) {
        this.name = name;
        this.login = login;
        this.password = password;
    }

    // Getter und Setter
...
}
  • Zeile 11: Die Klasse erweitert die Klasse [AbstractEntity], die bereits für die anderen Entitäten verwendet wird;

Die Klasse [Role] ist das Abbild der Tabelle [ROLES]:


package spring.security.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Table;

import spring.data.entities.AbstractEntity;

@Entity
@Table(name = "ROLES")
public class Role extends AbstractEntity {

    // Eigenschaften
    @Column(name="NAME")
    private String name;

    // Konstruktoren
    public Role() {
    }

    public Role(String name) {
        this.name = name;
    }

    // Getter und Setter
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

Die Klasse [UserRole] ist das Spiegelbild der Tabelle [USERS_ROLES]:


package spring.security.entities;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

import spring.data.entities.AbstractEntity;

@Entity
@Table(name = "USERS_ROLES")
public class UserRole extends AbstractEntity {

    // Fremdschlüssel
    @Column(name = "USER_ID", insertable = false, updatable = false)
    private Long userId;
    @Column(name = "ROLE_ID", insertable = false, updatable = false)
    private Long roleId;

    // Ein UserRole verweist auf einen User
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;

    // Ein UserRole verweist auf eine Rolle
    @ManyToOne
    @JoinColumn(name = "ROLE_ID")
    private Role role;

    // Konstruktoren
    public UserRole() {

    }

    public UserRole(User user, Role role) {
        this.user = user;
        this.role = role;
    }

    // Getter und Setter
...
}

  • Zeilen 22–24: definieren den Fremdschlüssel von der Tabelle [USERS_ROLES] zur Tabelle [USERS];
  • Zeilen 27–29: definieren den Fremdschlüssel von der Tabelle [USERS_ROLES] zur Tabelle [ROLES];

16.4.5. Die [repositories]

Jede der oben genannten Entitäten JPA wird von einem Spring Data-Objekt [repository] verwaltet:

  

Die Schnittstelle [UserRepository] verwaltet den Zugriff auf die Entitäten [User]:


package spring.security.repositories;

import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;

import spring.security.entities.Role;
import spring.security.entities.User;

public interface UserRepository extends CrudRepository<User, Long> {

    // Liste der Rollen eines Benutzers, der durch seine ID identifiziert wird
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);

    // Liste der Rollen eines Benutzers, der durch sein eindeutiges Login identifiziert wird
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);

    // Suche nach einem Benutzer anhand seines Logins
    User findUserByLogin(String login);
}
  • Zeile 9: Die Schnittstelle [UserRepository] erweitert die Schnittstelle [CrudRepository] von Spring Data (Zeile 4);
  • Zeilen 12–13: Die Methode [getRoles(User user)] ermöglicht es, alle Rollen eines Benutzers abzurufen, der durch seine [id] identifiziert wird
  • Zeilen 16–17: dasselbe, jedoch für einen Benutzer, der über seinen Benutzernamen und sein Passwort identifiziert wird;
  • Zeile 20: zum Auffinden eines Benutzers anhand seines Logins;

Die Schnittstelle [RoleRepository] verwaltet den Zugriff auf die Entitäten [Role]:


package spring.security.repositories;

import org.springframework.data.repository.CrudRepository;

import spring.security.entities.Role;

public interface RoleRepository extends CrudRepository<Role, Long> {

    // Suche nach einer Rolle anhand ihres Namens
    Role findRoleByName(String name);

}
  • Zeile 7: Die Schnittstelle [RoleRepository] erweitert die Schnittstelle [CrudRepository];
  • Zeile 10: Eine Rolle kann anhand ihres Namens gesucht werden;

Die Schnittstelle [UserRoleRepository] verwaltet den Zugriff auf die Entitäten [UserRole]:


package spring.security.repositories;

import org.springframework.data.repository.CrudRepository;

import spring.security.entities.UserRole;

public interface UserRoleRepository extends CrudRepository<UserRole, Long> {

}
  • Zeile 5: Die Schnittstelle [UserRoleRepository] erweitert lediglich die Schnittstelle [CrudRepository], ohne ihr neue Methoden hinzuzufügen;

16.4.6. Die Klassen zur Verwaltung von Benutzern und Rollen

  

Spring Security schreibt die Erstellung einer Klasse vor, die die folgende Schnittstelle [UsersDetail] implementiert:

 

Diese Schnittstelle wird hier durch die Klasse [AppUserDetails] implementiert:


package spring.security.dao;

import java.util.ArrayList;
import java.util.Collection;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import spring.security.entities.Role;
import spring.security.entities.User;
import spring.security.repositories.UserRepository;

public class AppUserDetails implements UserDetails {

    private static final long serialVersionUID = 1L;

    // Eigenschaften
    private User user;
    private UserRepository userRepository;

    // Konstruktoren
    public AppUserDetails() {
    }

    public AppUserDetails(User user, UserRepository userRepository) {
        this.user = user;
        this.userRepository = userRepository;
    }

    // -------------------------Schnittstelle
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        Collection<GrantedAuthority> authorities = new ArrayList<>();
        for (Role role : userRepository.getRoles(user.getId())) {
            authorities.add(new SimpleGrantedAuthority(role.getName()));
        }
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getLogin();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    // Getter und Setter
    ...
}
  • Zeile 14: Die Klasse [AppUserDetails] implementiert die Schnittstelle [UserDetails];
  • Zeilen 19–20: Die Klasse kapselt einen Benutzer (Zeile 19) und das Repository, über das die Details dieses Benutzers abgerufen werden können (Zeile 20);
  • Zeilen 26–29: Der Konstruktor, der die Klasse mit einem Benutzer und dessen Repository instanziiert;
  • Zeilen 32–36: Implementierung der Methode [getAuthorities] der Schnittstelle [UserDetails]. Sie muss eine Sammlung von Elementen vom Typ [GrantedAuthority] oder einem abgeleiteten Typ erstellen. Hier verwenden wir den abgeleiteten Typ [SimpleGrantedAuthority] (Zeile 36), der den Namen einer der Rollen des Benutzers aus Zeile 19 kapselt;
  • Zeilen 35–37: Die Liste der Rollen des Benutzers aus Zeile 19 wird durchlaufen, um eine Liste von Elementen vom Typ [SimpleGrantedAuthority] zu erstellen;
  • Zeilen 42–44: Implementieren die Methode [getPassword] der Schnittstelle [UserDetails]. Das Passwort des Benutzers aus Zeile 19 wird zurückgegeben;
  • Zeilen 42–44: Implementieren die Methode [getUserName] der Schnittstelle [UserDetails]. Der Benutzername des Benutzers aus Zeile 19 wird zurückgegeben;
  • Zeilen 51–54: Das Benutzerkonto läuft nie ab;
  • Zeilen 56–59: Das Benutzerkonto wird niemals gesperrt;
  • Zeilen 61–64: Die Anmeldedaten des Benutzers verfallen nie;
  • Zeilen 66–69: Das Benutzerkonto ist immer aktiv;

Spring Security schreibt außerdem die Existenz einer Klasse vor, die die Schnittstelle [AppUserDetailsService] implementiert:

 

Diese Schnittstelle wird von der folgenden Klasse [AppUserDetailsService] implementiert:


package spring.security.dao;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import spring.security.entities.User;
import spring.security.repositories.UserRepository;

@Service
public class AppUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
        // Suche nach dem Benutzer anhand seines Logins
        User user = userRepository.findUserByLogin(login);
        // Gefunden?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // Die Benutzerdaten werden zurückgegeben
        return new AppUserDetails(user, userRepository);
    }

}
  • Zeile 12: Die Klasse wird eine Spring-Komponente sein und steht somit in ihrem Kontext zur Verfügung;
  • Zeilen 15–16: Die Komponente [UserRepository] wird hier injiziert;
  • Zeilen 19–28: Implementierung der Methode [loadUserByUsername] der Schnittstelle [UserDetailsService] (Zeile 10). Der Parameter ist der Benutzername des Benutzers;
  • Zeile 21: Der Benutzer wird anhand seines Logins gesucht;
  • Zeilen 23–25: Wird er nicht gefunden, wird eine Ausnahme ausgelöst;
  • Zeile 27: Ein Objekt vom Typ [AppUserDetails] wird erstellt und zurückgegeben. Es handelt sich tatsächlich um den Typ [UserDetails] (Zeile 19);

16.4.7. Die Projektkonfiguration

Das Projekt besteht aus zwei Klassen:

Die Klasse [DaoConfig] konfiguriert die durch das neue Projekt eingebrachte Schicht [DAO]:


package spring.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@EnableJpaRepositories(basePackages = { "spring.security.repositories" })
@ComponentScan(basePackages = { "spring.security.dao" })
@Import({ spring.data.config.DaoConfig.class })
public class DaoConfig {

    // Konstanten
    final static private String[] ENTITIES_PACKAGES = { "spring.data.entities", "spring.security.entities" };

    @Bean
    public String[] packagesToScan() {
        return ENTITIES_PACKAGES;
    }

}
  • Zeile 10: Die Konfigurationsklasse [spring.data.config.DaoConfig] aus dem Projekt [intro-spring-data-01] wird importiert, die die Schicht [DAO] für Produkte und Kategorien implementiert;
  • Zeile 8: Es werden die Ordner des aktuellen Projekts angegeben, die [repositories] Spring Data enthalten;
  • Zeile 9: Hier werden die Ordner des aktuellen Projekts angegeben, die Spring-Komponenten für die Schicht [DAO] enthalten;
  • Zeile 14: Hier werden die Ordner angegeben, die JPA-Entitäten enthalten. Dazu gehören die des Projekts [intro-spring-data-01] und die des Projekts des sicheren Servers. Diese Information ist Gegenstand des Beans in den Zeilen 16–19. Dieser Bean definiert den gleichnamigen Bean des Projekts [intro-spring-data-01] neu:

    final static private String[] ENTITIES_PACKAGES = { "spring.data.entities" };
    
    // EntityManagerFactory
    @Bean
    public EntityManagerFactory entityManagerFactory(JpaVendorAdapter jpaVendorAdapter, DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setJpaVendorAdapter(jpaVendorAdapter);
        factory.setPackagesToScan(packagesToScan());
        factory.setDataSource(dataSource);
        factory.afterPropertiesSet();
        return factory.getObject();
    }

    @Bean
    public String[] packagesToScan() {
        return ENTITIES_PACKAGES;
}

In der Schicht [DAO] durchsucht Zeile 8 die in Zeile 1 angegebenen Ordner. Aufgrund der Neudefinition der Bean in den Zeilen 14–17 im gesicherten Projekt (Zeilen 16–19) durchsucht die oben genannte Zeile 8 nun die Ordner von ["spring.data.entities", "spring.security.entities"]. Es ist zu beachten, dass die in Zeile 10 importierte Klasse der Klasse [spring.security.config.DaoConfig] die Annotation [@Configuration] enthalten muss, da das soeben erläuterte Phänomen sonst nicht funktioniert.

Die Klasse [SecurityConfig] konfiguriert den Sicherheitsaspekt des Projekts. Wir sind bereits auf eine Konfigurationsklasse von Spring Security gestoßen:


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");
    }
}

Wir gehen nun genauso vor:

  • Zeile 11: Definieren einer Klasse, die die Klasse [WebSecurityConfigurerAdapter] erweitert;
  • Zeile 13: Definieren einer Methode [configure(HttpSecurity http)], die die Zugriffsrechte auf die verschiedenen URL des Webdienstes festlegt;
  • Zeile 19: Definieren einer Methode [configure(AuthenticationManagerBuilder auth)], die die Benutzer und ihre Rollen festlegt;

Die Klasse [SecurityConfig] sieht wie folgt aus:


package spring.security.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpMethod;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

import spring.security.dao.AppUserDetailsService;

@EnableWebSecurity
@ComponentScan(basePackages = { "spring.security.service" })
@Import({ spring.webjson.config.AppConfig.class, DaoConfig.class })
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private AppUserDetailsService appUserDetailsService;

    // Sicherung
    private boolean activateSecurity = true;

    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        // Die Authentifizierung erfolgt über die Bean [appUserDetailsService]
        // Das Passwort wird mit dem Hash-Algorithmus verschlüsselt: BCrypt
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // Sichere Anwendung?
        if (activateSecurity) {
            // Das Passwort wird über den Header „Authorization: Basic xxxx“ übermittelt
            http.httpBasic();
            // Die Methode HTTP OPTIONS muss für alle autorisiert sein
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // Nur die Rolle ADMIN darf die Anwendung nutzen
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // alle URL
                    .hasRole("ADMIN");
            // keine Sitzung
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
}
  • Zeile 16: Aktivierung der Spring-Security-Komponenten;
  • Zeile 17: Die Spring-Komponenten aus dem Paket [spring.security.service] werden hinzugefügt;
  • Zeile 18: Es werden die Beans der soeben vorgestellten Schicht [DAO] sowie die des ungesicherten Webservers / jSON importiert;
  • Zeilen 21–22: Die Klasse [AppUserDetails], die den Benutzern Zugriff auf die Anwendung gewährt, wird injiziert;
  • Zeile 25: Ein boolescher Wert, der die Webanwendung absichert (true) oder nicht (false);
  • Zeilen 27–32: Die Methode [configure(HttpSecurity http)] definiert die Benutzer und ihre Rollen. Sie erhält als Parameter einen Typ [AuthenticationManagerBuilder]. Dieser Parameter wird um zwei Informationen ergänzt (Zeile 38):
    • eine Referenz auf den Dienst [appUserDetailsService] aus Zeile 22, der Zugriff auf die registrierten Benutzer gewährt. Dabei ist zu beachten, dass nicht ersichtlich ist, ob diese in einer Datenbank gespeichert sind. Sie könnten sich also in einem Cache befinden, von einem Webdienst bereitgestellt werden usw.
    • die für das Passwort verwendete Verschlüsselungsart. Wir erinnern daran, dass wir den Algorithmus BCrypt verwendet haben;
  • Zeilen 34–52: Die Methode [configure(HttpSecurity http)] definiert die Zugriffsrechte auf die URL des Webdienstes;
  • Zeile 37: Wir haben im Einführungsprojekt gesehen, dass Spring Security standardmäßig ein CSRF-Token (Cross-Site-Request-Forgery) verwaltet, das der Benutzer, der sich authentifizieren möchte, an den Server zurücksenden muss. Hier ist dieser Mechanismus deaktiviert. In Verbindung mit dem booleschen Wert (isSecured=false) ermöglicht dies die Nutzung der Webanwendung ohne Sicherheitsmaßnahmen;
  • Zeile 41: Der Authentifizierungsmodus über den Header HTTP wird aktiviert. Der Client muss den folgenden Header HTTP senden:
Authorization:Basic code

wobei „code“ die Base64-Kodierung der Zeichenfolge „login:password“ ist. Beispielsweise lautet die Base64-Kodierung der Zeichenfolge „admin:admin“ „YWRtaW46YWRtaW4=“. Ein Benutzer mit dem Login „[admin]“ und dem Passwort „[admin]“ sendet also zur Authentifizierung den folgenden Header „HTTP“:

Authorization:Basic YWRtaW46YWRtaW4=
  • Zeilen 46–48: Geben an, dass alle URL des Webdienstes für Benutzer mit der Rolle [ROLE_ADMIN] zugänglich sind. Das bedeutet, dass ein Benutzer ohne diese Rolle keinen Zugriff auf den Webdienst hat;
  • Zeile 50: Im Modus [session] muss sich ein Benutzer, der sich einmal authentifiziert hat, bei seinen folgenden Zugriffen nicht erneut authentifizieren. Hier wird dieser Modus deaktiviert, sodass sich der Benutzer bei jedem Zugriff erneut authentifizieren muss;

16.4.8. Tests der Schicht [DAO]

  

Zunächst erstellen wir eine ausführbare Klasse [CreateUser], die in der Lage ist, einen Benutzer mit einer Rolle anzulegen:


package sprin.security.tests;

import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.security.crypto.bcrypt.BCrypt;

import spring.security.config.DaoConfig;
import spring.security.entities.Role;
import spring.security.entities.User;
import spring.security.entities.UserRole;
import spring.security.repositories.RoleRepository;
import spring.security.repositories.UserRepository;
import spring.security.repositories.UserRoleRepository;

public class CreateUser {

    public static void main(String[] args) {
        // Syntax: Login, Passwort roleName

        // Es sind drei Parameter erforderlich
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        // Die Parameter werden abgerufen
        String login = args[0];
        String password = args[1];
        String roleName = String.format("ROLE_%s", args[2].toUpperCase());
        // Spring-Kontext
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        // Existiert die Rolle bereits?
        Role role = roleRepository.findRoleByName(roleName);
        // Wenn nicht, wird sie angelegt
        if (role == null) {
            role = roleRepository.save(new Role(roleName));
        }
        // Existiert der Benutzer bereits?
        User user = userRepository.findUserByLogin(login);
        // Falls nicht, wird er angelegt
        if (user == null) {
            // Das Passwort wird mit bcrypt gehasht
            String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
            // Der Benutzer wird gespeichert
            user = userRepository.save(new User(login, login, crypt));
            // Die Verknüpfung mit der Rolle wird hergestellt
            userRoleRepository.save(new UserRole(user, role));
        } else {
            // Der Benutzer existiert bereits – hat er die angeforderte Rolle?
            boolean trouvé = false;
            for (Role r : userRepository.getRoles(user.getId())) {
                if (r.getName().equals(roleName)) {
                    trouvé = true;
                    break;
                }
            }
            // Wenn nicht gefunden, wird die Zuordnung zur Rolle erstellt
            if (!trouvé) {
                userRoleRepository.save(new UserRole(user, role));
            }
        }

        // Spring-Kontext schließen
        context.close();
        // Ende
        System.out.println("Travail terminé...");
    }

}
  • Zeile 17: Die Klasse erwartet drei Argumente, die einen Benutzer definieren: dessen Login, dessen Passwort und dessen Rolle;
  • Zeilen 25–27: Die drei Parameter werden abgerufen;
  • Zeile 29: Der Spring-Kontext wird anhand der Konfigurationsklasse „[AppConfig]“ aufgebaut;
  • Zeilen 30–32: Es werden die Referenzen der drei [Repository] abgerufen, die uns beim Anlegen des Benutzers nützlich sein können;
  • Zeile 34: Es wird geprüft, ob die Rolle bereits existiert;
  • Zeilen 36–38: Ist dies nicht der Fall, wird sie in der Datenbank angelegt. Sie erhält einen Namen vom Typ [ROLE_XX];
  • Zeile 40: Es wird geprüft, ob der Benutzername bereits existiert;
  • Zeilen 42–49: Falls der Benutzername noch nicht existiert, wird er in der Datenbank angelegt;
  • Zeile 44: Das Passwort wird verschlüsselt. Hier wird die Klasse [BCrypt] von Spring Security (Zeile 4) verwendet. Daher werden die Archive dieses Frameworks benötigt. Die Datei [pom.xml] enthält diese Abhängigkeit:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • Zeile 46: Der Benutzer wird in der Datenbank gespeichert;
  • Zeile 48: ebenso wie die Beziehung, die ihn mit seiner Rolle verbindet;
  • Zeilen 51–57: Fall, in dem der Benutzer bereits existiert – es wird geprüft, ob die Rolle, die ihm zugewiesen werden soll, bereits zu seinen Rollen gehört;
  • Zeile 59–61: Wenn die gesuchte Rolle nicht gefunden wurde, wird eine Zeile in der Tabelle [USERS_ROLES] angelegt, um den Benutzer mit seiner Rolle zu verknüpfen;
  • Es wurde keine Absicherung gegen mögliche Ausnahmen vorgenommen. Es handelt sich um eine Hilfsklasse zum schnellen Anlegen eines Benutzers mit einer Rolle.

Wenn man die Klasse mit den Argumenten [x x guest] ausführt, erhält man in der Datenbank die folgenden Ergebnisse:

Tabelle [USERS]

Tabelle

Tabelle [ROLES]

 

Tabelle [USERS_ROLES]

 

Betrachten wir nun die zweite Klasse [UsersTest], bei der es sich um einen Test JUnit handelt:

  

package spring.security.tests;

import java.util.List;

import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.SpringApplicationConfiguration;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.bcrypt.BCrypt;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.collect.Lists;

import spring.security.config.DaoConfig;
import spring.security.dao.AppUserDetails;
import spring.security.dao.AppUserDetailsService;
import spring.security.entities.Role;
import spring.security.entities.User;
import spring.security.repositories.UserRepository;

@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class UsersTest {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private AppUserDetailsService appUserDetailsService;

    // Mapper jSON
    private ObjectMapper mapper = new ObjectMapper();

    @Test
    public void findAllUsersWithTheirRoles() throws JsonProcessingException {
        Iterable<User> users = userRepository.findAll();
        for (User user : users) {
            System.out.println(String.format("\n----------Utilisateur [%s]",mapper.writeValueAsString(user)));
            display("Roles :", userRepository.getRoles(user.getId()));
        }
    }

    @Test
    public void findUserByLogin() {
        // Der Benutzer wird abgerufen: [admin]
        User user = userRepository.findUserByLogin("admin");
        // Es wird überprüft, ob sein Passwort [admin] lautet
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // Es wird überprüft, ob die Rolle „admin / admin“ vorhanden ist
        List<Role> roles = Lists.newArrayList(userRepository.getRoles("admin", user.getPassword()));
        Assert.assertEquals(1L, roles.size());
        Assert.assertEquals("ROLE_ADMIN", roles.get(0).getName());
    }

    @Test
    public void loadUserByUsername() {
        // Der Benutzer [admin] wird abgerufen
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // Es wird überprüft, ob das Passwort „[admin]“ lautet
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        // Es wird die Rolle „admin / admin“ überprüft
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }

    // Hilfsmethode – zeigt die Elemente einer Sammlung an
    private void display(String message, Iterable<?> elements) throws JsonProcessingException {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(mapper.writeValueAsString(element));
        }
    }
}
  • Zeilen 37–44: Sichtprüfung. Es werden alle Benutzer mit ihren Rollen angezeigt;
  • Zeilen 46–56: Es wird überprüft, ob der Benutzer [admin] das Passwort [admin] und die Rolle [ROLE_ADMIN] hat, wobei das Repository [UserRepository] verwendet wird;
  • Zeile 51: [admin] ist das Passwort im Klartext. In der Datenbank ist es nach dem Algorithmus BCrypt verschlüsselt. Mit der Methode [BCrypt.checkpw] lässt sich überprüfen, ob das verschlüsselte Passwort im Klartext tatsächlich mit dem in der Datenbank gespeicherten übereinstimmt;
  • Zeilen 58–69: Es wird überprüft, ob der Benutzer [admin] das Passwort [admin] und die Rolle [ROLE_ADMIN] hat, wobei der Dienst [appUserDetailsService] verwendet wird;

Die Tests werden erfolgreich mit den folgenden Protokollen ausgeführt:

----------Utilisateur [{"id":14,"version":0,"identity":"admin","login":"admin","password":"$2a$10$FN1LMKjPU46aPffh9Zaw4exJOLo51JJPWrxqzak/eJrbt3CO9WzVG"}]
Roles :
{"id":6,"version":0,"name":"ROLE_ADMIN"}

----------Utilisateur [{"id":15,"version":0,"identity":"user","login":"user","password":"$2a$10$SJehR9Mv2VdyRZo9F0rXa.hKAoGLhJg6kSdyfExi40mEJrNOj0BTq"}]
Roles :
{"id":7,"version":0,"name":"ROLE_USER"}

----------Utilisateur [{"id":16,"version":0,"identity":"guest","login":"guest","password":"$2a$10$ubyWJb/vg2XZnUOAUjspZuz9jpHP3fIbPTbwQU115EtLdeSZ2PB7q"}]
Roles :
{"id":5,"version":0,"name":"ROLE_GUEST"}

----------Utilisateur [{"id":17,"version":0,"identity":"x","login":"x","password":"$2a$10$kEXA56wpKHFReVqwQTyWguKguK8I4uhA2zb6t3wGxag8Dyv7AhLom"}]
Roles :
{"id":5,"version":0,"name":"ROLE_GUEST"}

16.4.9. Tests des Webdienstes

Wir werden den Webdienst mit dem Chrome-Client [Advanced Rest Client] testen. Dazu müssen wir den Authentifizierungs-Header HTTP angeben:

Authorization:Basic code

wobei [code] der Base64-Code der Zeichenfolge [login:password] ist. Um diesen Code zu generieren, kann das folgende Programm verwendet werden:

  

package spring.security.helpers;

import org.springframework.security.crypto.codec.Base64;

public class Base64Encoder {

    public static void main(String[] args) {
        // Es werden zwei Argumente erwartet: Login und Passwort
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // Die beiden Argumente werden abgerufen
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // Die Zeichenkette wird kodiert
        byte[] data = Base64.encode(chaîne.getBytes());
        // die Base64-Kodierung wird angezeigt
        System.out.println(new String(data));
    }

}

Wenn wir dieses Programm mit den beiden Argumenten [admin admin] ausführen:

  

erhalten wir folgendes Ergebnis:

YWRtaW46YWRtaW4=

Da wir nun wissen, wie man den Authentifizierungs-Header HTTP generiert, starten wir den sicheren Webdienst und fordern dann mit dem Chrome-Client [Advanced Rest Client] die Liste aller Produkte an:

  • In [1] fordern wir die URL der Kategorien an;
  • in [2] mit der Methode GET;
  • in [3] geben wir den Header HTTP für die Authentifizierung an. Der Code [YWRtaW46YWRtaW4=] ist die Base64-Kodierung der Zeichenfolge [admin:admin];
  • Bei „[4]“ senden wir den Befehl „HTTP“;

Die Antwort des Servers lautet wie folgt:

  • in [1], der Authentifizierungsheader HTTP;
  • in [2] sendet der Server eine Antwort mit der Kennung jSON zurück;

Man erhält tatsächlich die Liste der Kategorien:

 

Versuchen wir nun eine Anfrage HTTP mit einem falschen Authentifizierungsheader. Die Antwort lautet dann wie folgt:

  • in [1]: der Authentifizierungsheader HTTP;

Wir erhalten folgende Antwort:

  • zu [2]: die Antwort des Webdienstes;

Versuchen wir es nun mit dem Benutzer „user / user“. Er existiert, hat aber keinen Zugriff auf den Webdienst. Wenn wir das Base64-Kodierungsprogramm mit den beiden Argumenten [user user] ausführen:

  

erhalten wir folgendes Ergebnis:

dXNlcjp1c2Vy
  • in [1]: der Kopf „HTTP“ ist fehlerhaft;
  • in [2]: die Antwort des Webdienstes. Sie unterscheidet sich von der vorherigen, die [401 Unauthorized] lautete. Diesmal hat sich der Benutzer zwar korrekt authentifiziert, verfügt jedoch nicht über ausreichende Rechte, um auf URL zuzugreifen;

Unser sicherer Webdienst ist nun betriebsbereit.

16.4.10. Eine Authentifizierungs-ID URL

  

Wir werden eine URL erstellen, mit der wir feststellen können, ob ein Benutzer zum Zugriff auf den Webdienst berechtigt ist oder nicht. Dazu erstellen wir den folgenden neuen Controller MVC [AuthenticateController]:


package spring.security.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;

import spring.webjson.models.Response;

@Controller
public class AuthenticateController {

    // Spring-Abhängigkeiten
    @Autowired
    private ApplicationContext context;

    @RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String authenticate() throws JsonProcessingException {
        // Antwort: jSON
        ObjectMapper mapperResponse = context.getBean(ObjectMapper.class);
        return mapperResponse.writeValueAsString(new Response<Void>(0, null, null));
    }

}
  • Zeile 15: Die Klasse „[AuthenticateController]“ ist ein Spring-Controller. Als solcher stellt sie „URL“ bereit;
  • Zeile 22: stellt die URL und [/authenticate] bereit;
  • Zeile 23: Das Ergebnis der Methode wird direkt an den Client gesendet;
  • Zeilen 26–27: Die Methode gibt lediglich ein leeres [Response]-Objekt zurück, wobei [status] den Wert 0 hat, was anzeigt, dass kein Fehler aufgetreten ist;

Wozu dient dieses URL? Wenn wir lediglich einen Benutzer authentifizieren möchten, werden wir danach fragen. Wir haben gesehen, dass die Sicherheitsschicht eine Ausnahme zurückgibt, wenn sie diesen Benutzer nicht akzeptiert. Hier ein Beispiel;

Mit dem Benutzer [admin:admin]:

Wir erhalten eine leere Antwort, aber keine Ausnahme.

Mit dem Benutzer [user:user]:

Es ist eine Ausnahme aufgetreten.

16.4.11. Fazit

Das Hinzufügen der für Spring Security erforderlichen Klassen konnte ohne Änderungen am ursprünglichen Web-/JSON-Projekt erfolgen. Dieser sehr günstige Fall ergibt sich daraus, dass die drei in die Datenbank hinzugefügten Tabellen unabhängig von den bestehenden Tabellen sind. Man hätte sie sogar in einer separaten Datenbank ablegen können. In anderen Fällen können die hinzugefügten Tabellen Beziehungen zu den bestehenden Tabellen aufweisen. In diesem Fall müssen die Entitäten JPA geändert werden, was in der Regel Auswirkungen auf alle Schichten des Projekts hat.

16.5. Ein für den gesicherten Webdienst / jSON programmierter Client

Wir haben bereits einen Client für den ungesicherten Webdienst / jSON geschrieben:

Wir werden nun einen programmierten Client für den gesicherten Webdienst erstellen:

Wir duplizieren das bereits erstellte Projekt [intro-webjson-client] in ein neues Projekt [intro-spring-security-client-01]:

  

16.5.1. Die Klasse [AbstractDao]

Die Klasse [AbstractDao] stellt die Kommunikation HTTP mit dem gesicherten Webserver / jSON sicher. Wie wir gerade gesehen haben, muss der Client bei dieser Kommunikation HTTP nun einen Authentifizierungsheader senden, zum Beispiel:

Authorization:Basic YWRtaW46YWRtaW4=

Dies geschieht wie folgt:


package spring.security.client.dao;

import java.net.URI;
...

public abstract class AbstractDao {

    // Daten
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    // Generische Anfrage
    protected String getResponse(User user, String url, String jsonPost) {

// URL: URL – Kontakt

  • Zeile 15: Die generische Methode [getResponse], die für die Kommunikation HTTP mit dem gesicherten Webdienst zuständig ist, akzeptiert nun als ersten Parameter den Benutzer, der eine URL anfordert. Die Klasse [User] lautet wie folgt:

Diese Klasse sieht wie folgt aus:

  

package spring.security.client.entities;

public class User {

    // Eigenschaften
    private String login;
    private String password;

    // Konstruktor
    public User() {
    }

    public User(String login, String password) {
        this.login = login;
        this.password = password;
    }

    // Getter und Setter
...
}

Die Methode [getResponse] lautet nun wie folgt:


package spring.security.client.dao;

import java.net.URI;
import java.net.URISyntaxException;
import java.util.Base64;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.MediaType;
import org.springframework.http.RequestEntity;
import org.springframework.http.RequestEntity.BodyBuilder;
import org.springframework.http.RequestEntity.HeadersBuilder;
import org.springframework.web.client.RestTemplate;

import spring.security.client.entities.User;

public abstract class AbstractDao {

    // Daten
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;

    private String getBase64(User user) {
        // Benutzername und Passwort werden in Base64 kodiert – erfordert Java 8
        String chaîne = String.format("%s:%s", user.getLogin(), user.getPassword());
        return String.format("Basic %s", new String(Base64.getEncoder().encode(chaîne.getBytes())));
    }

    // Generische Anfrage
    protected String getResponse(User user, String url, String jsonPost) {

        // URL: URL – bitte kontaktieren
        // jsonPost: Der Wert jSON soll gesendet werden
        try {
            // Abfrage ausführen
            RequestEntity<?> request;
            if (jsonPost == null) {
                HeadersBuilder<?> headersBuilder = RequestEntity.get(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    headersBuilder = headersBuilder.header("Authorization", getBase64(user));
                }
                request = headersBuilder.build();
            } else {
                BodyBuilder bodyBuilder = RequestEntity.post(new URI(String.format("%s%s", urlServiceWebJson, url)))
                        .header("Content-Type", "application/json").accept(MediaType.APPLICATION_JSON);
                if (user != null) {
                    bodyBuilder = bodyBuilder.header("Authorization", getBase64(user));
                }
                request = bodyBuilder.body(jsonPost);
            }

            // Die Anfrage wird ausgeführt
            return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
            }).getBody();
        } catch (URISyntaxException e1) {
            throw new DaoException(20, e1);
        } catch (RuntimeException e2) {
            throw new DaoException(21, e2);
        }
    }

}

  • Zeilen 42–44, 49–51: Wenn der Benutzer [user] nicht null ist, wird der Authentifizierungsheader hinzugefügt. Die Verschlüsselung Base64 des Benutzers und seines Passworts erfolgt durch die Methode [getBase64] in den Zeilen 25–29. Es ist zu beachten, dass diese Methode eine Klasse [Base64] verwendet, die zu JDK 1.8 gehört.
  • Abgesehen von den vorangegangenen Zeilen bleibt der Code unverändert;

16.5.2. Die Schnittstelle [IDao]

Alle Methoden der Schnittstelle [IDao] erhalten einen zusätzlichen Parameter [User user]:

  

package spring.security.client.dao;

import java.util.List;

import spring.security.client.entities.Categorie;
import spring.security.client.entities.Produit;
import spring.security.client.entities.User;

public interface IDaoClient {

    // Authentifizierung
    public void authenticate(User user);

    // Einfügen einer Produktliste
    public List<Produit> addProduits(User user, List<Produit> produits);

    // Löschen aller Produkte
    public void deleteAllProduits(User user);

    // Aktualisierung einer Produktliste
    public List<Produit> updateProduits(User user, List<Produit> produits);

    // Abruf aller Produkte
    public List<Produit> getAllProduits(User user);

    // Einfügen einer Kategorieliste
    public List<Categorie> addCategories(User user, List<Categorie> categories);

    // Löschen aller Kategorien
    public void deleteAllCategories(User user);

    // Aktualisierung einer Kategorieliste
    public List<Categorie> updateCategories(User user, List<Categorie> categories);

    // Abruf aller Kategorien
    public List<Categorie> getAllCategories(User user);

    // ein bestimmtes Produkt
    public Produit getProduitByIdWithCategorie(User user, Long idProduit);

    public Produit getProduitByIdWithoutCategorie(User user, Long idProduit);

    public Produit getProduitByNameWithCategorie(User user, String nom);

    public Produit getProduitByNameWithoutCategorie(User user, String nom);

    // eine bestimmte Kategorie
    public Categorie getCategorieByIdWithProduits(User user, Long idCategorie);

    public Categorie getCategorieByIdWithoutProduits(User user, Long idCategorie);

    public Categorie getCategorieByNameWithProduits(User user, String nom);

    public Categorie getCategorieByNameWithoutProduits(User user, String nom);

}
  • Zeile 12: Wir haben die Methode [authenticate(User user)] hinzugefügt, um einen Benutzer zu authentifizieren. Sie löst eine Ausnahme aus, wenn der Benutzer keine Berechtigung zum Zugriff auf die Webservice-Methoden URL und [/authenticate] hat;

16.5.3. Die Klasse [Dao]

Alle Methoden der Klasse [Dao] erhalten einen zusätzlichen Parameter [User user], den sie an die generische Methode [getResponse] der Klasse [AbstractDao] übergeben. Hier sind zwei Beispiele:


// Authentifizierung
    @Override
    public void authenticate(User user) {
        getResponse(user, "/authenticate", null);
    }

    @Override
    public List<Produit> addProduits(User user, List<Produit> produits) {
        // ----------- Produkte hinzufügen (ohne deren Kategorie)
        try {
            // Mapper jSON
            ObjectMapper mapperPost = context.getBean(ObjectMapper.class);
            mapperPost.setFilters(jsonFilterProduitWithoutCategorie);
            ObjectMapper mapperResponse = mapperPost;
            // Anfrage
            Response<List<Produit>> response = mapperResponse.readValue(
                    getResponse(user, "/addProduits", mapperPost.writeValueAsString(produits)),
                    new TypeReference<Response<List<Produit>>>() {
                    });
            // Fehler?
            if (response.getStatus() != 0) {
                // Es wird 1 Ausnahme ausgelöst
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                // Der Kern der Serverantwort wird zurückgegeben
                return response.getBody();
            }
        } catch (DaoException e1) {
            throw e1;
        } catch (IOException | RuntimeException e2) {
            throw new DaoException(100, e2);
        }
    }

16.5.4. Unit-Tests der Klasse [Dao]

Die Klasse [Test01] für Unit-Tests der Klasse [Dao] wird wie folgt geändert:

  

package client.tests.junit;

...

@SpringApplicationConfiguration(classes = DaoConfig.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class Test01 {

    // Spring-Kontext
    @Autowired
    private ApplicationContext context;
    // Schicht [DAO]
    @Autowired
    private IDaoClient dao;

    // Benutzer
    static private User admin;
    static private User user;
    static private User unknown;

    @BeforeClass
    public static void init() {
        admin = new User("admin", "admin");
        user = new User("user", "user");
        unknown = new User("x", "y");
    }

    @Before
    public void cleanAndFill() {
        // Die Datenbank wird vor jedem Test bereinigt
        log("Vidage de la base de données", 1);
        // Die Tabelle [CATEGORIES] wird geleert – in der Folge wird auch die Tabelle [PRODUITS] geleert
        dao.deleteAllCategories(admin);
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // Die Tabellen werden gefüllt
        List<Categorie> categories = new ArrayList<Categorie>();
        for (int i = 0; i < 2; i++) {
            Categorie categorie = new Categorie(String.format("categorie%d", i));
            for (int j = 0; j < 5; j++) {
                categorie.addProduit(new Produit(String.format("produit%d%d", i, j), 100 * (1 + (double) (i * 10 + j) / 100),
                        String.format("desc%d%d", i, j)));
            }
            categories.add(categorie);
        }
        // Hinzufügen der Kategorie – durch die Kaskadenwirkung werden auch die Produkte eingefügt
        dao.addCategories(admin, categories);
    }

    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // Liste der Kategorien
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories(admin);
        affiche(categories, context.getBean("jsonMapperCategorieWithoutProduits", ObjectMapper.class));
        // Liste der Produkte
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits(admin);
        affiche(produits, context.getBean("jsonMapperProduitWithoutCategorie", ObjectMapper.class));
        // Einige Überprüfungen
        Assert.assertEquals(2, categories.size());
        Assert.assertEquals(10, produits.size());
        Categorie categorie = findCategorieByName("categorie0", categories);
        Assert.assertNotNull(categorie);
        Produit produit = findProduitByName("produit03", produits);
        Assert.assertNotNull(produit);
        Long idCategorie = produit.getIdCategorie();
        Assert.assertEquals(categorie.getId(), idCategorie);
    }
...
    @Test()
    public void checkUserUser() {
        ServiceException se = null;
        try {
            dao.authenticate(user);
        } catch (ServiceException e) {
            se = e;
        }
        Assert.assertNotNull(se);
        Assert.assertEquals("403 Forbidden", se.getMessages().get(0));
    }

    @Test()
    public void checkUserUnknown() {
        ServiceException se = null;
        try {
            dao.authenticate(unknown);
        } catch (ServiceException e) {
            se = e;
        }
        Assert.assertNotNull(se);
        Assert.assertEquals("401 Unauthorized", se.getMessages().get(0));
    }

    @Test()
    public void checkUserAdmin() {
        ServiceException se = null;
        try {
            dao.authenticate(admin);
        } catch (ServiceException e) {
            se = e;
        }
        Assert.assertNull(se);
    }
...
}
  • Bei der Initialisierung der Testklasse (Zeilen 21–26) werden drei Benutzer angelegt:
    • Der Benutzer [admin] hat Zugriff auf die URL des Webdienstes, Test, Zeilen 96–104;
    • Der Benutzer [user] existiert, ist jedoch nicht berechtigt, die URL des Webdienstes zu nutzen, Test Zeilen 71–81;
    • Der Benutzer [unknown] existiert nicht, Test in den Zeilen 83–93;
  • Die Testmethoden entsprechen denen, die bereits für den ungesicherten Webdienst betrachtet wurden, mit dem Unterschied, dass die Methoden der Schnittstelle [IDaoClient] mit dem Benutzer [admin] als erstem Parameter aufgerufen werden, der zur Nutzung von URL berechtigt ist;

Der Test ist erfolgreich, allerdings lässt sich feststellen, dass er langsamer ist als beim ungesicherten Webdienst. Die Absicherung einer Anwendung erhöht deren Antwortzeiten merklich. Ein wichtiger Faktor für die Leistung des gesicherten Webdienstes ist zu beachten: In der Klasse [AppConfig], die ihn konfiguriert, haben wir Folgendes geschrieben:


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // Sichere Anwendung?
        if (activateSecurity) {
            // Das Passwort wird über den Header „Authorization: Basic xxxx“ übermittelt
            http.httpBasic();
            // Die Methode HTTP OPTIONS muss für alle autorisiert sein
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // Nur die Rolle ADMIN darf die Anwendung nutzen
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // alle URL
                    .hasRole("ADMIN");
            // keine Sitzung
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
}

Zeile 17 hat ihren Preis. Sie zwingt den Benutzer, sich bei jedem Zugriff zu authentifizieren. Wenn man sie auskommentiert, verkürzt sich die Dauer des vorherigen Tests JUnit von 10,57 Sekunden auf 4,21 Sekunden, da sich der Benutzer [admin] nur für den ersten Test authentifiziert und nicht für die folgenden (auch wenn der Authentifizierungsheader HTTP vom Client gesendet wird, überprüft der Server das Passwort des Benutzers nicht erneut). Bei einem ungesicherten Webdienst sinkt die Dauer des Tests JUnit auf 2,33 Sekunden.