Skip to content

19. Захист доступу до веб-сервісу за допомогою Spring Security

19.1. Місце Spring Security у веб-додатку

Розглянемо роль Spring Security у розробці веб-додатку. Найчастіше такий додаток будується на багатошаровій архітектурі, наприклад, такій:

  • шар [Spring Security] надає доступ до шару [web] лише авторизованим користувачам.

19.2. Посібник із Spring Security

Ми знову імпортуємо посібник Spring, виконавши кроки 1–3, наведені нижче:

  

Проєкт складається з таких елементів:

  • у папці [templates] містяться сторінки HTML проекту;
  • [Application] — це виконуваний клас проекту;
  • [MvcConfig] — це клас конфігурації Spring MVC;
  • [WebSecurityConfig] — це клас конфігурації Spring Security;

19.2.1. Конфігурація Maven

Проєкт [3] є проєктом Maven. Розглянемо його файл [pom.xml], щоб дізнатися про його залежності:


<?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>
        <!-- тег::безпека[] -->
        <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>
  • рядки 10–14: проект є проектом Spring Boot;
  • рядки 17–20: залежність від фреймворку [Thymeleaf];
  • рядки 22–25: залежність від фреймворку Spring Security;

19.2.2. Види Thymeleaf

  

Вигляд [home.html] має такий вигляд:

  

<!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>
  • рядок 12: атрибут [th:href="@{/hello}"] згенерує атрибут [href] тегу <a>. Значення [@{/hello}] згенерує шлях [<context>/hello], де [context] — це контекст веб-додатка;

Згенерований код HTML має такий вигляд:


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

Вигляд [hello.html] такий:

  

<!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>
  • рядок 9: Атрибут [th:inline="text"] згенерує текст тегу <h1>. Цей текст містить вираз $, який має бути обчислений. Елемент [[${#httpServletRequest.remoteUser}]] є значенням атрибута [RemoteUser] поточного запиту HTTP. Це ім’я користувача, що ввійшов у систему;
  • рядок 10: форма HTML. Атрибут [th:action="@{/logout}"] згенерує атрибут [action] тегу [form]. Значення [@{/logout}] згенерує шлях [<context>/logout], де [context] — це контекст веб-додатка;

Згенерований код HTML має такий вигляд:


<!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>
  • рядок 8: переклад «Hello [[${#httpServletRequest.remoteUser}]]!»;
  • рядок 9: переклад @{/logout};
  • рядок 11: приховане поле з назвою (атрибут name) _csrf;

Останній вигляд [login.html] такий:

  

<!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>
  • рядок 9: атрибут [th:if="${param.error}"] забезпечує, що тег <div> буде згенеровано лише в тому випадку, якщо URL, який відображає сторінку входу, містить параметр [error] (http://context/login?error);
  • рядок 10: атрибут [th:if="${param.logout}"] зумовлює, що тег <div> буде згенеровано лише в тому випадку, якщо URL, що відображає сторінку входу, містить параметр [logout] (http://context/login?logout);
  • рядки 11–23: форма HTML;
  • рядок 11: форма буде відправлена на URL [<context>/login], де <context> — це контекст веб-додатку;
  • рядок 13: поле введення з назвою [username];
  • рядок 17: поле введення з назвою [password];

Згенерований код HTML має такий вигляд:


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

Зверніть увагу, що у рядку 28 Thymeleaf додав приховане поле з назвою [_csrf].

19.2.3. Конфігурація Spring MVC

  

Клас [MvcConfig] налаштовує фреймворк 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");
    }

}
  • рядок 7: анотація [@Configuration] перетворює клас [MvcConfig] на клас конфігурації;
  • рядок 8: клас [MvcConfig] успадковує клас [WebMvcConfigurerAdapter], щоб перевизначити деякі його методи;
  • рядок 10: перевизначення методу батьківського класу;
  • рядки 11–16: метод [addViewControllers] дозволяє пов’язувати URL з поданнями HTML. У ньому встановлюються такі зв’язки:
URL
погляд
/, /home
/templates/home.html
/hello
/templates/hello.html
/login
/templates/login.html

Суфікс [html] та папка [templates] є значеннями за замовчуванням, які використовує Thymeleaf. Їх можна змінити у налаштуваннях. Папка [templates] має знаходитися в кореневому каталозі Classpath проекту:

Вище за [1] папки [java] та [resources] є папками-джерелами (source folders). Це означає, що їхній вміст буде розміщений у кореневому каталозі Classpath проекту. Отже, у [2] папки [hello] та [templates] будуть у кореневому каталозі Classpath.

19.2.4. Налаштування Spring Security

  

Клас [WebSecurityConfig] налаштовує фреймворк 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");
    }
}
  • рядок 9: анотація [@Configuration] перетворює клас [WebSecurityConfig] на клас конфігурації;
  • рядок 10: анотація [@EnableWebSecurity] перетворює клас [WebSecurityConfig] на клас конфігурації Spring Security;
  • рядок 11: клас [WebSecurity] успадковує клас [WebSecurityConfigurerAdapter], щоб перевизначити деякі його методи;
  • рядок 12: перевизначення методу батьківського класу;
  • рядки 13–16: метод [configure(HttpSecurity http)] перевизначено для визначення прав доступу до різних URL у додатку;
  • рядок 14: метод [http.authorizeRequests()] дозволяє пов’язувати URL з правами доступу. У ньому встановлено такі зв’язки:
URL
правило
код
/, /home
доступ без автентифікації

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
доступ лише після автентифікації
http.anyRequest().authenticated();
  • рядок 15: визначає метод аутентифікації. Аутентифікація здійснюється через форму URL [/login], доступну для всіх [http.formLogin().loginPage("/login").permitAll()]. Вихід із системи (logout) також доступний для всіх;
  • рядки 19–21: перевизначають метод [configure(AuthenticationManagerBuilder auth)], який керує користувачами;
  • рядок 20: автентифікація здійснюється за допомогою «жорстко» заданих користувачів [auth.inMemoryAuthentication()]. Користувач тут визначається за допомогою логіна [user], пароля [password] та ролі [USER]. Користувачам з однаковою роллю можна надати однакові права;

19.2.5. Виконуваний клас

  

Клас [Application] має такий вигляд:


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

}
  • рядок 8: анотація [@EnableAutoConfiguration] вимагає від Spring Boot (рядок 3) виконати конфігурацію, яку розробник не зробив явно;
  • рядок 9: перетворює клас [Application] на клас конфігурації Spring;
  • рядок 10: вимагає сканування папки класу [Application] з метою пошуку компонентів Spring. Таким чином будуть виявлені обидва класи [MvcConfig] та [WebSecurityConfig], оскільки вони мають анотацію [@Configuration];
  • рядок 13: метод [main] класу, що виконується;
  • рядок 14: статичний метод [SpringApplication.run] виконується з класом конфігурації [Application] як параметром. Ми вже стикалися з цим процесом і знаємо, що вбудований у залежності Maven проекту сервер Tomcat буде запущений, а проект розгорнуто на ньому. Ми бачили, що чотири об’єкти URL управлялися об’єктом [/, /home, /login, /hello] і що деякі з них були захищені правами доступу.

19.2.6. Тестування додатка

Почнемо з запиту на URL [/], який є одним із чотирьох прийнятих URL. Він пов’язаний із поданням [/templates/home.html]:

 

Запитана URL [/] є загальнодоступною. Саме тому ми її отримали. Посилання [here] таке:

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

URL [/hello] буде запитано, коли ви натиснете на посилання. Воно захищене:

URL
правило
код
/, /home
доступ без авторизації

http.authorizeRequests().antMatchers("/", "/home").permitAll()
autres URL
доступ лише після автентифікації
http.anyRequest().authenticated();

Щоб отримати доступ, необхідно пройти автентифікацію. Spring Security перенаправить браузер клієнта на сторінку автентифікації. Згідно з наведеною конфігурацією, це сторінка URL [/login]. Доступ до неї відкритий для всіх:


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

Отже, ми отримуємо [1]:

Вихідний код отриманої сторінки такий:

<!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>
  • у рядку 7 з’являється приховане поле, якого немає на вихідній сторінці [login.html]. Його додав Thymeleaf. Цей код, який називається CSRF (Cross Site Request Forgery), призначений для усунення вразливості безпеки. Цей токен потрібно надіслати до Spring Security разом з даними автентифікації, щоб автентифікація була прийнята;

Нагадаємо, що Spring Security розпізнає лише комбінацію «user/password». Якщо ввести в [2] будь-що інше, ми отримаємо ту саму сторінку з повідомленням про помилку в [3]. Spring Security перенаправив браузер на сторінку URL [http://localhost:8080/login?error]. Наявність параметра [error] спричинила відображення тегу:


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

Тепер введемо очікувані значення user/password [4]:

  • у [4] ми авторизуємося;
  • у [5] Spring Security перенаправляє нас на URL [/hello], оскільки саме URL ми запитували, коли нас перенаправили на сторінку входу. Ідентифікаційні дані користувача були відображені в наступному рядку [hello.html]:
    <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>

На сторінці [5] відображається така форма:


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

При натисканні на кнопку [Sign Out] буде виконано POST на сторінці URL [/logout]. Цей файл, як і URL та [/login], доступний для всіх:


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

У нашому об’єднанні URL / views ми нічого не визначили для URL та [/logout]. Що відбудеться? Спробуємо:

  • у [6] ми натискаємо кнопку [Sign Out];
  • У [7] ми бачимо, що нас було перенаправлено на URL та [http://localhost:8080/login?logout]. Це Spring Security ініціював це перенаправлення. Наявність параметра [logout] у URL призвела до відображення наступного рядка у вигляді:

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

19.2.7. Висновок

У попередньому прикладі ми могли б спочатку написати веб-додаток, а потім забезпечити його безпеку. Spring Security не є інтрузивним. Можна налаштувати безпеку вже написаного веб-додатка. Крім того, ми з’ясували наступне:

  • можна визначити сторінку автентифікації;
  • автентифікація повинна супроводжуватися токеном CSRF, виданим Spring Security;
  • якщо автентифікація не вдалася, користувача перенаправляють на сторінку автентифікації, причому в параметрі URL додається параметр error;
  • якщо автентифікація проходить успішно, користувач перенаправляється на сторінку, яку він запитував під час автентифікації. Якщо безпосередньо зайти на сторінку автентифікації, оминувши проміжну сторінку, Spring Security перенаправляє нас на URL [/] (цей випадок не розглядався);
  • вихід із системи здійснюється шляхом запиту на сторінку URL [/logout] із параметром POST. Spring Security перенаправляє нас на сторінку автентифікації з параметром logout у URL;

Усі ці висновки ґрунтуються на стандартній поведінці Spring Security. Цю поведінку можна змінити шляхом конфігурації, перевизначивши певні методи класу [WebSecurityConfigurerAdapter].

Попередній підручник не дуже допоможе нам далі. Адже ми будемо використовувати:

  • базу даних для зберігання користувачів, їхніх паролів та ролей;
  • аутентифікацію за заголовком HTTP;

Посібників з того, що ми хочемо зробити тут, досить мало. Рішення, яке буде запропоновано, — це комбінація коду, знайденого тут і там.