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. У ньому встановлюються такі зв’язки:
погляд | |
/templates/home.html | |
/templates/hello.html | |
/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 з правами доступу. У ньому встановлено такі зв’язки:
правило | код | |
доступ без автентифікації | | |
доступ лише після автентифікації |
- рядок 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] таке:
URL [/hello] буде запитано, коли ви натиснете на посилання. Воно захищене:
правило | код | |
доступ без авторизації | | |
доступ лише після автентифікації |
Щоб отримати доступ, необхідно пройти автентифікацію. Spring Security перенаправить браузер клієнта на сторінку автентифікації. Згідно з наведеною конфігурацією, це сторінка URL [/login]. Доступ до неї відкритий для всіх:
http.formLogin().loginPage("/login").permitAll().and().logout().permitAll();
Отже, ми отримуємо [1]:
![]() |
Вихідний код отриманої сторінки такий:
- у рядку 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]:
На сторінці [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;
Посібників з того, що ми хочемо зробити тут, досить мало. Рішення, яке буде запропоновано, — це комбінація коду, знайденого тут і там.














