JavaRush /Курсы /Spring Security /formLogin — точка вхо...

formLogin — точка входа

Spring Security
8 уровень , 0 лекция
Открыта

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-зону. Это не баг, это и есть смысл всей системы.

1
Задача
Spring Security, 8 уровень, 0 лекция
Недоступна
`formLogin` для личной зоны
`formLogin` для личной зоны
1
Задача
Spring Security, 8 уровень, 0 лекция
Недоступна
`formLogin` поверх admin-правила
`formLogin` поверх admin-правила
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ