Skip to content

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ń:
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 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ń:
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;

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:

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ę 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:

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