1. Вход поверх правил доступа
Когда мы настраиваем authorizeHttpRequests и закрываем /api/me правилом authenticated(), мы делаем очень правильную вещь: больше никто случайно не прочитает личные данные. Но тут появляется второй вопрос, который новичков часто ловит в засаду: «Окей, я закрыл дверь. А ключ где выдаётся?». И вот тут formLogin неожиданно становится не про “UI”, а про инженерную завершённость security-модели.
Представьте, что у нас уже есть такой security baseline (пример намеренно компактный):
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// Правила доступа: какие URL публичные, какие требуют входа, а какие закрыты вообще
http.authorizeHttpRequests(auth -> auth
// Публичная зона: доступна без аутентификации
.requestMatchers("/api/public/**").permitAll()
// Приватная зона: доступна только аутентифицированному пользователю
.requestMatchers("/api/me").authenticated()
// Всё остальное режем, чтобы случайно не открыть лишнее
.anyRequest().denyAll()
);
// Собираем SecurityFilterChain (цепочку фильтров Spring Security)
return http.build();
}
С точки зрения backend-разработчика это выглядит прекрасно. У нас есть публичная зона и приватная зона, всё аккуратно. Но если вы откроете /api/me в браузере, вы быстро поймёте, что «закрыто» — это ещё не «готово». Браузеру (и человеку) нужен понятный сценарий, как стать аутентифицированным пользователем.
В этот момент полезно увидеть простую таблицу «что уже есть / чего не хватает»:
| Что уже сделано (дни 1–7) | Что всё ещё не решено для браузерного пользователя |
|---|---|
| Есть пользователи (UserDetailsService) | Где им вводить логин/пароль? |
| Пароли encoded (PasswordEncoder) | Как отправить credentials «нормально», а не через танцы с заголовками? |
| Есть rules (authenticated(), hasRole()) | Как пройти из anonymous‑состояния в authenticated‑состояние? |
| Есть защищённый endpoint (/api/me) | Как «дойти» до него обычному человеку? |
И вот на этом месте появляется главный смысл сегодняшнего дня: formLogin — это мост между уже понятной вам моделью пользователей/паролей/ролей и реальным browser-friendly способом аутентификации.
2. formLogin как сценарий аутентификации
Слово formLogin многих обманывает. Кажется, что это “просто страница логина”. Но в Spring Security это прежде всего включение цельного сценария: как запросы ведут себя, когда пользователь ещё не вошёл, где он вводит логин и пароль, что считается успехом, что считается ошибкой и как приложение возвращается в нормальный режим работы после login. Страница — лишь видимая часть айсберга (и да, она обычно не выигрывает конкурсы красоты).
Минимальная мысль, которую важно зафиксировать: formLogin() не отменяет вашу архитектуру. Он не говорит «забудь про AuthenticationManager и UserDetailsService». Наоборот: он говорит «теперь давай дадим пользователю нормальный вход, используя те же механизмы проверки, которые ты уже изучил».
Вот самый минимальный «включатель» form login (с нулём кастомизации):
import static org.springframework.security.config.Customizer.withDefaults;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// Любой запрос требует аутентификации
http.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
// Включаем сценарий входа по форме (генерируемая страница логина + обработка POST логина)
.formLogin(form -> form
// Важно: точка входа должна быть доступна, иначе «дверь с замком снаружи»
.permitAll()
);
return http.build();
}
Обратите внимание: мы не написали ни одного контроллера «/login», не написали HTML, не делали “ручную” проверку пароля. Мы просто сказали: «в моём приложении есть сценарий входа по форме». И дальше Spring Security подключает инфраструктуру, чтобы браузерный вход стал возможен.
Важно не путать: formLogin нужен не только «ради людей». Он нужен и вам, как разработчику, чтобы видеть и понимать, как Spring Security превращает anonymous запрос в authenticated контекст. Это один из самых наглядных способов перестать воспринимать security как “магическую обёртку вокруг контроллеров”.
3. formLogin и знакомые компоненты
Сейчас будет важный момент, который спасает кучу нервов: formLogin не приносит новую модель пользователей. Он просто добавляет «входную точку», через которую пользователь может предоставить credentials, а framework — прогнать их через уже знакомую вам цепочку.
Схема на уровне «как это читать глазами backend-разработчика» выглядит так:
flowchart TD
A["Браузер: POST логин/пароль"] --> B["Фильтр formLogin"]
B --> C["AuthenticationManager"]
C --> D["DaoAuthenticationProvider"]
D --> E["UserDetailsService"]
D --> F["PasswordEncoder"]
C --> G["Успех: Authentication authenticated=true"]
G --> H["SecurityContext"]
В этой схеме почти всё — старые знакомые. Новое здесь только то, что появляется понятная точка, где браузер может отправить логин и пароль «как в реальном мире»: через форму.
Чтобы совсем закрепить, давайте посмотрим на user model, которую мы уже делали. Она не меняется из-за formLogin:
import org.springframework.context.annotation.Bean;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
@Bean
UserDetailsService users(PasswordEncoder encoder) {
// Encoder нужен, чтобы хранить и сравнивать пароли не в открытом виде
UserDetails maria = User.builder()
// Логин пользователя
.username("maria")
// Пароль в хешированном виде (Spring Security будет сравнивать через PasswordEncoder)
.password(encoder.encode("password"))
// Роли пойдут в hasRole(...) правила
.roles("USER")
.build();
// Простейшее хранилище пользователей в памяти (для учебных примеров)
return new InMemoryUserDetailsManager(maria);
}
Если вчера вы понимали, зачем тут PasswordEncoder, то сегодня логика такая: formLogin просто добавит возможность пользователю “maria” войти через браузер, а Spring Security — проверить её пароль через тот же encoder, а её роль — использовать в тех же hasRole("...") правилах.
Полезно держать в голове и маленькую таблицу «кто за что отвечает», потому что в formLogin часто обвиняют не тот компонент:
| Компонент | Роль в сценарии |
|---|---|
| formLogin() | Даёт browser-friendly вход: где и как вводятся credentials |
| AuthenticationManager | Принимает решение “аутентифицирован или нет” |
| DaoAuthenticationProvider | Проверяет username/password через user store |
| UserDetailsService | Загружает пользователя по username |
| PasswordEncoder | Сравнивает raw пароль с encoded хешем |
| authorizeHttpRequests | Решает “пускать или не пускать” в endpoint после того, как user уже известен |
Заметьте, что formLogin не заменяет authorization. Даже если пользователь вошёл, это не значит, что он стал админом. Это значит только, что он перестал быть anonymous.
4. Проект без formLogin: тупик для браузера
На уровне проекта Secure Content Platform API ситуация обычно выглядит так: мы сделали endpoint /api/me, он требует аутентификацию, и всё честно закрыто. Например, самый «учебный» вариант контроллера может быть таким:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
class MeController {
@GetMapping("/api/me")
String me() {
// Приватный эндпоинт: его смысл — показать, что без аутентификации сюда нельзя
return "private zone";
}
}
И при этом — вот типичный security config, который закрывает личную зону:
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
// Публичные URL — без входа
.requestMatchers("/api/public/**").permitAll()
// Приватный endpoint — только для вошедших пользователей
.requestMatchers("/api/me").authenticated()
// Остальное закрываем
.anyRequest().denyAll()
);
return http.build();
}
С точки зрения security это нормально. Но с точки зрения UX это напоминает закрытый клуб, у которого на двери висит табличка «Вход только для членов клуба», но никакого ресепшена, где можно стать членом клуба, нет.
Да, можно сказать: «Пусть пользователь как-то сам отправит пароль». Но это в реальности превращается либо в “а где кнопка логина?”, либо в “я открыл в браузере — и мне просто сказали ‘Unauthorized’”. И новичок в этот момент часто начинает делать одну из двух странных вещей: либо пытается «открыть /api/me всем, чтобы оно работало», либо пишет самодельный /api/login, где руками сравнивает пароль строкой. Оба пути плохие (первый ломает безопасность, второй ломает архитектуру).
formLogin появляется ровно как ответ на эту дыру: он добавляет нормальный сценарий входа, не меняя ваш домен, не трогая контроллеры и не заставляя вас писать “security-логики” в бизнес-коде.
5. Подключаем formLogin в проекте
Сейчас мы сделаем то, что методически очень важно: добавим formLogin, не трогая остальную модель. То есть роли, правила доступа, UserDetailsService остаются как есть. Мы просто включаем browser-friendly вход.
Вот реалистичный (но всё ещё компактный) вариант для текущего этапа:
import static org.springframework.security.config.Customizer.withDefaults;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(auth -> auth
// Публичная зона
.requestMatchers("/api/public/**").permitAll()
// Любые /api/me/** — только для вошедших
.requestMatchers("/api/me/**").authenticated()
// Админка — только роль ADMIN
.requestMatchers("/api/admin/**").hasRole("ADMIN")
// Остальное — закрыто
.anyRequest().denyAll()
)
// Включаем formLogin: это добавляет понятный браузерный сценарий входа
.formLogin(form -> form
// Даём доступ к странице/процессу логина, иначе вход будет недостижим
.permitAll()
);
return http.build();
}
Что это даёт в поведении приложения (человеческим языком, без попытки «прыгнуть в следующий уровень курса»):
Когда анонимный пользователь (в браузере) пытается попасть в защищённую зону, система теперь не просто говорит «нельзя», а предлагает понятный сценарий: «войдите». После успешного входа запросы начинают выполняться уже как запросы конкретного пользователя, и ваши правила authenticated()/hasRole("...") наконец-то начинают работать так, как задумывались — не в теории, а руками.
И вот тут важно поймать правильную мысль: мы не “настроили страницу”. Мы включили инфраструктуру, которая использует те же UserDetailsService и PasswordEncoder, но делает вход возможным для браузера. Страница — просто визуализация этого механизма.
Если вы любите “контрольный вопрос к себе”, то он такой: «Какая часть кода проверяет пароль?». Правильный ответ: не контроллер, не “страница”, и даже не formLogin. Пароль проверяет AuthenticationManager через provider’ы, и это мы уже проходили. formLogin только запускает этот процесс в браузерном сценарии.
6. Типичные ошибки при formLogin
Ошибка №1: воспринимать formLogin как историю про UI, а не про security-сценарий.
На formLogin у новичков часто срабатывает “рефлекс фронтендера”: раз вижу форму — значит, всё про UI. Из-за этого появляется ошибка мышления, что formLogin — это какая-то отдельная штука, которая “сама всё решит”.
Ошибка №2: ослаблять или ломать правила доступа, чтобы «просто работало».
В результате человек начинает ослаблять или ломать правила доступа, чтобы “просто работало”, и получает систему, где логин есть, а безопасности нет.
Ошибка №3: писать “свой login endpoint” в контроллере и сравнивать пароль вручную.
Другая популярная ошибка — пытаться написать “свой login endpoint” на уровне контроллера и сравнить пароль вручную. Обычно это выглядит примерно так: достали пользователя из UserDetailsService, сравнили пароль строкой, вернули “ok”.
Это ломает сразу две вещи: во‑первых, вы обходите PasswordEncoder (а значит, либо пароли у вас окажутся в открытом виде, либо сравнение никогда не будет работать), во‑вторых, вы обходите главный механизм Spring Security — создание корректного Authentication и помещение его в SecurityContext. В итоге ваш “логин” вроде бы отвечает 200 OK, но приложение всё равно считает пользователя anonymous. Магия? Нет, просто вы не сделали того, что нужно security-инфраструктуре.
Ошибка №4: закрыть логин-поток слишком широкими правилами (например, denyAll()).
Ещё один классический момент: разработчик включает formLogin, но внезапно «всё ломается» из‑за слишком широких правил типа anyRequest()denyAll(). Он забывает, что login flow — это тоже набор endpoint’ов, которые должны быть достижимы. На сегодняшней лекции мы ещё не кастомизируем URL’ы, но уже полезно помнить сам принцип: точка входа должна быть доступна, иначе это будет дверь, к которой прикрутили замок… снаружи.
Ошибка №5: путать «войти» и «получить права администратора».
И наконец, очень частая путаница — смешивание «войти» и «получить права администратора». formLogin отвечает на вопрос “кто ты?”, а ваши hasRole("ADMIN") и остальные правила отвечают “что тебе можно?”. Поэтому после успешного входа пользователь с ролью USER всё равно должен получать запрет на admin-зону. Это не баг, это и есть смысл всей системы.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ