Вход на сайт: форма и сессия
API проверяет пароль в каждом запросе, но человеку в браузере так неудобно. Для сайта используют вход через форму: пользователь один раз вводит имя и пароль, сервер запоминает его в сессии, а браузер хранит номер сессии в cookie JSESSIONID. Для сайта мы выбрали именно сессии, а не токены JWT: они встроены в Spring Security, а для сайта с серверными страницами их достаточно. JWT нужен, когда к API обращается отдельное приложение — например, мобильное; об этом следующий урок.
Вторая цепочка
// src/main/java/com/example/blog/SecurityConfig.java
@Bean
@Order(2)
public SecurityFilterChain webSecurity(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/posts/new").authenticated()
.requestMatchers(HttpMethod.GET, "/", "/posts/*", "/css/**").permitAll()
.requestMatchers("/error").permitAll()
.anyRequest().authenticated())
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/", false)
.permitAll())
.logout(logout -> logout.logoutSuccessUrl("/"));
return http.build();
}У этой цепочки нет securityMatcher, поэтому она берёт всё, что не забрала цепочка API с @Order(1). Строка /posts/new стоит выше /posts/*: иначе форма новой статьи попала бы под «открыто для всех». CSRF здесь включён — для форм в браузере он нужен.
Страница входа
// src/main/java/com/example/blog/PageController.java
@GetMapping("/login")
public String login() {
return "login";
}<!-- src/main/resources/templates/login.html -->
<p class="error" th:if="${param.error}">Неверное имя или пароль.</p>
<form th:action="@{/login}" method="post">
<label>Имя <input type="text" name="username" autofocus></label>
<label>Пароль <input type="password" name="password"></label>
<button type="submit">Войти</button>
</form>Поля должны называться username и password — так их ждёт Spring Security. Сам POST /login обрабатывает он же, писать метод не нужно. При неверном пароле браузер вернётся на /login?error, и param.error покажет сообщение. Благодаря th:action Thymeleaf сам добавит в форму скрытое поле _csrf с токеном.
Кто вошёл
Чтобы показывать в шапке имя и кнопку выхода, подключите дополнение Thymeleaf для Spring Security:
<!-- pom.xml -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency><!-- src/main/resources/templates/fragments/layout.html -->
<html xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<header th:fragment="header">
<a th:href="@{/}">Блог</a>
<a th:href="@{/posts/new}">Новая статья</a>
<span sec:authorize="isAuthenticated()">
<span sec:authentication="name">anna</span>
<form th:action="@{/logout}" method="post" style="display: inline">
<button type="submit">Выйти</button>
</form>
</span>
<a sec:authorize="isAnonymous()" th:href="@{/login}">Войти</a>
</header>Выход — это POST /logout с токеном CSRF, поэтому он сделан формой, а не ссылкой. В контроллере форм пользователя даёт параметр Principal: postService.create(form, principal.getName()), а список авторов из формы можно убрать.
Проверка
Откройте http://localhost:8080/posts/new — браузер перейдёт на /login. После входа Анной в шапке появится anna и кнопка «Выйти», а форма опубликует статью от её имени. Тот же путь через curl выглядит так: ответ на верный вход —
HTTP/1.1 302
Set-Cookie: JSESSIONID=E631F5BDDBCB28E0E6DF15E25E8E6754; Path=/; HttpOnly
Location: http://localhost:8080/а с неверным паролем Location ведёт на /login?error. Отправка формы статьи без поля _csrf получит 403.
Частая ошибка
Если не открыть адрес /error, клиенты API вместо 401 начнут получать переадресацию на форму входа:
HTTP/1.1 302
WWW-Authenticate: Basic realm="Realm", charset="UTF-8"Цепочка API отвечает отказом, Spring показывает страницу /error, а её уже обрабатывает цепочка сайта и требует входа. Строка .requestMatchers("/error").permitAll() возвращает правильный 401.