Skip to content

16. [Kurs]: Absicherung des Zugriffs auf einen Webdienst mit Spring Security

Stichworte: mehrschichtige Architektur, Spring, Dependency Injection, sicherer Webdienst / JSON, Client / Server

16.1. Support

 

Die Projekte für dieses Kapitel befinden sich im Ordner [support / chap-16]. Das SQL-Skript dient zur Erstellung der für die Tests erforderlichen Datenbank.

16.2. 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 [Spring Security]-Schicht gewährt nur autorisierten Benutzern Zugriff auf die [Web]-Schicht.

16.3. Ein Tutorial zu Spring Security

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

  

Das Projekt umfasst folgende Elemente:

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

16.3.1. Maven-Konfiguration

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


<?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 [Thymeleaf]-Framework;
  • Zeilen 22–25: Abhängigkeit vom Spring Security-Framework;

16.3.2. 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 HTML-Code 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 HTTP-Anfrage. Dies ist der Name des angemeldeten Benutzers;
  • Zeile 10: ein HTML-Formular. Das Attribut [th:action="@{/logout}"] generiert das Attribut [action] des [form]-Tags. Der Wert [@{/logout}] generiert den Pfad [<context>/logout], wobei [context] der Kontext der Webanwendung ist;

Der generierte HTML-Code 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 „Hallo [[${#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}"] stellt sicher, dass das <div>-Tag nur generiert wird, wenn die URL, die die Anmeldeseite anzeigt, den Parameter [error] enthält (http://context/login?error);
  • Zeile 10: Das Attribut [th:if="${param.logout}"] stellt sicher, dass das <div>-Tag nur generiert wird, wenn die URL, die die Anmeldeseite anzeigt, den Parameter [logout] enthält (http://context/login?logout);
  • Zeilen 11–23: ein HTML-Formular;
  • Zeile 11: Das Formular wird an die 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 HTML-Code 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>

Beachten Sie in Zeile 28, dass Thymeleaf ein verstecktes Feld namens [_csrf] hinzugefügt hat.

16.3.3. Spring MVC-Konfiguration

  

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


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 zu überschreiben;
  • Zeile 10: Neudefinition einer Methode aus der übergeordneten Klasse;
  • Zeilen 11–16: Die Methode [addViewControllers] ermöglicht die Zuordnung von URLs zu HTML-Ansichten. Dort werden folgende Zuordnungen vorgenommen:
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 Klassenpfads des Projekts befinden:

In [1] oben sind die Ordner [java] und [resources] beide Quellordner. Das bedeutet, dass sich ihr Inhalt im Stammverzeichnis des Klassenpfads des Projekts befindet. Daher befinden sich in [2] die Ordner [hello] und [templates] im Stammverzeichnis des Klassenpfads.

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 zu überschreiben;
  • Zeile 12: Neudefinition einer Methode aus der übergeordneten Klasse;
  • Zeilen 13–16: Die Methode [configure(HttpSecurity http)] wird überschrieben, um Zugriffsrechte für die verschiedenen URLs der Anwendung zu definieren;
  • Zeile 14: Die Methode [http.authorizeRequests()] ermöglicht die Zuordnung von URLs zu Zugriffsrechten. Dort werden folgende Zuordnungen vorgenommen:
URL
Regel
Code
/, /home
Zugriff ohne Authentifizierung

http.authorizeRequests().antMatchers("/", "/home").permitAll()
andere URLs
Nur authentifizierter Zugriff
http.anyRequest().authenticated();
  • Zeile 15: definiert die Authentifizierungsmethode. Die Authentifizierung erfolgt über ein URL-Formular [/login], das für alle zugänglich ist [http.formLogin().loginPage("/login").permitAll()]. Auch die Abmeldung ist für alle zugänglich;
  • Zeilen 19–21: definieren die Methode [configure(AuthenticationManagerBuilder auth)] neu, die Benutzer verwaltet;
  • Zeile 20: Die Authentifizierung erfolgt über fest programmierte Benutzer [auth.inMemoryAuthentication()]. Ein Benutzer wird hier mit dem Benutzernamen [user], dem Passwort [password] und der Rolle [USER] definiert. Benutzern mit derselben Rolle können dieselben Berechtigungen erteilt werden;

16.3.5. Ausführbare Klasse

  

Die Klasse [Application] sieht wie folgt aus:


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 durchzuführen, die der Entwickler nicht explizit eingerichtet hat;
  • Zeile 9: macht die Klasse [Application] zu einer Spring-Konfigurationsklasse;
  • Zeile 10: weist das System an, das Verzeichnis mit der [Application]-Klasse nach Spring-Komponenten zu durchsuchen. Die beiden Klassen [MvcConfig] und [WebSecurityConfig] werden somit gefunden, da sie die Annotation [@Configuration] tragen;
  • Zeile 13: die [main]-Methode der ausführbaren Klasse;
  • Zeile 14: Die statische Methode [SpringApplication.run] wird mit der Konfigurationsklasse [Application] als Parameter ausgeführt. Wir sind diesem Vorgang bereits begegnet und wissen, dass der in den Maven-Abhängigkeiten des Projekts eingebettete Tomcat-Server gestartet und das Projekt darauf bereitgestellt wird. Wir haben gesehen, dass vier URLs verwaltet wurden [/, /home, /login, /hello] und dass einige durch Zugriffsrechte geschützt waren.

16.3.6. Testen der Anwendung

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

 

Die angeforderte URL [/] ist für jeden zugänglich. Deshalb konnten wir sie abrufen. Der Link [hier] lautet wie folgt:

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

Die URL [/hello] wird aufgerufen, wenn wir auf den Link klicken. Diese ist geschützt:

URL
Regel
Code
/, /home
Zugriff ohne Authentifizierung

http.authorizeRequests().antMatchers("/", "/home").permitAll()
andere URLs
nur authentifizierter Zugriff
http.anyRequest().authenticated();

Sie müssen authentifiziert sein, um darauf zugreifen zu können. Spring Security leitet den Browser des Clients dann auf die Authentifizierungsseite weiter. Basierend auf der gezeigten Konfiguration ist dies die Seite unter der URL [/login]. Diese Seite ist für alle zugänglich:


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

So erhalten wir sie [1]:

Der Quellcode der abgerufenen 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. Thymeleaf hat es hinzugefügt. Dieser Code, bekannt als 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 es akzeptiert wird;

Wir erinnern uns, dass von Spring Security nur das Paar aus Benutzername und Passwort 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 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 Benutzer-/Passwort-Werte ein [4]:

  • In [4] melden wir uns an;
  • in [5] leitet uns Spring Security zur URL [/hello] weiter, da dies die URL ist, die wir angefordert haben, als wir zur Anmeldeseite weitergeleitet wurden. Die Identität des Benutzers wurde durch die folgende Zeile in [hello.html] angezeigt:

    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

Seite [5] zeigt das folgende Formular an:


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

Wenn Sie auf die Schaltfläche [Abmelden] klicken, wird eine POST-Anfrage an die URL [/logout] gesendet. Wie die URL [/login] ist auch diese URL für jeden zugänglich:


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

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

  • In [6] klicken wir auf die Schaltfläche [Abmelden];
  • in [7] sehen wir, dass wir zur URL [http://localhost:8080/login?logout] weitergeleitet wurden. Spring Security hat diese Weiterleitung angefordert. Das Vorhandensein des Parameters [logout] in der URL führte dazu, 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 zuerst schreiben und sie später sichern können. Spring Security ist nicht-intrusiv. Sie können Sicherheit für eine bereits geschriebene Webanwendung implementieren. Darüber hinaus haben wir folgende Punkte festgestellt:

  • Es ist möglich, eine Authentifizierungsseite zu definieren;
  • die Authentifizierung muss mit dem von Spring Security ausgegebenen CSRF-Token erfolgen;
  • Wenn die Authentifizierung fehlschlägt, werden Sie mit einem zusätzlichen Fehlerparameter in der URL auf die Authentifizierungsseite weitergeleitet;
  • Wenn die Authentifizierung erfolgreich ist, werden Sie zu der Seite weitergeleitet, die zum Zeitpunkt der Authentifizierung angefordert wurde. Wenn Sie die Authentifizierungsseite direkt aufrufen, ohne eine Zwischenseite zu durchlaufen, leitet Spring Security Sie zur URL [/] weiter (dieser Fall wurde nicht demonstriert);
  • Sie melden sich ab, indem Sie die URL [/logout] mit einer POST-Anfrage aufrufen. Spring Security leitet Sie dann mit dem Parameter „logout“ in der URL zur Authentifizierungsseite weiter;

Alle diese Schlussfolgerungen basieren auf dem Standardverhalten von Spring Security. Dieses Verhalten kann durch Konfiguration geändert werden, indem bestimmte Methoden der Klasse [WebSecurityConfigurerAdapter] überschrieben werden.

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

  • eine Datenbank zum Speichern von Benutzern, deren Passwörtern und deren Rollen;
  • eine HTTP-Header-basierte Authentifizierung;

Es gibt nur sehr wenige Tutorials für das, was wir hier tun wollen. Die Lösung, die wir vorschlagen, ist eine Kombination aus Code-Schnipseln, die wir hier und da gefunden haben.

16.4. Implementierung der Sicherheit im Produkt-Webservice / JSON

16.4.1. Die Datenbank

Die Datenbank [dbintrospringdata] wird aktualisiert, um Benutzer, deren Passwörter und deren Rollen aufzunehmen. Es werden drei neue Tabellen hinzugefügt:

Image

Tabelle [USERS]: Benutzer

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

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

 

Der zur Verschlüsselung von Passwörtern verwendete Algorithmus ist der BCRYPT-Algorithmus.

Tabelle [ROLES]: Rollen

  • ID: Primärschlüssel;
  • VERSION: die Versionsspalte der Zeile;
  • NAME: Rollenname. Standardmäßig erwartet Spring Security Namen im Format ROLE_XX, wie z. B. ROLE_ADMIN oder ROLE_GUEST;
 

Tabelle [USERS_ROLES]: Verknüpfungstabelle zwischen USERS und ROLES

Ein Benutzer kann mehrere Rollen haben, und eine Rolle kann mehrere Benutzer umfassen. Dies ist eine Viele-zu-Viele-Beziehung, die durch die Tabelle [USERS_ROLES] dargestellt wird.

  • ID: Primärschlüssel;
  • VERSION: Spalte für die Zeilenversionierung;
  • USER_ID: Benutzer-ID;
  • ROLE_ID: Rollen-ID;
 

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 JPA-Entitäten, die den drei neuen Datenbanktabellen entsprechen;
    • [spring.security.repositories]: enthält die Spring-Data-Repositorys, die mit den drei neuen Tabellen verknüpft sind;
    • [spring.security.dao]: enthält einen auf den [Repositorys] basierenden Dienst;
    • [spring.security.config]: enthält die Projektkonfiguration, einschließlich der Konfiguration für den sicheren Zugriff auf den Webdienst;
    • [spring.security.boot]: enthält die Startklasse für den sicheren Webdienst;

16.4.3. Die Maven-Konfiguration

Das neue Projekt ist ein Maven-Projekt, das durch die folgende [pom.xml]-Datei 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 logs -->
        <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: Wir verwenden den vorhandenen Code mit dem zuvor untersuchten Webservice-/JSON-Archiv wieder;
  • Zeilen 29–32: die Abhängigkeit, die die Spring Security-Klassen einbindet;
  • Zeilen 34–37: die Logging-Bibliothek;
  • Zeilen 39–42: die Bibliothek, die die Verwendung von Spring Boot-Annotationen ermöglicht;
  • Zeilen 44–48: die für das Testen erforderliche Bibliothek;

16.4.4. Die neuen [JPA]-Entitäten

Die JPA-Schicht definiert drei neue Entitäten:

  

Die Klasse [User] repräsentiert die 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 {
 
    // properties
    @Column(name = "NAME")
    private String name;
    @Column(name = "LOGIN")
    private String login;
    @Column(name = "PASSWORD")
    private String password;
 
    // manufacturer
    public User() {
    }
 
    public User(String name, String login, String password) {
        this.name = name;
        this.login = login;
        this.password = password;
    }
 
    // getters and setters
...
}
  • Zeile 11: Die Klasse erweitert die Klasse [AbstractEntity], die bereits für die anderen Entitäten verwendet wird;

Die Klasse [Role] repräsentiert die 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 {
 
    // properties
    @Column(name="NAME")
    private String name;

    // manufacturers
    public Role() {
    }
 
    public Role(String name) {
        this.name = name;
    }
 
    // getters and setters
    public String getName() {
        return name;
    }
 
    public void setName(String name) {
        this.name = name;
    }
 
}

Die Klasse [UserRole] repräsentiert die 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 {
 
    // foreign keys
    @Column(name = "USER_ID", insertable = false, updatable = false)
    private Long userId;
    @Column(name = "ROLE_ID", insertable = false, updatable = false)
    private Long roleId;
 
    // a UserRole refers to a User
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;
 
    // a UserRole refers to a Role
    @ManyToOne
    @JoinColumn(name = "ROLE_ID")
    private Role role;
 
    // manufacturers
    public UserRole() {
 
    }
 
    public UserRole(User user, Role role) {
        this.user = user;
        this.role = role;
    }
 
    // getters and setters
...
}
 
  • Zeilen 22–24: Definieren Sie den Fremdschlüssel von der Tabelle [USERS_ROLES] zur Tabelle [USERS];
  • Zeilen 27–29: Definieren Sie den Fremdschlüssel von der Tabelle [USERS_ROLES] zur Tabelle [ROLES];

16.4.5. Die [repositories]

Jede der vorangegangenen JPA-Entitäten wird von einem Spring Data [Repository] verwaltet:

  

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


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 des rôles d'un utilisateur identifié par son id
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);
 
    // liste des rôles d'un utilisateur identifié par son login unique
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);
 
    // recherche d'un utilisateur via son login
    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)] ruft alle Rollen für einen Benutzer ab, der durch seine [id] identifiziert wird
  • Zeilen 16–17: wie oben, jedoch für einen Benutzer, der durch seinen Benutzernamen und sein Passwort identifiziert wird;
  • Zeile 20: Um einen Benutzer anhand seines Benutzernamens zu finden;

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


package spring.security.repositories;
 
import org.springframework.data.repository.CrudRepository;
 
import spring.security.entities.Role;
 
public interface RoleRepository extends CrudRepository<Role, Long> {
 
    // search for a role by name
    Role findRoleByName(String name);
 
}
  • Zeile 7: Die Schnittstelle [RoleRepository] erweitert die Schnittstelle [CrudRepository];
  • Zeile 10: Sie können nach einer Rolle anhand ihres Namens suchen;

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


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 neue Methoden hinzuzufügen;

16.4.6. Klassen für die Benutzer- und Rollenverwaltung

  

Für Spring Security muss eine Klasse erstellt werden, die die folgende [UsersDetail]-Schnittstelle implementiert:

 

Diese Schnittstelle wird hier von der 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;
 
    // properties
    private User user;
    private UserRepository userRepository;
 
    // manufacturers
    public AppUserDetails() {
    }
 
    public AppUserDetails(User user, UserRepository userRepository) {
        this.user = user;
        this.userRepository = userRepository;
    }
 
    // -------------------------interface
    @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;
    }
 
    // getters and setters
    ...
}
  • Zeile 14: Die Klasse [AppUserDetails] implementiert die Schnittstelle [UserDetails];
  • Zeilen 19–20: Die Klasse kapselt einen Benutzer (Zeile 19) und das Repository, das Details zu diesem Benutzer bereitstellt (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 des Typs [GrantedAuthority] oder eines abgeleiteten Typs 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: Wir durchlaufen die Liste der Rollen des Benutzers aus Zeile 19, um eine Liste von Elementen vom Typ [SimpleGrantedAuthority] zu erstellen;
  • Zeilen 42–44: Implementieren Sie die Methode [getPassword] der Schnittstelle [UserDetails]. Wir geben das Passwort des Benutzers aus Zeile 19 zurück;
  • Zeilen 42–44: Implementierung der Methode [getUserName] der Schnittstelle [UserDetails]. Rückgabe des Benutzernamens des Benutzers aus Zeile 19;
  • 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 erfordert außerdem das Vorhandensein einer Klasse, die die Schnittstelle [AppUserDetailsService] implementiert:

 

Diese Schnittstelle wird von der folgenden [AppUserDetailsService]-Klasse 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 {
        // search for user via login
        User user = userRepository.findUserByLogin(login);
        // found?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // render user details
        return new AppUserDetails(user, userRepository);
    }
 
}
  • Zeile 12: Die Klasse wird eine Spring-Komponente sein, sodass sie in ihrem Kontext verfügbar ist;
  • Zeilen 15–16: Die [UserRepository]-Komponente 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 Benutzernamens gesucht;
  • Zeilen 23–25: Wird der Benutzer nicht gefunden, wird eine Ausnahme ausgelöst;
  • Zeile 27: Ein [AppUserDetails]-Objekt wird erstellt und zurückgegeben. Es ist tatsächlich vom Typ [UserDetails] (Zeile 19);

16.4.7. Projektkonfiguration

Das Projekt wird durch zwei Klassen konfiguriert:

Die Klasse [DaoConfig] konfiguriert die durch das neue Projekt eingeführte [DAO]-Schicht:


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 {
 
    // constants
    final static private String[] ENTITIES_PACKAGES = { "spring.data.entities", "spring.security.entities" };
 
    @Bean
    public String[] packagesToScan() {
        return ENTITIES_PACKAGES;
    }
 
}
  • Zeile 10: Wir importieren die Konfigurationsklasse [spring.data.config.DaoConfig] aus dem Projekt [intro-spring-data-01], das die [DAO]-Schicht für Produkte und Kategorien implementiert;
  • Zeile 8: Wir geben die Ordner im aktuellen Projekt an, die Spring Data [Repositorys] enthalten;
  • Zeile 9: Wir geben die Ordner im aktuellen Projekt an, die Spring-Komponenten enthalten, die mit der [DAO]-Schicht zusammenhängen;
  • Zeile 14: Hier werden die Verzeichnisse angegeben, die JPA-Entitäten enthalten. Dazu gehören sowohl diejenigen aus dem Projekt [intro-spring-data-01] als auch diejenigen aus dem Secure-Server-Projekt. Diese Informationen sind in der Bean in den Zeilen 16–19 definiert. Diese Bean überschreibt die gleichnamige Bean im Projekt [intro-spring-data-01]:

    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 [DAO]-Schicht durchsucht Zeile 8 die in Zeile 1 angegebenen Verzeichnisse. Aufgrund der Neudefinition des Beans in den Zeilen 14–17 im secure-Projekt (Zeilen 16–19) durchsucht Zeile 8 nun die Verzeichnisse ["spring.data.entities", "spring.security.entities"]. Beachten Sie, dass die in Zeile 10 aus der Klasse [spring.security.config.DaoConfig] importierte Klasse die Annotation [@Configuration] enthalten muss; andernfalls funktioniert das oben beschriebene Verhalten nicht.

Die Klasse [SecurityConfig] konfiguriert den Sicherheitsaspekt des Projekts. Wir sind bereits auf eine Spring-Security-Konfigurationsklasse 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 genauso vor:

  • Zeile 11: Definieren Sie eine Klasse, die die Klasse [WebSecurityConfigurerAdapter] erweitert;
  • Zeile 13: Definieren Sie eine Methode [configure(HttpSecurity http)], die Zugriffsrechte für die verschiedenen URLs des Webdienstes festlegt;
  • Zeile 19: Definieren Sie eine Methode [configure(AuthenticationManagerBuilder auth)], die Benutzer und deren Rollen definiert;

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;
 
    // security
    private boolean activateSecurity = true;
 
    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        // authentication is performed by bean [appUserDetailsService]
        // the password is encrypted using the BCrypt hash algorithm
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // secure application?
        if (activateSecurity) {
            // the password is transmitted by the header Authorization: Basic xxxx
            http.httpBasic();
            // the HTTP OPTIONS method must be authorized for all
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // only the ADMIN role can use the application
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // all URL
                    .hasRole("ADMIN");
            // no session
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
}
  • Zeile 16: um Spring Security-Komponenten zu aktivieren;
  • Zeile 17: Wir fügen die Spring-Komponenten aus dem Paket [spring.security.service] hinzu;
  • Zeile 18: Importieren der Beans aus der soeben eingeführten [DAO]-Schicht sowie derjenigen aus dem ungesicherten Webserver/JSON;
  • Zeilen 21–22: Die Klasse [AppUserDetails], die Zugriff auf die Benutzer der Anwendung bietet, wird injiziert;
  • Zeile 25: Ein boolescher Wert, der die Webanwendung absichert (true) oder nicht absichert (false);
  • Zeilen 27–32: Die Methode [configure(HttpSecurity http)] definiert Benutzer und ihre Rollen. Sie nimmt einen Typ [AuthenticationManagerBuilder] als Parameter entgegen. Dieser Parameter wird um zwei Informationen ergänzt (Zeile 38):
    • einen Verweis auf den [appUserDetailsService] aus Zeile 22, der Zugriff auf registrierte Benutzer gewährt. Beachten Sie hierbei, dass nicht ausdrücklich angegeben wird, dass diese in einer Datenbank gespeichert sind. Sie könnten daher in einem Cache liegen, von einem Webdienst bereitgestellt werden usw.
    • den für das Passwort verwendeten Verschlüsselungstyp. Erinnern Sie sich daran, dass wir den BCrypt-Algorithmus verwendet haben;
  • Zeilen 34–52: Die Methode [configure(HttpSecurity http)] definiert Zugriffsrechte auf die URLs 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 Kombination mit dem booleschen Wert (isSecured=false) ermöglicht dies die Nutzung der Webanwendung ohne Sicherheitsmaßnahmen;
  • Zeile 41: Wir aktivieren die Authentifizierung über HTTP-Header. Der Client muss den folgenden HTTP-Header senden:
Authorization:Basic code

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

Authorization:Basic YWRtaW46YWRtaW4=
  • Zeilen 46–48: legen fest, dass alle URLs für den Webdienst für Benutzer mit der Rolle [ROLE_ADMIN] zugänglich sind. Das bedeutet, dass ein Benutzer ohne diese Rolle nicht auf den Webdienst zugreifen kann;
  • Zeile 50: Im [session]-Modus muss sich ein Benutzer, der sich einmal authentifiziert hat, bei nachfolgenden Zugriffen nicht erneut authentifizieren. Hier deaktivieren wir diesen Modus, sodass sich der Benutzer bei jedem Zugriff auf den Dienst neu authentifizieren muss;

16.4.8. Testen der [DAO]-Schicht

  

Zunächst erstellen wir eine ausführbare Klasse [CreateUser], die einen Benutzer mit einer Rolle anlegen kann:


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 password roleName
 
        // three parameters are required
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        // parameters are retrieved
        String login = args[0];
        String password = args[1];
        String roleName = String.format("ROLE_%s", args[2].toUpperCase());
        // spring context
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        // does the role already exist?
        Role role = roleRepository.findRoleByName(roleName);
        // if it doesn't exist, we create it
        if (role == null) {
            role = roleRepository.save(new Role(roleName));
        }
        // does the user already exist?
        User user = userRepository.findUserByLogin(login);
        // if it doesn't exist, we create it
        if (user == null) {
            // hash the password with bcrypt
            String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
            // save user
            user = userRepository.save(new User(login, login, crypt));
            // we create the relationship with the role
            userRoleRepository.save(new UserRole(user, role));
        } else {
            // the user already exists - does he/she have the required role?
            boolean trouvé = false;
            for (Role r : userRepository.getRoles(user.getId())) {
                if (r.getName().equals(roleName)) {
                    trouvé = true;
                    break;
                }
            }
            // if not found, we create the relationship with the role
            if (!trouvé) {
                userRoleRepository.save(new UserRole(user, role));
            }
        }
 
        // closing Spring context
        context.close();
        // end
        System.out.println("Travail terminé...");
    }
 
}
  • Zeile 17: Die Klasse erwartet drei Argumente, die einen Benutzer definieren: dessen Benutzername, Passwort und Rolle;
  • Zeilen 25–27: Die drei Parameter werden abgerufen;
  • Zeile 29: Der Spring-Kontext wird aus der Konfigurationsklasse [AppConfig] erstellt;
  • Zeilen 30–32: Die Referenzen auf die drei [Repository]-Objekte, die für die Erstellung des Benutzers nützlich sein könnten, werden abgerufen;
  • Zeile 34: Wir prüfen, ob die Rolle bereits existiert;
  • Zeilen 36–38: Falls nicht, erstellen wir sie in der Datenbank. Sie erhält einen Namen der Form [ROLE_XX];
  • Zeile 40: Wir prüfen, ob der Benutzername bereits existiert;
  • Zeilen 42–49: Wenn der Benutzername nicht existiert, legen wir ihn in der Datenbank an;
  • Zeile 44: Wir verschlüsseln das Passwort. Hier verwenden wir die [BCrypt]-Klasse aus Spring Security (Zeile 4). Wir benötigen daher die Archive für dieses Framework. 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 verknüpft;
  • Zeilen 51–57: Wenn der Benutzer bereits existiert, prüfen wir, ob die Rolle, die wir ihm zuweisen möchten, bereits zu seinen Rollen gehört;
  • Zeilen 59–61: Wird die gesuchte Rolle nicht gefunden, wird eine Zeile in der Tabelle [USERS_ROLES] angelegt, um den Benutzer mit seiner Rolle zu verknüpfen;
  • Wir haben keine Absicherung gegen mögliche Ausnahmen vorgenommen. Dies ist eine Hilfsklasse zum schnellen Anlegen eines Benutzers mit einer Rolle.

Wenn die Klasse mit den Argumenten [x x guest] ausgeführt wird, werden die folgenden Ergebnisse in der Datenbank erhalten:

Tabelle [USERS]

Tabelle [ROLES]

 

Tabelle [USERS_ROLES]

 

Betrachten wir nun die zweite Klasse [UsersTest], bei der es sich um einen JUnit-Test 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() {
        // user [admin] is retrieved
        User user = userRepository.findUserByLogin("admin");
        // we check that his password is [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // check admin / admin role
        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() {
        // user [admin] is retrieved
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // we check that his password is [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        // check admin / admin role
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }
 
    // utility method - displays items in a collection
    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. Wir zeigen alle Benutzer zusammen mit ihren Rollen an;
  • Zeilen 46–56: Wir überprüfen mithilfe des [UserRepository], ob der Benutzer [admin] das Passwort [admin] und die Rolle [ROLE_ADMIN] hat;
  • Zeile 51: [admin] ist das Klartext-Passwort. In der Datenbank ist es mit dem BCrypt-Algorithmus verschlüsselt. Die Methode [BCrypt.checkpw] überprüft, ob das verschlüsselte Klartext-Passwort mit dem in der Datenbank übereinstimmt;
  • Zeilen 58–69: Wir überprüfen mithilfe von [appUserDetailsService], ob der Benutzer [admin] das Passwort [admin] und die Rolle [ROLE_ADMIN] hat;

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. Testen des Webdienstes

Wir werden den Webservice mit dem Chrome-Client [Advanced Rest Client] testen. Wir müssen den HTTP-Authentifizierungsheader angeben:

Authorization:Basic code

wobei [code] die Base64-kodierte Zeichenfolge [login:password] ist. Um diesen Code zu generieren, können Sie das folgende Programm verwenden:

  

package spring.security.helpers;
 
import org.springframework.security.crypto.codec.Base64;
 
public class Base64Encoder {
 
    public static void main(String[] args) {
        // we expect two arguments: login password
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // we retrieve the two arguments
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // encode the string
        byte[] data = Base64.encode(chaîne.getBytes());
        // displays its Base64 encoding
        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 HTTP-Authentifizierungsheader generiert, starten wir den sicheren Webdienst und fordern dann mithilfe des Chrome-Clients [Advanced Rest Client] die Liste aller Produkte an:

  • In [1] fordern wir die URL der Kategorien an;
  • in [2] verwenden wir eine GET-Methode;
  • in [3] geben wir den HTTP-Authentifizierungsheader an. Der Code [YWRtaW46YWRtaW4=] ist die Base64-Kodierung der Zeichenfolge [admin:admin];
  • in [4] senden wir die HTTP-Anfrage;

Die Antwort des Servers lautet wie folgt:

  • in [1] der HTTP-Authentifizierungsheader;
  • in [2] gibt der Server eine JSON-Antwort zurück;

Wir erhalten erfolgreich die Liste der Kategorien:

 

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

  • in [1]: der HTTP-Authentifizierungsheader;

Wir erhalten die folgende Antwort:

  • in [2]: die Antwort des Webdienstes;

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

  

Wir erhalten folgendes Ergebnis:

dXNlcjp1c2Vy
  • in [1]: der falsche HTTP-Authentifizierungsheader;
  • in [2]: die Antwort des Webdienstes. Sie unterscheidet sich von der vorherigen, die [401 Unauthorized] lautete. Diesmal hat sich der Benutzer korrekt authentifiziert, verfügt jedoch nicht über ausreichende Berechtigungen, um auf die URL zuzugreifen;

Unser sicherer Webdienst ist nun betriebsbereit.

16.4.10. Eine Authentifizierungs-URL

  

Wir erstellen eine URL, mit der wir feststellen können, ob ein Benutzer zum Zugriff auf den Webdienst berechtigt ist. Dazu erstellen wir den folgenden neuen MVC-Controller [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 dependencies
    @Autowired
    private ApplicationContext context;
 
    @RequestMapping(value = "/authenticate", method = RequestMethod.GET, produces = "application/json; charset=UTF-8")
    @ResponseBody
    public String authenticate() throws JsonProcessingException {
        // answer 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 URLs bereit;
  • Zeile 22: stellt die URL [/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, dessen [status] jedoch den Wert 0 hat, was darauf hinweist, dass kein Fehler aufgetreten ist;

Wozu dient diese URL? Wenn wir lediglich einen Benutzer authentifizieren möchten, rufen wir sie auf. Wir haben gesehen, dass die Sicherheitsschicht eine Ausnahme auslöst, wenn sie diesen Benutzer nicht akzeptiert. Hier ist 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

Die für Spring Security erforderlichen Klassen wurden hinzugefügt, ohne das ursprüngliche Web-/JSON-Projekt zu verändern. Dieses sehr vorteilhafte Szenario ergibt sich aus der Tatsache, dass die drei zur Datenbank hinzugefügten Tabellen unabhängig von den bestehenden Tabellen sind. Wir hätten sie sogar in einer separaten Datenbank unterbringen können. In anderen Fällen können die hinzugefügten Tabellen Beziehungen zu bestehenden Tabellen aufweisen. In diesem Fall müssen die JPA-Entitäten geändert werden, was sich in der Regel auf alle Schichten des Projekts auswirkt.

16.5. Ein für den sicheren Web/JSON-Dienst programmierter Client

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

Wir werden nun einen Client erstellen, der für den sicheren Webdienst programmiert ist:

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

  

16.5.1. Die Klasse [AbstractDao]

Die Klasse [AbstractDao] übernimmt die HTTP-Kommunikation mit dem sicheren Web-/JSON-Server. Wie wir gerade gesehen haben, muss der Client bei dieser HTTP-Kommunikation 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 {
 
    // data
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;
 
    // generic request
    protected String getResponse(User user, String url, String jsonPost) {
 
// url : URL to contact

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

Diese Klasse sieht wie folgt aus:

  

package spring.security.client.entities;
 
public class User {
 
    // properties
    private String login;
    private String password;
 
    // manufacturer
    public User() {
    }
 
    public User(String login, String password) {
        this.login = login;
        this.password = password;
    }
 
    // getters and setters
...
}

Die Methode [getResponse] sieht dann wie folgt aus:


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 {

    // data
    @Autowired
    protected RestTemplate restTemplate;
    @Autowired
    protected String urlServiceWebJson;
 
    private String getBase64(User user) {
        // encodes user and password in base 64 - requires 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())));
    }
 
    // generic request
    protected String getResponse(User user, String url, String jsonPost) {
 
        // url : URL to contact
        // jsonPost: the jSON value to be posted
        try {
            // request execution
            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);
            }
 
            // execute the query
            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 nicht null ist, wird der Authentifizierungsheader hinzugefügt. Die Base64-Kodierung des Benutzers und seines Passworts wird von der Methode [getBase64] in den Zeilen 25–29 übernommen. Beachten Sie, dass diese Methode eine [Base64]-Klasse verwendet, die zu JDK 1.8 gehört.
  • Abgesehen von den vorangegangenen Zeilen bleibt der Code unverändert;

16.5.2. Die [IDao]-Schnittstelle

Alle Methoden der [IDao]-Schnittstelle 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 {
 
    // authentication
    public void authenticate(User user);
 
    // insert product list
    public List<Produit> addProduits(User user, List<Produit> produits);
 
    // removal of all products
    public void deleteAllProduits(User user);
 
    // product list update
    public List<Produit> updateProduits(User user, List<Produit> produits);
 
    // all products obtained
    public List<Produit> getAllProduits(User user);
 
    // inserting a list of categories
    public List<Categorie> addCategories(User user, List<Categorie> categories);
 
    // delete all categories
    public void deleteAllCategories(User user);
 
    // updating a list of categories
    public List<Categorie> updateCategories(User user, List<Categorie> categories);
 
    // obtaining all categories
    public List<Categorie> getAllCategories(User user);
 
    // a special product
    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);
 
    // a special category
    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 URL [/authenticate] des Webdienstes hat;

16.5.3. Die Klasse [Dao]

Alle Methoden in 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:


// authentication
    @Override
    public void authenticate(User user) {
        getResponse(user, "/authenticate", null);
    }
 
    @Override
    public List<Produit> addProduits(User user, List<Produit> produits) {
        // ----------- add products (without category)
        try {
            // mappers jSON
            ObjectMapper mapperPost = context.getBean(ObjectMapper.class);
            mapperPost.setFilters(jsonFilterProduitWithoutCategorie);
            ObjectMapper mapperResponse = mapperPost;
            // request
            Response<List<Produit>> response = mapperResponse.readValue(
                    getResponse(user, "/addProduits", mapperPost.writeValueAsString(produits)),
                    new TypeReference<Response<List<Produit>>>() {
                    });
            // mistake?
            if (response.getStatus() != 0) {
                // 1 exception is thrown
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                // render the core of the server response
                return response.getBody();
            }
        } catch (DaoException e1) {
            throw e1;
        } catch (IOException | RuntimeException e2) {
            throw new DaoException(100, e2);
        }
    }
 

16.5.4. Unit-Tests für die Klasse [Dao]

Die Klasse [Test01] für die 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 context
    @Autowired
    private ApplicationContext context;
    // layer [DAO]
    @Autowired
    private IDaoClient dao;
 
    // users
    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() {
        // the base is cleaned before each test
        log("Vidage de la base de données", 1);
        // table [CATEGORIES] is emptied - by cascade, table [PRODUITS] will be emptied
        dao.deleteAllCategories(admin);
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // fill the tables
        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);
        }
        // add the category - the products will be cascaded in as well
        dao.addCategories(admin, categories);
    }
 
    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // list of categories
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories(admin);
        affiche(categories, context.getBean("jsonMapperCategorieWithoutProduits", ObjectMapper.class));
        // product list
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits(admin);
        affiche(produits, context.getBean("jsonMapperProduitWithoutCategorie", ObjectMapper.class));
        // a few checks
        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 Webdienst-URLs, Testzeilen 96–104;
    • der Benutzer [user] existiert, ist jedoch nicht zur Nutzung der Webdienst-URLs berechtigt, Testzeilen 71–81;
    • der Benutzer [unknown] existiert nicht, Testzeilen 83–93;
  • die Testmethoden entsprechen denen, die bereits für den ungesicherten Webdienst gezeigt wurden, mit dem Unterschied, dass die Methoden der Schnittstelle [IDaoClient] mit dem Benutzer [admin] – der die Berechtigung zur Nutzung der URLs hat – als erstem Parameter aufgerufen werden;

Der Test ist erfolgreich, aber wir sehen, dass er langsamer ist als beim ungesicherten Webdienst. Die Absicherung einer Anwendung erhöht deren Antwortzeiten erheblich. Es gibt einen wichtigen Faktor, der die Leistung des gesicherten Webdienstes beeinflusst: In der Klasse [AppConfig], die ihn konfiguriert, haben wir geschrieben:


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // secure application?
        if (activateSecurity) {
            // the password is transmitted by the header Authorization: Basic xxxx
            http.httpBasic();
            // the HTTP OPTIONS method must be authorized for all
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // only the ADMIN role can use the application
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // all URL
                    .hasRole("ADMIN");
            // no session
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
}

Zeile 17 hat ihren Preis. Sie zwingt den Benutzer dazu, sich bei jedem Zugriff zu authentifizieren. Wenn wir sie auskommentieren, sinkt die Dauer des vorherigen JUnit-Tests 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 nachfolgenden (auch wenn der HTTP-Authentifizierungsheader vom Client gesendet wird, überprüft der Server das Passwort des Benutzers nicht erneut). Bei einem ungesicherten Webdienst sinkt die Dauer des JUnit-Tests auf 2,33 Sekunden.