19. Zabezpieczanie dostępu do serwisu internetowego za pomocą Spring Security
19.1. 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.
19.2. 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 HTML;
- [Application]: jest klasą wykonywalną projektu;
- [MvcConfig]: jest to klasa konfiguracyjna Springa MVC;
- [WebSecurityConfig]: jest klasą konfiguracyjną Spring Security;
19.2.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>
<!-- koniec::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;
19.2.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] w 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;
Ostatni 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 zauważyć, że w wierszu 28 Thymeleaf dodał ukryte pole o nazwie [_csrf].
19.2.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 folderze [2] foldery [hello] i [templates] będą znajdować się w katalogu głównym ścieżki Classpath.
19.2.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;
19.2.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 obiekty URL były zarządzane przez [/, /home, /login, /hello] oraz że niektóre z nich były chronione uprawnieniami dostępu.
19.2.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 [/] jest dostępny dla wszystkich. Dlatego go uzyskaliśmy. Link [here] jest następujący:
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ę uwierzytelniania. Zgodnie z przedstawioną 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 parę użytkownik/hasło. Jeśli wprowadzimy coś innego w [2], otrzymamy tę samą stronę z komunikatem o błędzie w [3]. Spring Security przekierowało przeglądarkę na stronę o adresie 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 kodu [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 kod 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 skojarzeniu URL / widoki nie zdefiniowaliśmy niczego dla URL i [/logout]. Co się stanie? Spróbujmy:
![]() |
- w [6] klikamy przycisk [Sign Out];
- W pliku [7] widać, że zostaliśmy przekierowani do pliku URL i [http://localhost:8080/login?logout]. To Spring Security zainicjowało to przekierowanie. 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>
19.2.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.














