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ń:
widok | |
/templates/home.html | |
/templates/hello.html | |
/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ń:
reguła | kod | |
dostęp bez uwierzytelniania | | |
dostęp wyłącznie po uwierzytelnieniu |
- 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:
Po kliknięciu linku pojawi się żądanie URL [/hello]. Jest ono zabezpieczone:
zasada | kod | |
dostęp bez uwierzytelniania | | |
dostęp wyłącznie po uwierzytelnieniu |
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:
- 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]:
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:

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:
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:
- 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:
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:
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:
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:
![]() |
- 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:
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.
























































