Skip to content

16. [Cours]: Zabezpieczenie dostępu do serwisu internetowego za pomocą Spring Security

Słowa kluczowe: architektura wielowarstwowa, Spring, wstrzykiwanie zależności, usługa internetowa / zabezpieczona usługa jSON, klient / serwer

16.1. Support

 

Projekty z tego rozdziału znajdują się w folderze [support / chap-16]. Skrypt SQL służy do wygenerowania bazy danych niezbędnej do przeprowadzenia testów.

16.2. Rola Spring Security w aplikacji internetowej

Umieśćmy Spring Security w kontekście tworzenia aplikacji internetowej. Najczęściej będzie ona oparta na architekturze wielowarstwowej, takiej jak poniższa:

  • warstwa [Spring Security] przyznaje dostęp do warstwy [web] wyłącznie uprawnionym użytkownikom.

16.3. Samouczek dotyczący Spring Security

Ponownie zaimportujemy bibliotekę Spring, wykonując kroki od 1 do 3 poniżej:

  

Projekt składa się z następujących elementów:

  • w folderze [templates] znajdują się strony projektu o nazwie HTML;
  • [Application]: jest klasą wykonywalną projektu;
  • [MvcConfig]: jest klasą konfiguracyjną Springa MVC;
  • [WebSecurityConfig]: jest klasą konfiguracyjną Spring Security;

16.3.1. Konfiguracja Maven

Projekt [3] jest projektem Maven. Przyjrzyjmy się jego plikowi [pom.xml], aby poznać jego zależności:


<?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>
  • wiersze 10–14: projekt jest projektem Spring Boot;
  • wiersze 17–20: zależność od frameworka [Thymeleaf];
  • wiersze 22–25: zależność od frameworka Spring Security;

16.3.2. Widoki Thymeleaf

  

Widok [home.html] wygląda następująco:

  

<!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>
  • wiersz 12: atrybut [th:href="@{/hello}"] wygeneruje atrybut [href] dla tagu [<a>]. Wartość [@{/hello}] wygeneruje ścieżkę [<context>/hello], gdzie [context] stanowi kontekst aplikacji internetowej;

Wygenerowany kod HTML wygląda następująco:


<!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>

Widok [hello.html] wygląda następująco:

  

<!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>
  • wiersz 9: Atrybut [th:inline="text"] wygeneruje tekst tagu [<h1>]. Tekst ten zawiera wyrażenie $, które musi zostać obliczone. Element [[${#httpServletRequest.remoteUser}]] jest wartością atrybutu [RemoteUser] bieżącego zapytania HTTP. Jest to nazwa zalogowanego użytkownika;
  • wiersz 10: formularz HTML. Atrybut [th:action="@{/logout}"] wygeneruje atrybut [action] tagu [form]. Wartość [@{/logout}] wygeneruje ścieżkę [<context>/logout], gdzie [context] to kontekst aplikacji internetowej;

Wygenerowany kod HTML wygląda następująco:


<!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>
  • wiersz 8: tłumaczenie „Hello [[${#httpServletRequest.remoteUser}]]!”;
  • wiersz 9: tłumaczenie @{/logout};
  • wiersz 11: ukryte pole o nazwie (atrybut name) _csrf;

Widok [login.html] wygląda następująco:

  

<!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>
  • wiersz 9: atrybut [th:if="${param.error}"] powoduje, że tag <div> zostanie wygenerowany tylko wtedy, gdy URL, który wyświetla stronę logowania, zawiera parametr [error] (http://context/login?error);
  • wiersz 10: atrybut [th:if="${param.logout}"] powoduje, że tag <div> zostanie wygenerowany tylko wtedy, gdy URL, który wyświetla stronę logowania, zawiera parametr [logout] (http://context/login?logout);
  • wiersze 11–23: formularz HTML;
  • wiersz 11: formularz zostanie przesłany do URL [<context>/login], gdzie <context> to kontekst aplikacji internetowej;
  • wiersz 13: pole wprowadzania danych o nazwie [username];
  • wiersz 17: pole wprowadzania danych o nazwie [password];

Wygenerowany kod HTML wygląda następująco:


<!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>

Warto zwrócić uwagę, że w wierszu 28 Thymeleaf dodał ukryte pole o nazwie [_csrf].

16.3.3. Konfiguracja Spring MVC

  

Klasa [MvcConfig] konfiguruje framework Spring 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");
    }

}
  • wiersz 7: adnotacja [@Configuration] sprawia, że klasa [MvcConfig] staje się klasą konfiguracyjną;
  • wiersz 8: klasa [MvcConfig] rozszerza klasę [WebMvcConfigurerAdapter] w celu przedefiniowania niektórych jej metod;
  • wiersz 10: redefinicja metody klasy nadrzędnej;
  • wiersze 11–16: metoda [addViewControllers] umożliwia powiązanie obiektów URL z widokami HTML. Dokonano następujących powiązań:
URL
widok
/, /home
/templates/home.html
/hello
/templates/hello.html
/login
/templates/login.html

Rozszerzenie [html] oraz folder [templates] to wartości domyślne używane przez Thymeleaf. Można je zmienić w konfiguracji. Folder [templates] musi znajdować się w katalogu głównym ścieżki Classpath projektu:

Powyżej [1], foldery [java] i [resources] są folderami źródłowymi (source folders). Oznacza to, że ich zawartość będzie znajdować się w katalogu głównym ścieżki klasy projektu. Zatem w katalogu [2] katalogi [hello] i [templates] będą znajdować się w katalogu głównym ścieżki Classpath.

16.3.4. Konfiguracja Spring Security

  

Klasa [WebSecurityConfig] konfiguruje framework Spring Security:


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");
    }
}
  • wiersz 9: adnotacja [@Configuration] sprawia, że klasa [WebSecurityConfig] staje się klasą konfiguracyjną;
  • wiersz 10: adnotacja [@EnableWebSecurity] sprawia, że klasa [WebSecurityConfig] staje się klasą konfiguracyjną Spring Security;
  • wiersz 11: klasa [WebSecurity] rozszerza klasę [WebSecurityConfigurerAdapter] w celu przedefiniowania niektórych jej metod;
  • wiersz 12: redefinicja metody klasy nadrzędnej;
  • wiersze 13–16: metoda [configure(HttpSecurity http)] została zdefiniowana na nowo w celu określenia uprawnień dostępu do różnych elementów URL w aplikacji;
  • wiersz 14: metoda [http.authorizeRequests()] umożliwia powiązanie obiektów URL z uprawnieniami dostępu. Dokonano w niej następujących powiązań:
URL
reguła
kod
/, /home
dostęp bez uwierzytelniania

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
dostęp wyłącznie po uwierzytelnieniu
http.anyRequest().authenticated();
  • wiersz 15: określa metodę uwierzytelniania. Uwierzytelnianie odbywa się za pośrednictwem formularza URL [/login], dostępnego dla wszystkich [http.formLogin().loginPage("/login").permitAll()]. Wylogowanie (logout) jest również dostępne dla wszystkich;
  • wiersze 19–21: redefiniują metodę [configure(AuthenticationManagerBuilder auth)], która zarządza użytkownikami;
  • wiersz 20: uwierzytelnianie odbywa się przy użyciu użytkowników zdefiniowanych na stałe ([auth.inMemoryAuthentication()]). Użytkownik jest tutaj zdefiniowany za pomocą loginu [user], hasła [password] oraz roli [USER]. Użytkownikom posiadającym tę samą rolę można przyznać te same uprawnienia;

16.3.5. Klasa wykonywalna

  

Klasa [Application] ma następującą postać:


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

}
  • wiersz 8: adnotacja [@EnableAutoConfiguration] nakazuje Spring Boot (wiersz 3) wykonanie konfiguracji, której programista nie przeprowadził w sposób jawny;
  • wiersz 9: sprawia, że klasa [Application] staje się klasą konfiguracyjną Spring;
  • wiersz 10: nakazuje przeszukanie katalogu klasy [Application] w celu znalezienia komponentów Spring. W ten sposób zostaną wykryte obie klasy [MvcConfig] i [WebSecurityConfig], ponieważ posiadają one adnotację [@Configuration];
  • wiersz 13: metoda [main] klasy wykonywalnej;
  • wiersz 14: statyczna metoda [SpringApplication.run] jest wykonywana z klasą konfiguracyjną [Application] jako parametrem. Spotkaliśmy się już z tym procesem i wiemy, że serwer Tomcat zawarty w zależnościach Maven projektu zostanie uruchomiony, a projekt zostanie na nim wdrożony. Widzieliśmy, że cztery instancje URL były zarządzane przez [/, /home, /login, /hello] oraz że niektóre z nich były chronione uprawnieniami dostępu.

16.3.6. Testowanie aplikacji

Zacznijmy od wywołania URL, który jest jednym z czterech zaakceptowanych URL. Jest on powiązany z widokiem [/templates/home.html]:

 

Żądany URL, czyli [/], jest dostępny dla wszystkich. Dlatego właśnie go uzyskaliśmy. Link [here] wygląda następująco:

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

Po kliknięciu linku pojawi się żądanie URL [/hello]. Jest ono zabezpieczone:

URL
zasada
kod
/, /home
dostęp bez uwierzytelniania

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
dostęp wyłącznie po uwierzytelnieniu
http.anyRequest().authenticated();

Aby uzyskać dostęp, należy się uwierzytelnić. Spring Security przekieruje wówczas przeglądarkę klienta na stronę uwierzytelniającą. Zgodnie z widoczną konfiguracją jest to strona o adresie URL [/login]. Strona ta jest dostępna dla wszystkich:


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

Otrzymujemy zatem [1]:

Kod źródłowy uzyskanej strony wygląda następująco:

<!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>
  • W wierszu 7 pojawia się ukryte pole, którego nie ma na oryginalnej stronie [login.html]. Zostało ono dodane przez Thymeleaf. Kod ten, o nazwie CSRF (Cross Site Request Forgery), ma na celu wyeliminowanie luki w zabezpieczeniach. Ten token musi zostać przesłany do Spring Security wraz z danymi uwierzytelniającymi, aby uwierzytelnienie zostało zaakceptowane;

Pamiętamy, że Spring Security rozpoznaje wyłącznie użytkownika o identyfikatorze user/password. Jeśli wpiszemy coś innego, np. [2], otrzymamy tę samą stronę z komunikatem o błędzie w polu [3]. Spring Security przekierował przeglądarkę na stronę URL [http://localhost:8080/login?error]. Obecność parametru [error] spowodowała wyświetlenie znacznika:


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

Teraz wprowadźmy oczekiwane wartości user/password [4]:

  • w [4] logujemy się;
  • w [5] Spring Security przekierowuje nas do URL [/hello], ponieważ to właśnie o URL prosiliśmy, gdy zostaliśmy przekierowani na stronę logowania. Tożsamość użytkownika została wyświetlona w następującym wierszu [hello.html]:
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

Strona [5] wyświetla następujący formularz:


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

Po kliknięciu przycisku [Sign Out] zostanie wygenerowany POST na stronie URL [/logout]. Ten plik, podobnie jak URL i [/login], jest dostępny dla wszystkich:


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

W naszym powiązaniu URL / widoki nie zdefiniowaliśmy niczego dla URL i [/logout]. Co się stanie? Spróbujmy:

  • w [6] klikamy przycisk [Sign Out];
  • w [7] widzimy, że zostaliśmy przekierowani do URL [http://localhost:8080/login?logout]. To Spring Security zażądało tego przekierowania. Obecność parametru [logout] w URL spowodowała wyświetlenie następującego wiersza w widoku:

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

16.3.7. Wniosek

W powyższym przykładzie mogliśmy najpierw napisać aplikację internetową, a dopiero potem ją zabezpieczyć. Spring Security nie jest inwazyjny. Można wdrożyć zabezpieczenia w już napisanej aplikacji internetowej. Ponadto odkryliśmy następujące kwestie:

  • możliwe jest zdefiniowanie strony uwierzytelniającej;
  • uwierzytelnianiu musi towarzyszyć token CSRF wydany przez Spring Security;
  • jeśli uwierzytelnianie się nie powiedzie, użytkownik zostanie przekierowany na stronę uwierzytelniania z dodatkowym parametrem „error” w tokenie URL;
  • jeśli uwierzytelnienie się powiedzie, użytkownik zostanie przekierowany na stronę, o którą prosił w momencie uwierzytelnienia. Jeśli użytkownik bezpośrednio wywoła stronę uwierzytelniania bez przechodzenia przez stronę pośrednią, wówczas Spring Security przekierowuje go do URL [/] (ten przypadek nie został przedstawiony);
  • wylogowujemy się, wysyłając żądanie do strony URL [/logout] z parametrem POST. Spring Security przekierowuje nas wówczas na stronę uwierzytelniania z parametrem logout w URL;

Wszystkie te wnioski opierają się na domyślnym zachowaniu Spring Security. Zachowanie to można zmienić poprzez konfigurację, redefiniując niektóre metody klasy [WebSecurityConfigurerAdapter].

Poprzedni samouczek nie będzie nam zbytnio pomocny w dalszej części. Będziemy bowiem korzystać z:

  • bazy danych do przechowywania użytkowników, ich haseł i ról;
  • uwierzytelnianie za pomocą nagłówka HTTP;

Istnieje stosunkowo niewiele samouczków dotyczących tego, co chcemy tutaj zrealizować. Proponowane rozwiązanie stanowi połączenie fragmentów kodu znalezionych w różnych źródłach.

16.4. Wdrażanie zabezpieczeń w usłudze internetowej / JSON dotyczącej produktów

16.4.1. Baza danych

Baza danych [dbintrospringdata] zostaje rozbudowana, aby uwzględnić użytkowników, ich hasła i role. Pojawiają się trzy nowe tabele:

Image

Tabela [USERS]: użytkownicy

  • ID: klucz główny;
  • VERSION: kolumna wersjonowania wiersza;
  • IDENTITY: identyfikator opisowy użytkownika;
  • LOGIN: nazwa użytkownika;
  • PASSWORD: hasło użytkownika;

W tabeli USERS hasła nie są przechowywane w postaci jawnej:

 

Algorytmem szyfrującym hasła jest algorytm BCRYPT.

Tabela [ROLES]: role

  • ID: klucz główny;
  • VERSION: kolumna wersji wiersza;
  • NAME: nazwa roli. Domyślnie Spring Security oczekuje nazw w formacie ROLE_XX, na przykład ROLE_ADMIN lub ROLE_GUEST;
 

Tabela [USERS_ROLES]: tabela połączeń USERS / ROLES

Użytkownik może pełnić kilka ról, a jedna rola może obejmować kilku użytkowników. Mamy tu do czynienia z relacją wiele-do-wielu, odzwierciedloną w tabeli [USERS_ROLES].

  • ID: klucz główny;
  • VERSION: kolumna wersji wiersza;
  • USER_ID: identyfikator użytkownika;
  • ROLE_ID: identyfikator roli;
 

16.4.2. Projekt Eclipse

Tworzymy następujący projekt Eclipse:

1
  

  • w [1]: nowy projekt zawierający następujące pakiety:
    • [spring.security.entities]: zawiera encje JPA odpowiadające trzem nowym tabelom w bazie danych;
    • [spring.security.repositories]: zawiera elementy [repositories] z biblioteki Spring Data powiązane z trzema nowymi tabelami;
    • [spring.security.dao]: zawiera usługę opartą na plikach [repositories];
    • [spring.security.config]: zawiera konfigurację projektu, w szczególności dotyczącą bezpiecznego dostępu do serwisu internetowego;
    • [spring.security.boot]: zawiera klasę uruchamiającą zabezpieczoną usługę internetową;

16.4.3. Konfiguracja Maven

Nowy projekt jest projektem Maven skonfigurowanym przez następujący plik [pom.xml]:


<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>
        <!-- Logi Spring -->
        <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>
        <!-- Test Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <!-- wtyczki -->
    <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>
  • wiersze 23–27: przejmujemy istniejące ustawienia wraz z archiwum analizowanej usługi internetowej / json;
  • wiersze 29–32: zależność, która wprowadza klasy Spring Security;
  • wiersze 34–37: biblioteka logów;
  • wiersze 39–42: biblioteka umożliwiająca korzystanie z adnotacji Spring Boot;
  • wiersze 44–48: biblioteka niezbędna do przeprowadzania testów;

16.4.4. Nowe encje [JPA]

Warstwa JPA definiuje trzy nowe encje:

  

Klasa [User] jest odzwierciedleniem tabeli [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 {

    // właściwości
    @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;
    }

    // metody pobierające i ustawiające
...
}
  • wiersz 11: klasa ta rozszerza klasę [AbstractEntity], która jest już używana dla innych encji;

Klasa [Role] jest kopią tabeli [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 {

    // właściwości
    @Column(name="NAME")
    private String name;

    // konstruktory
    public Role() {
    }

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

    // metody pobierające i ustawiające
    public String getName() {
        return name;
    }

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

}

Klasa [UserRole] jest odzwierciedleniem tabeli [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 {

    // klucze obce
    @Column(name = "USER_ID", insertable = false, updatable = false)
    private Long userId;
    @Column(name = "ROLE_ID", insertable = false, updatable = false)
    private Long roleId;

    // UserRole odwołuje się do obiektu typu User
    @ManyToOne
    @JoinColumn(name = "USER_ID")
    private User user;

    // UserRole odwołuje się do roli
    @ManyToOne
    @JoinColumn(name = "ROLE_ID")
    private Role role;

    // konstruktory
    public UserRole() {

    }

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

    // metody pobierające i ustawiające
...
}

  • wiersze 22–24: definiują klucz obcy z tabeli [USERS_ROLES] do tabeli [USERS];
  • wiersze 27–29: definiują klucz obcy z tabeli [USERS_ROLES] do tabeli [ROLES];

16.4.5. [repositories]

Każda z powyższych encji JPA jest zarządzana przez encję [repository] w Spring Data:

  

Interfejs [UserRepository] zarządza dostępem do encji [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> {

    // lista ról użytkownika zidentyfikowanego na podstawie jego identyfikatora
    @Query("select ur.role from UserRole ur where ur.user.id=?1")
    Iterable<Role> getRoles(long id);

    // lista ról użytkownika zidentyfikowanego na podstawie unikalnego loginu
    @Query("select ur.role from UserRole ur where ur.user.login=?1 and ur.user.password=?2")
    Iterable<Role> getRoles(String login, String password);

    // wyszukiwanie użytkownika na podstawie loginu
    User findUserByLogin(String login);
}
  • wiersz 9: interfejs [UserRepository] rozszerza interfejs [CrudRepository] z biblioteki Spring Data (wiersz 4);
  • wiersze 12–13: metoda [getRoles(User user)] pozwala uzyskać wszystkie role użytkownika zidentyfikowanego za pomocą jego [id]
  • wiersze 16–17: to samo, ale dla użytkownika zidentyfikowanego na podstawie loginu i hasła;
  • wiersz 20: wyszukiwanie użytkownika na podstawie loginu;

Interfejs [RoleRepository] zarządza dostępem do jednostek [Role]:


package spring.security.repositories;

import org.springframework.data.repository.CrudRepository;

import spring.security.entities.Role;

public interface RoleRepository extends CrudRepository<Role, Long> {

    // wyszukiwanie roli na podstawie nazwy
    Role findRoleByName(String name);

}
  • wiersz 7: interfejs [RoleRepository] rozszerza interfejs [CrudRepository];
  • wiersz 10: można wyszukiwać rolę według nazwy;

Interfejs [UserRoleRepository] zarządza dostępem do encji [UserRole]:


package spring.security.repositories;

import org.springframework.data.repository.CrudRepository;

import spring.security.entities.UserRole;

public interface UserRoleRepository extends CrudRepository<UserRole, Long> {

}
  • wiersz 5: interfejs [UserRoleRepository] jedynie rozszerza interfejs [CrudRepository], nie dodając do niego nowych metod;

16.4.6. Klasy zarządzania użytkownikami i rolami

  

Spring Security wymaga utworzenia klasy implementującej następujący interfejs [UsersDetail]:

 

Interfejs ten jest tutaj implementowany przez klasę [AppUserDetails]:


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;

    // właściwości
    private User user;
    private UserRepository userRepository;

    // konstruktory
    public AppUserDetails() {
    }

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

    // -------------------------interfejs
    @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;
    }

    // metody pobierające i ustawiające
    ...
}
  • wiersz 14: klasa [AppUserDetails] implementuje interfejs [UserDetails];
  • wiersze 19–20: klasa ta zawiera użytkownika (wiersz 19) oraz repozytorium, które pozwala uzyskać szczegóły dotyczące tego użytkownika (wiersz 20);
  • wiersze 26–29: konstruktor, który tworzy instancję klasy z użytkownikiem i jego repozytorium;
  • wiersze 32–36: implementacja metody [getAuthorities] z interfejsu [UserDetails]. Musi ona utworzyć kolekcję elementów typu [GrantedAuthority] lub typu pochodnego. W tym przypadku używamy typu pochodnego [SimpleGrantedAuthority] (wiersz 36), który zawiera nazwę jednej z ról użytkownika z wiersza 19;
  • wiersze 35–37: przeglądamy listę ról użytkownika z wiersza 19 w celu utworzenia listy elementów typu [SimpleGrantedAuthority];
  • wiersze 42–44: implementują metodę [getPassword] interfejsu [UserDetails]. Zwracane jest hasło użytkownika z wiersza 19;
  • wiersze 42–44: implementują metodę [getUserName] interfejsu [UserDetails]. Zwracana jest nazwa użytkownika z wiersza 19;
  • wiersze 51–54: konto użytkownika nigdy nie wygasa;
  • wiersze 56–59: konto użytkownika nigdy nie zostaje zablokowane;
  • wiersze 61–64: dane logowania użytkownika nigdy nie tracą ważności;
  • wiersze 66–69: konto użytkownika jest zawsze aktywne;

Spring Security wymaga również istnienia klasy implementującej interfejs [AppUserDetailsService]:

 

Interfejs ten jest implementowany przez następującą klasę [AppUserDetailsService]:


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 {
        // wyszukiwanie użytkownika na podstawie loginu
        User user = userRepository.findUserByLogin(login);
        // znaleziono?
        if (user == null) {
            throw new UsernameNotFoundException(String.format("login [%s] inexistant", login));
        }
        // zwracamy szczegóły użytkownika
        return new AppUserDetails(user, userRepository);
    }

}
  • wiersz 12: klasa będzie komponentem Springa, a więc dostępna w swoim kontekście;
  • wiersze 15–16: komponent [UserRepository] zostanie tutaj wstrzyknięty;
  • wiersze 19–28: implementacja metody [loadUserByUsername] z interfejsu [UserDetailsService] (wiersz 10). Parametrem jest login użytkownika;
  • wiersz 21: użytkownik jest wyszukiwany na podstawie jego loginu;
  • wiersze 23–25: jeśli użytkownika nie znaleziono, zgłaszany jest wyjątek;
  • wiersz 27: tworzony i zwracany jest obiekt typu [AppUserDetails]. Jest to rzeczywiście typ [UserDetails] (wiersz 19);

16.4.7. Konfiguracja projektu

Projekt składa się z dwóch klas:

Klasa [DaoConfig] konfiguruje warstwę [DAO] wprowadzoną przez nowy projekt:


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 {

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

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

}
  • wiersz 10: importowana jest klasa konfiguracyjna [spring.data.config.DaoConfig] z projektu [intro-spring-data-01], która implementuje warstwę [DAO] dotyczącą produktów i kategorii;
  • wiersz 8: wskazujemy foldery bieżącego projektu zawierające komponenty Spring Data związane z warstwą [repositories];
  • wiersz 9: określa się foldery bieżącego projektu zawierające komponenty Spring dotyczące warstwy [DAO];
  • wiersz 14: określa się katalogi zawierające encje JPA. Są to zarówno pliki z projektu [intro-spring-data-01], jak i z projektu bezpiecznego serwera. Informacja ta jest przedmiotem bean’a z wierszy 16–19. Bean ten redefiniuje bean o tej samej nazwie z projektu [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;
}

W warstwie [DAO] wiersz 8 skanuje foldery wskazane w wierszu 1. Ze względu na redefinicję komponentu z wierszy 14–17 w projekcie zabezpieczonym (wiersze 16–19) powyższy wiersz 8 będzie odtąd skanował foldery ["spring.data.entities", "spring.security.entities"]. Należy zauważyć, że klasa importowana w wierszu 10 z klasy [spring.security.config.DaoConfig] musi zawierać adnotację [@Configuration], w przeciwnym razie opisane powyżej zjawisko nie będzie działać.

Klasa [SecurityConfig] konfiguruje aspekty bezpieczeństwa projektu. Spotkaliśmy się już z klasą konfiguracyjną Spring Security:


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

Postępujemy w ten sam sposób:

  • wiersz 11: zdefiniujmy klasę rozszerzającą klasę [WebSecurityConfigurerAdapter];
  • wiersz 13: zdefiniuj metodę [configure(HttpSecurity http)], która określa uprawnienia dostępu do różnych elementów URL serwisu internetowego;
  • wiersz 19: zdefiniowanie metody [configure(AuthenticationManagerBuilder auth)], która określa użytkowników i ich role;

Klasa [SecurityConfig] będzie wyglądać następująco:


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;

    // zabezpieczenie
    private boolean activateSecurity = true;

    @Override
    protected void configure(AuthenticationManagerBuilder registry) throws Exception {
        // uwierzytelnianie odbywa się za pomocą komponentu [appUserDetailsService]
        // hasło jest szyfrowane za pomocą algorytmu skrótu BCrypt
        registry.userDetailsService(appUserDetailsService).passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // czy aplikacja jest bezpieczna?
        if (activateSecurity) {
            // hasło jest przesyłane w nagłówku Authorization: Basic xxxx
            http.httpBasic();
            // metoda HTTP OPTIONS musi być autoryzowana dla wszystkich
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // tylko rola ADMIN może korzystać z aplikacji
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // wszystkie URL
                    .hasRole("ADMIN");
            // brak sesji
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
    }
}
  • wiersz 16: w celu aktywacji elementów Spring Security;
  • wiersz 17: dodajemy komponenty Spring z pakietu [spring.security.service];
  • wiersz 18: importujemy fasole z warstwy [DAO], którą właśnie przedstawiliśmy, a także te z niezabezpieczonego serwera WWW / jSON;
  • wiersze 21–22: wstrzykuje się klasę [AppUserDetails], która zapewnia użytkownikom dostęp do aplikacji;
  • wiersz 25: wartość logiczna określająca, czy aplikacja internetowa jest zabezpieczona (true), czy nie (false);
  • wiersze 27–32: metoda [configure(HttpSecurity http)] definiuje użytkowników i ich role. Otrzymuje jako parametr typ [AuthenticationManagerBuilder]. Parametr ten jest uzupełniany o dwie informacje (wiersz 38):
    • odwołanie do usługi [appUserDetailsService] z wiersza 22, która zapewnia dostęp zarejestrowanym użytkownikom. Należy tutaj zauważyć, że nie pojawia się informacja o tym, że są oni zarejestrowani w bazie danych. Mogą więc znajdować się w pamięci podręcznej, być dostarczani przez usługę internetową itp.
    • rodzaj szyfrowania zastosowanego dla hasła. Przypominamy, że zastosowaliśmy algorytm BCrypt;
  • wiersze 34–52: metoda [configure(HttpSecurity http)] definiuje uprawnienia dostępu do URL serwisu internetowego;
  • wiersz 37: w projekcie wprowadzającym widzieliśmy, że domyślnie Spring Security zarządza tokenem CSRF (Cross Site Request Forgery), który użytkownik chcący się uwierzytelnić musiał odesłać do serwera. W tym przypadku mechanizm ten jest wyłączony. W połączeniu z wartością logiczną (isSecured=false) pozwala to na korzystanie z aplikacji internetowej bez zabezpieczeń;
  • wiersz 41: włączamy tryb uwierzytelniania za pomocą nagłówka HTTP. Klient będzie musiał wysłać następujący nagłówek HTTP:
Authorization:Basic code

gdzie „code” to kodowanie ciągu „login:password” za pomocą algorytmu Base64. Na przykład kodowanie Base64 ciągu admin:admin to YWRtaW46YWRtaW4=. Zatem użytkownik o nazwie użytkownika [admin] i haśle [admin] wyśle następujący nagłówek HTTP w celu uwierzytelnienia:

Authorization:Basic YWRtaW46YWRtaW4=
  • wiersze 46–48: wskazują, że wszystkie elementy URL serwisu internetowego są dostępne dla użytkowników posiadających rolę [ROLE_ADMIN]. Oznacza to, że użytkownik nieposiadający tej roli nie ma dostępu do serwisu internetowego;
  • wiersz 50: w trybie [session] użytkownik, który uwierzytelnił się raz, nie musi tego robić przy kolejnych logowaniach. Tutaj wyłączamy ten tryb, więc użytkownik będzie musiał uwierzytelniać się przy każdym logowaniu;

16.4.8. Testy warstwy [DAO]

  

Najpierw tworzymy klasę wykonywalną [CreateUser], która umożliwia utworzenie użytkownika z przypisaną rolą:


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) {
        // składnia: login hasło roleName

        // potrzebne są trzy parametry
        if (args.length != 3) {
            System.out.println("Syntaxe : [pg] user password role");
            System.exit(0);
        }
        // pobieramy parametry
        String login = args[0];
        String password = args[1];
        String roleName = String.format("ROLE_%s", args[2].toUpperCase());
        // kontekst Spring
        AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(DaoConfig.class);
        UserRepository userRepository = context.getBean(UserRepository.class);
        RoleRepository roleRepository = context.getBean(RoleRepository.class);
        UserRoleRepository userRoleRepository = context.getBean(UserRoleRepository.class);
        // czy rola już istnieje?
        Role role = roleRepository.findRoleByName(roleName);
        // jeśli nie istnieje, tworzymy ją
        if (role == null) {
            role = roleRepository.save(new Role(roleName));
        }
        // czy użytkownik już istnieje?
        User user = userRepository.findUserByLogin(login);
        // jeśli nie istnieje, to go tworzymy
        if (user == null) {
            // haszujemy hasło za pomocą bcrypt
            String crypt = BCrypt.hashpw(password, BCrypt.gensalt());
            // zapisujemy użytkownika
            user = userRepository.save(new User(login, login, crypt));
            // tworzymy powiązanie z rolą
            userRoleRepository.save(new UserRole(user, role));
        } else {
            // użytkownik już istnieje – czy posiada żądaną rolę?
            boolean trouvé = false;
            for (Role r : userRepository.getRoles(user.getId())) {
                if (r.getName().equals(roleName)) {
                    trouvé = true;
                    break;
                }
            }
            // jeśli nie znaleziono, tworzy się powiązanie z rolą
            if (!trouvé) {
                userRoleRepository.save(new UserRole(user, role));
            }
        }

        // zamknięcie kontekstu Spring
        context.close();
        // koniec
        System.out.println("Travail terminé...");
    }

}
  • wiersz 17: klasa oczekuje trzech argumentów definiujących użytkownika: jego login, hasło oraz rolę;
  • wiersze 25–27: pobierane są te trzy parametry;
  • wiersz 29: kontekst Springa jest tworzony na podstawie klasy konfiguracyjnej [AppConfig];
  • wiersze 30–32: pobierane są odniesienia do trzech obiektów klasy [Repository], które mogą być przydatne do utworzenia użytkownika;
  • wiersz 34: sprawdzamy, czy rola już istnieje;
  • wiersze 36–38: jeśli nie, tworzymy ją w bazie danych. Będzie miała nazwę typu [ROLE_XX];
  • wiersz 40: sprawdzamy, czy login już istnieje;
  • wiersze 42–49: jeśli nazwa użytkownika nie istnieje, tworzymy ją w bazie danych;
  • wiersz 44: szyfrujemy hasło. Wykorzystujemy tutaj klasę [BCrypt] z biblioteki Spring Security (wiersz 4). Potrzebujemy więc archiwów tego frameworka. Plik [pom.xml] zawiera następującą zależność:

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  • wiersz 46: użytkownik jest zapisywany w bazie danych;
  • wiersz 48: podobnie jak relacja łącząca go z jego rolą;
  • wiersze 51–57: sytuacja, w której login już istnieje – sprawdzamy wówczas, czy wśród jego ról znajduje się już ta, którą chcemy mu przypisać;
  • wiersze 59–61: jeśli nie znaleziono poszukiwanej roli, tworzy się wiersz w tabeli [USERS_ROLES], aby powiązać użytkownika z jego rolą;
  • nie zabezpieczono się przed ewentualnymi wyjątkami. Jest to klasa pomocnicza służąca do szybkiego tworzenia użytkownika z przypisaną rolą.

Po uruchomieniu klasy z argumentami [x x guest] w bazie danych uzyskuje się następujące wyniki:

Tabela [USERS]

Tabela [ROLES]

 

Tabela [USERS_ROLES]

 

Rozważmy teraz drugą klasę [UsersTest], która jest testem JUnit:

  

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() {
        // pobieramy użytkownika [admin]
        User user = userRepository.findUserByLogin("admin");
        // sprawdzamy, czy jego hasło to [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", user.getPassword()));
        // sprawdzamy rolę admin / admin
        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() {
        // pobieramy użytkownika [admin]
        AppUserDetails userDetails = (AppUserDetails) appUserDetailsService.loadUserByUsername("admin");
        // sprawdza się, czy jego hasło to [admin]
        Assert.assertTrue(BCrypt.checkpw("admin", userDetails.getPassword()));
        // sprawdzamy rolę admin / admin
        @SuppressWarnings("unchecked")
        List<SimpleGrantedAuthority> authorities = (List<SimpleGrantedAuthority>) userDetails.getAuthorities();
        Assert.assertEquals(1L, authorities.size());
        Assert.assertEquals("ROLE_ADMIN", authorities.get(0).getAuthority());
    }

    // metoda pomocnicza – wyświetla elementy kolekcji
    private void display(String message, Iterable<?> elements) throws JsonProcessingException {
        System.out.println(message);
        for (Object element : elements) {
            System.out.println(mapper.writeValueAsString(element));
        }
    }
}
  • wiersze 37–44: test wizualny. Wyświetlane są wszyscy użytkownicy wraz z ich rolami;
  • wiersze 46–56: sprawdzamy, czy użytkownik [admin] ma hasło [admin] i rolę [ROLE_ADMIN], korzystając z repozytorium [UserRepository];
  • wiersz 51: [admin] to hasło w postaci niezaszyfrowanej. W bazie jest ono zaszyfrowane zgodnie z algorytmem BCrypt. Metoda [BCrypt.checkpw] pozwala sprawdzić, czy hasło w postaci jawnej po zaszyfrowaniu jest rzeczywiście równe temu z bazy;
  • wiersze 58–69: sprawdzamy, czy użytkownik [admin] ma hasło [admin] i rolę [ROLE_ADMIN], korzystając z usługi [appUserDetailsService];

Testy zakończyły się powodzeniem, generując następujące logi:

----------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. Testy serwisu internetowego

Przetestujemy usługę internetową przy użyciu klienta Chrome o nazwie [Advanced Rest Client]. Będziemy musieli podać nagłówek uwierzytelniający HTTP:

Authorization:Basic code

gdzie [code] to kod Base64 ciągu znaków [login:password]. Aby wygenerować ten kod, można skorzystać z następującego programu:

  

package spring.security.helpers;

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

public class Base64Encoder {

    public static void main(String[] args) {
        // oczekuje dwóch argumentów: login i hasło
        if (args.length != 2) {
            System.out.println("Syntaxe : login password");
            System.exit(0);
        }
        // pobierane są oba argumenty
        String chaîne = String.format("%s:%s", args[0], args[1]);
        // koduje się ciąg znaków
        byte[] data = Base64.encode(chaîne.getBytes());
        // wyświetla się jej kodowanie Base64
        System.out.println(new String(data));
    }

}

Jeśli uruchomimy ten program z dwoma argumentami [admin admin]:

  

otrzymujemy następujący wynik:

YWRtaW46YWRtaW4=

Teraz, gdy wiemy już, jak wygenerować nagłówek uwierzytelniający HTTP, uruchamiamy zabezpieczoną usługę internetową, a następnie za pomocą przeglądarki Chrome [Advanced Rest Client] żądamy listy wszystkich produktów:

  • w [1] żądamy URL z kategoriami;
  • w [2], przy użyciu metody GET;
  • w [3] podajemy nagłówek uwierzytelniający HTTP. Kod [YWRtaW46YWRtaW4=] to kodowanie Base64 ciągu znaków [admin:admin];
  • w [4] wysyłamy polecenie HTTP;

Odpowiedź serwera jest następująca:

  • w [1] znajduje się nagłówek uwierzytelniający HTTP;
  • w [2] serwer zwraca odpowiedź jSON;

Otrzymujemy listę kategorii:

 

Spróbujmy teraz wysłać żądanie HTTP z nieprawidłowym nagłówkiem uwierzytelniającym. Odpowiedź jest wówczas następująca:

  • w [1]: nagłówek uwierzytelniający HTTP;

Otrzymujemy następującą odpowiedź:

  • w [2]: odpowiedź serwisu internetowego;

Teraz spróbujmy z użytkownikiem user / user. Użytkownik ten istnieje, ale nie ma dostępu do serwisu internetowego. Jeśli uruchomimy program do kodowania Base64 z dwoma argumentami [user user]:

  

otrzymujemy następujący wynik:

dXNlcjp1c2Vy
  • w [1]: nieprawidłowy nagłówek uwierzytelniający HTTP;
  • w [2]: odpowiedź serwisu internetowego. Różni się ona od poprzedniej, która brzmiała [401 Unauthorized]. Tym razem użytkownik uwierzytelnił się poprawnie, ale nie posiada wystarczających uprawnień, aby uzyskać dostęp do URL;

Nasza bezpieczna usługa internetowa działa już prawidłowo.

16.4.10. Kod uwierzytelniający URL

  

Utworzymy URL, który pozwoli nam sprawdzić, czy użytkownik ma uprawnienia do dostępu do serwisu internetowego. W tym celu tworzymy następujący nowy kontroler 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 {

    // zależności Spring
    @Autowired
    private ApplicationContext context;

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

}
  • wiersz 15: klasa [AuthenticateController] jest kontrolerem Spring. W związku z tym udostępnia ona URL;
  • wiersz 22: udostępnia URL [/authenticate];
  • wiersz 23: wynik metody zostanie wysłany bezpośrednio do klienta;
  • wiersze 26–27: metoda zwraca jedynie pusty obiekt [Response], ale z wartością [status] równą 0, co wskazuje, że nie wystąpił błąd;

Do czego służy ten URL? Gdy będziemy chcieli po prostu uwierzytelnić użytkownika, poprosimy o niego. Widzieliśmy już, że jeśli warstwa bezpieczeństwa nie zaakceptuje tego użytkownika, zwraca wyjątek. Oto przykład;

W przypadku użytkownika [admin:admin]:

Otrzymujemy pustą odpowiedź, ale nie pojawia się wyjątek.

W przypadku użytkownika [user:user]:

Wystąpił wyjątek.

16.4.11. Wniosek

Dodanie klas niezbędnych dla Spring Security było możliwe bez wprowadzania zmian w oryginalnym projekcie internetowym / json. Ten bardzo korzystny przypadek wynika z faktu, że trzy tabele dodane do bazy danych są niezależne od istniejących tabel. Można je było nawet umieścić w oddzielnej bazie danych. W innych przypadkach dodane tabele mogą być powiązane z istniejącymi tabelami. Wówczas konieczna jest modyfikacja encji JPA, co zazwyczaj ma wpływ na wszystkie warstwy projektu.

16.5. Klient zaprogramowany dla zabezpieczonej usługi internetowej / jSON

Napisaliśmy już klienta dla niezabezpieczonej usługi internetowej / jSON:

Teraz stworzymy klienta zaprogramowanego dla zabezpieczonej usługi internetowej:

Duplikujemy już napisany projekt [intro-webjson-client] do nowego projektu [intro-spring-security-client-01]:

  

16.5.1. Klasa [AbstractDao]

Klasa [AbstractDao] zapewnia komunikację HTTP z zabezpieczonym serwerem WWW / jSON. Jak właśnie widzieliśmy, w tej komunikacji HTTP klient musi teraz wysłać nagłówek uwierzytelniający, na przykład:

Authorization:Basic YWRtaW46YWRtaW4=

Odbywa się to w następujący sposób:


package spring.security.client.dao;

import java.net.URI;
...

public abstract class AbstractDao {

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

    // ogólne żądanie
    protected String getResponse(User user, String url, String jsonPost) {

// adres URL: URL – prosimy o kontakt

  • wiersz 15: metoda generyczna [getResponse], odpowiedzialna za komunikację HTTP z zabezpieczoną usługą internetową, przyjmuje teraz jako pierwszy parametr użytkownika, który żąda URL. Klasa [User] ma następującą postać:

Klasa ta wygląda następująco:

  

package spring.security.client.entities;

public class User {

    // właściwości
    private String login;
    private String password;

    // konstruktor
    public User() {
    }

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

    // metody pobierające i ustawiające
...
}

Metoda [getResponse] przyjmuje zatem następującą postać:


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 {

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

    private String getBase64(User user) {
        // kodowanie użytkownika i hasła w systemie Base64 – wymaga 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())));
    }

    // ogólne zapytanie
    protected String getResponse(User user, String url, String jsonPost) {

        // adres URL: URL – należy się skontaktować
        // jsonPost: wartość jSON do wysłania
        try {
            // wykonanie zapytania
            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);
            }

            // realizujemy zapytanie
            return restTemplate.exchange(request, new ParameterizedTypeReference<String>() {
            }).getBody();
        } catch (URISyntaxException e1) {
            throw new DaoException(20, e1);
        } catch (RuntimeException e2) {
            throw new DaoException(21, e2);
        }
    }

}

  • wiersze 42–44, 49–51: jeśli użytkownik [user] nie jest null, dodaje się nagłówek uwierzytelniający. Szyfrowanie Base64 użytkownika i jego hasła jest realizowane przez metodę [getBase64] w wierszach 25–29. Należy zwrócić uwagę, że metoda ta wykorzystuje klasę [Base64] należącą do JDK 1.8.
  • Poza powyższymi wierszami kod pozostaje bez zmian;

16.5.2. Interfejs [IDao]

Wszystkie metody interfejsu [IDao] przyjmują dodatkowy parametr [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 {

    // uwierzytelnianie
    public void authenticate(User user);

    // dodawanie listy produktów
    public List<Produit> addProduits(User user, List<Produit> produits);

    // usunięcie wszystkich produktów
    public void deleteAllProduits(User user);

    // aktualizacja listy produktów
    public List<Produit> updateProduits(User user, List<Produit> produits);

    // pobieranie wszystkich produktów
    public List<Produit> getAllProduits(User user);

    // dodanie listy kategorii
    public List<Categorie> addCategories(User user, List<Categorie> categories);

    // usunięcie wszystkich kategorii
    public void deleteAllCategories(User user);

    // aktualizacja listy kategorii
    public List<Categorie> updateCategories(User user, List<Categorie> categories);

    // pobieranie wszystkich kategorii
    public List<Categorie> getAllCategories(User user);

    // konkretny 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);

    // określona kategoria
    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);

}
  • wiersz 12: dodaliśmy metodę [authenticate(User user)] w celu uwierzytelnienia użytkownika. Wywołuje ona wyjątek, jeśli użytkownik nie ma uprawnień dostępu do URL [/authenticate] serwisu internetowego;

16.5.3. Klasa [Dao]

Wszystkie metody klasy [Dao] przyjmują dodatkowy parametr [User user], który przekazują do metody generycznej [getResponse] klasy [AbstractDao]. Oto dwa przykłady:


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

    @Override
    public List<Produit> addProduits(User user, List<Produit> produits) {
        // ----------- dodawanie produktów (bez kategorii)
        try {
            // mapery jSON
            ObjectMapper mapperPost = context.getBean(ObjectMapper.class);
            mapperPost.setFilters(jsonFilterProduitWithoutCategorie);
            ObjectMapper mapperResponse = mapperPost;
            // zapytanie
            Response<List<Produit>> response = mapperResponse.readValue(
                    getResponse(user, "/addProduits", mapperPost.writeValueAsString(produits)),
                    new TypeReference<Response<List<Produit>>>() {
                    });
            // błąd?
            if (response.getStatus() != 0) {
                // rzucamy 1 wyjątek
                throw new DaoException(response.getStatus(), response.getMessages());
            } else {
                // zwracany jest rdzeń odpowiedzi serwera
                return response.getBody();
            }
        } catch (DaoException e1) {
            throw e1;
        } catch (IOException | RuntimeException e2) {
            throw new DaoException(100, e2);
        }
    }

16.5.4. Testy jednostkowe klasy [Dao]

Klasa [Test01] zawierająca testy jednostkowe klasy [Dao] została zmodyfikowana w następujący sposób:

  

package client.tests.junit;

...

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

    // kontekst Spring
    @Autowired
    private ApplicationContext context;
    // warstwa [DAO]
    @Autowired
    private IDaoClient dao;

    // użytkownicy
    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() {
        // przed każdym testem czyszczymy bazę danych
        log("Vidage de la base de données", 1);
        // opróżniamy tabelę [CATEGORIES] – w efekcie kaskadowym zostanie opróżniona tabela [PRODUITS]
        dao.deleteAllCategories(admin);
        // --------------------------------------------------------------------------------------
        log("Remplissage de la base", 1);
        // wypełnia się tabele
        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);
        }
        // dodajemy kategorię – w wyniku kaskadowego działania produkty również zostaną wstawione
        dao.addCategories(admin, categories);
    }

    @Test
    public void showDataBase() throws BeansException, JsonProcessingException {
        // lista kategorii
        log("Liste des catégories", 2);
        List<Categorie> categories = dao.getAllCategories(admin);
        affiche(categories, context.getBean("jsonMapperCategorieWithoutProduits", ObjectMapper.class));
        // lista produktów
        log("Liste des produits", 2);
        List<Produit> produits = dao.getAllProduits(admin);
        affiche(produits, context.getBean("jsonMapperProduitWithoutCategorie", ObjectMapper.class));
        // kilka sprawdzeń
        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);
    }
...
}
  • podczas inicjalizacji klasy testowej, w wierszach 21–26, tworzonych jest trzech użytkowników:
    • użytkownik [admin] ma dostęp do URL serwisu internetowego, test w wierszach 96–104;
    • użytkownik [user] istnieje, ale nie ma uprawnień do korzystania z URL serwisu internetowego, test w wierszach 71–81;
    • użytkownik [unknown] nie istnieje, test wierszy 83–93;
  • metody testowe są takie same jak te omówione wcześniej dla niezabezpieczonej usługi internetowej, z tą różnicą, że metody interfejsu [IDaoClient] są wywoływane z użytkownikiem [admin] jako pierwszym parametrem, który ma uprawnienia do korzystania z URL;

Test zakończył się powodzeniem, ale widać, że działa wolniej niż w przypadku niezabezpieczonej usługi internetowej. Zabezpieczenie aplikacji znacznie wydłuża czas jej odpowiedzi. Warto zwrócić uwagę na istotny czynnik wpływający na wydajność zabezpieczonej usługi internetowej: w klasie [AppConfig], która ją konfiguruje, zapisaliśmy:


    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // CSRF
        http.csrf().disable();
        // czy aplikacja jest bezpieczna?
        if (activateSecurity) {
            // hasło jest przekazywane w nagłówku Authorization: Basic xxxx
            http.httpBasic();
            // metoda HTTP OPTIONS musi być autoryzowana dla wszystkich
            http.authorizeRequests() //
                    .antMatchers(HttpMethod.OPTIONS, "/", "/**").permitAll();
            // tylko rola ADMIN może korzystać z aplikacji
            http.authorizeRequests() //
                    .antMatchers("/", "/**") // wszystkie URL
                    .hasRole("ADMIN");
            // brak sesji
            http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
        }
}

Wiersz 17 wiąże się z pewnym obciążeniem. Zmusza on użytkownika do uwierzytelniania się przy każdym dostępie. Jeśli go skomentujemy, czas trwania poprzedniego testu JUnit skraca się z 10,57 sekundy do 4,21 sekundy, dzieje się tak, ponieważ użytkownik [admin] uwierzytelnia się tylko podczas pierwszego testu, a nie podczas kolejnych (nawet jeśli nagłówek uwierzytelniający HTTP jest wysyłany przez klienta, serwer nie weryfikuje ponownie hasła użytkownika). W przypadku niezabezpieczonej usługi internetowej czas trwania testu JUnit spada do 2,33 sekundy.