1. authorizeHttpRequests: правила вместо if-ов
Если вы когда-нибудь пытались «защитить API по-быстрому», то почти наверняка приходили к чему-то вроде if(user == null) return 401; прямо в контроллере. Первые пять минут это даже работает — пока не появляется второй контроллер, третий endpoint, «админка», «публичная зона», а потом ещё и внезапная потребность повторять одни и те же правила в нескольких местах. В итоге security превращается в слой хаоса.
Spring Security предлагает другой способ думать о доступе. Вместо того чтобы писать «проверки» в коде бизнес-слоя, мы описываем политику доступа к запросам отдельным декларативным языком. По‑человечески это звучит как короткие фразы: «всё требует входа», «публичное можно всем», «личное — только тем, кто вошёл». Именно здесь появляется authorizeHttpRequests.
Важно поймать идею: authorizeHttpRequests — это не «магическая кнопка безопасности», а способ превратить доступ в набор правил, который можно прочитать глазами сверху вниз. А если правило читается, его намного сложнее сломать случайной правкой (хотя… программисты — народ творческий, так что шанс остаётся).
2. authorizeHttpRequests(...): где живут правила и как их читать
Когда вы впервые видите authorizeHttpRequests, мозг может отреагировать примерно так: «Ага, ещё одна цепочка методов, сейчас будет боль». Но если убрать пафос, это просто блок конфигурации: «вот здесь мы описываем, кто имеет право выполнять HTTP-запросы». Это про authorization, а не про то, как именно пользователь входит в систему и откуда берётся пароль.
Вживую это выглядит так: у нас есть SecurityConfig, внутри — SecurityFilterChain, и мы добавляем одну строку, которая включает режим «правила доступа задаём мы». Дальше внутри лямбды auth -> ... будем писать сами правила.
Минимальный рабочий фрагмент, который уже задаёт смысл (и который удобно держать как стартовую точку), выглядит так:
// Внутри уже собранного SecurityFilterChain меняется вот этот блок:
http.authorizeHttpRequests(auth -> auth
// Правило "по умолчанию": всё должно требовать входа
.anyRequest().authenticated()
);
Сейчас не цепляйтесь к anyRequest() и authenticated() — мы подробно разберём их прямо в этой лекции. Пока важнее увидеть общую форму: authorizeHttpRequests принимает лямбду, внутри которой мы складываем правила. И дальше вам нужно научиться буквально проговаривать это как предложение: «любой запрос должен быть аутентифицирован».
Чтобы закрепить, полезно держать маленькую «шпаргалку-переводчик»:
| Фрагмент DSL | Как это читать по‑человечески | Про что это |
|---|---|---|
| authorizeHttpRequests(...) | “Настраиваем правила доступа к запросам” | Authorization |
| anyRequest() | “Всё, что осталось” | Матчинг запросов |
| permitAll() | “Пустить всех” | Разрешение без условий |
| authenticated() | “Пустить только вошедших” | Требование аутентификации |
В следующих лекциях этого дня появятся более точные правила для конкретных URL, но уже сейчас вы должны уверенно понимать базовый язык: «какой запрос» → «какое условие».
3. Базовые правила доступа
anyRequest() — ловим «всё остальное» одним правилом
Когда мы описываем правила доступа, сразу возникает вопрос: а что делать с запросами, которые мы явно не перечислили? Их можно игнорировать, надеяться на чудо, поставить свечку за стабильность продакшена — но лучше сделать профессионально: определить общее правило, которое срабатывает на всё, что не подошло под более точные условия. Это и есть anyRequest().
anyRequest() читается очень буквально: «любой запрос». На практике это означает «любой запрос, для которого не нашлось более конкретного правила выше». Даже если сегодня у вас правил всего одно, привычка думать «а чем заканчивается политика?» — это уже половина качества security-конфигурации.
Есть ещё одна важная мысль: anyRequest() по смыслу должно быть последним. Это как фраза «вообще всем» — если вы произнесли её первой, дальше уже бессмысленно уточнять. В Spring Security это не просто философия: anyRequest() фактически завершает построение набора правил, и попытки добавить что-то «после» обычно заканчиваются ошибкой конфигурации или недостижимыми правилами.
Простейший пример, который задаёт нижнюю границу безопасности, выглядит так:
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
// Настраиваем правила авторизации запросов
http.authorizeHttpRequests(auth -> auth
// "Любой запрос" (который не совпал с более конкретными правилами) должен быть от аутентифицированного пользователя
.anyRequest().authenticated()
);
Смысл простой: если мы забыли что-то описать отдельно, оно всё равно останется закрытым для анонимных. Это очень близко к принципу secure-by-default, который мы обсуждали ранее: лучше случайно закрыть лишнее (и потом осознанно открыть), чем случайно открыть лишнее (и потом объяснять на ретроспективе, почему у вас /api/admin/users был публичным).
permitAll() — «публично» не значит «безопасно»
permitAll() — это самое простое правило, и именно поэтому с ним легко ошибиться. Оно означает: «разрешить доступ всем, независимо от того, вошёл пользователь или нет». Если вы читаете это как «разрешить всем аутентифицированным» — это не permitAll, это уже ближе к authenticated. Тут всё буквально: permit — «разрешить», all — «всем».
Звучит так, будто permitAll отключает Spring Security. На самом деле нет: Spring Security продолжает жить в приложении, фильтры продолжают отрабатывать, SecurityContext продолжает существовать (просто там будет anonymous пользователь, если никто не вошёл). permitAll — это именно решение на уровне авторизации: «не требуй никаких условий».
Самый «открытый» baseline выглядит так:
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
// Настраиваем правила авторизации запросов
http.authorizeHttpRequests(auth -> auth
// Полностью публичный режим: пускаем всех, даже anonymous
.anyRequest().permitAll()
);
И это полезно как контрастный эксперимент. Например, вы хотите проверить, что endpoint вообще работает, и отделить проблему «у меня контроллер не поднялся» от проблемы «меня не пускает security». В таком режиме приложение снова становится «как будто без охраны».
Но важный профессиональный момент: permitAll на anyRequest() — это не решение, а максимум временная диагностическая позиция. В реальном бэкенд-проекте держать всё публичным — примерно как повесить на сервер табличку «пароль: admin». Возможно, честно, но лучше всё же не надо.
authenticated() — «вошёл в систему» без ролей и прав
Слово authenticated() звучит грозно, но по смыслу оно очень базовое: «пустить только те запросы, где пользователь аутентифицирован». То есть система должна понимать, кто делает запрос, и считать его не anonymous. При этом authenticated() не проверяет, какой именно это пользователь, не проверяет роль, не проверяет «можно ли ему публиковать статьи» и не проверяет «это его черновик или чужой». Это просто барьер «сначала представься».
Очень полезная аналогия (и простите за бытовую аналогию): authenticated() — это турникет по пропуску. Он проверяет, что пропуск есть, но не проверяет, в какую именно комнату вам можно. Для комнат будут другие механики, но сегодня мы учимся включать хотя бы турникет.
В коде это выглядит так:
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
// Настраиваем правила авторизации запросов
http.authorizeHttpRequests(auth -> auth
// Пускаем только запросы, где пользователь уже "не anonymous"
.anyRequest().authenticated()
);
И вот здесь важно связать это с тем, что мы уже знаем из предыдущих дней. Spring Security обычно восстанавливает Authentication в SecurityContext. Если запрос anonymous, внутри часто будет AnonymousAuthenticationToken (мы это обсуждали в контексте SecurityContext). authenticated() как раз означает: «anonymous — не подходит, нужен настоящий пользователь».
Ещё один частый «подводный камень новичка»: authenticated() — это не «проверка логина и пароля в этой точке». Проверка логина/пароля происходит в механизме аутентификации (filters + AuthenticationManager + providers). А authenticated() — это уже про результат: «есть ли у запроса состояние “пользователь подтверждён”».
То есть сначала где-то ранее в цепочке (или раньше по времени) должна случиться аутентификация, и только потом authenticated() может сказать: «да, пользователь вошёл, пускаем». Если аутентификация не произошла, Spring Security отвечает отказом (и вы видите 401, редирект на логин и прочие радости, которые мы уже наблюдали на Day 2).
4. Два baseline: «всё закрыто» и «всё открыто»
На этом этапе полезно сделать то, что любят инженеры: взять два крайних режима и посмотреть, как в них ведёт себя система. Один режим — «по умолчанию всё закрыто для anonymous», второй — «по умолчанию всё открыто для всех». В реальном продукте оба редко бывают финальными, но для тренировки чтения правил они идеальны.
Давайте возьмём наш учебный проект Secure Content Platform API. У него есть зоны, которые по смыслу должны быть публичными (например, чтение опубликованных статей), и зоны, которые должны быть личными или привилегированными (например, /api/me, редакторские и admin-операции). Мы пока не умеем выбирать конкретные URL правилами — это будет следующая лекция. Но уже сейчас можем увидеть, как работает «сетка безопасности».
Чтобы эксперимент был нагляднее, добавим самый простой endpoint для проверки (если он у вас уже есть — отлично, просто используйте существующий). Например, маленький «пинг»:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class PingController {
// Простой endpoint для проверки: отвечает "pong", чтобы было видно, что запрос дошёл до контроллера
@GetMapping("/api/ping")
public String ping() {
return "pong";
}
}
Если каркас цепочки уже собран и базовые механизмы входа оставлены включёнными, для этого сравнения меняется только один кусок — сами правила доступа.
Теперь рассмотрим два варианта.
Вариант А: «всё требует входа».
Мы ставим общий барьер:
// Базовая политика: любой запрос требует аутентификации
http.authorizeHttpRequests(auth -> auth
.anyRequest().authenticated()
);
В таком режиме любой запрос без аутентификации будет отвергнут. Чем именно — зависит от клиента, которым вы стучитесь (браузер, Postman, curl), и от того, какие механизмы аутентификации сейчас включены. По умолчанию Spring Security часто показывает логин-страницу в браузере и/или предлагает basic challenge клиентам. Но смысл один: «анонимно нельзя».
Если вы проверяете через curl, логика обычно выглядит так:
# Запрос без учётных данных — анонимный
curl -i http://localhost:8080/api/ping
# ожидаемо: 401 Unauthorized (или редирект на /login, если вы смотрите как браузер)
А если вы укажете учётные данные (например, дефолтного пользователя, которого Spring Boot создавал ранее), то запрос пройдёт:
# Запрос с basic auth — пользователь "представился"
curl -i -u user:password http://localhost:8080/api/ping
# HTTP/1.1 200
# pong
(Пароль, конечно, будет не password, а тот самый сгенерированный, который вы видели в логах при старте приложения ранее.)
Вариант Б: «всё публично».
Меняем одну строчку:
// Полностью публичная политика: любой запрос разрешён без условий
http.authorizeHttpRequests(auth -> auth
.anyRequest().permitAll()
);
И теперь запрос без аутентификации спокойно проходит:
# Теперь даже анонимный запрос проходит, потому что политика permitAll
curl -i http://localhost:8080/api/ping
# HTTP/1.1 200
# pong
Почему оба варианта плохи как финал? Потому что проект по своей природе не однозонный. В нём одновременно есть публичное чтение и приватные/привилегированные действия. Если «всё закрыто», вы ломаете публичную часть и делаете платформу контента странной: «чтобы прочитать статью — войдите». Если «всё открыто», ломаете безопасность: «чтобы управлять пользователями — просто зайдите по ссылке».
И именно поэтому нам нужен язык, который умеет сказать: «вот этот кусок публичный, а вот этот — только для вошедших». Сегодня мы выучили слова permitAll() и authenticated(). В следующей лекции мы научимся применять эти слова к конкретным URL. Пока же важно не путать смысл этих операторов и уметь прогнозировать поведение приложения в крайних режимах.
5. Типичные ошибки при настройке правил доступа
Ошибка №1: путать permitAll() и «разрешить после входа».
Новичок иногда думает, что permitAll — это «пустить всех пользователей системы». Но на самом деле это «пустить вообще всех, включая anonymous». Это критично в проекте, где есть admin-endpoints. С такой путаницей можно случайно оставить привилегированную операцию публичной, и это будет не баг, а катастрофа (просто очень быстрая).
Ошибка №2: считать authenticated() проверкой роли.
authenticated() не знает ничего про роли ADMIN или EDITOR. Он проверяет только факт «пользователь не anonymous». Из-за этого иногда возникают ложные ожидания: «почему обычный пользователь прошёл туда, куда нельзя?» Ответ: потому что вы требовали только authenticated(), а не более строгие правила. Роли и authorities появятся позже, но уже сейчас важно чётко отделять «вошёл» от «имеет право».
Ошибка №3: использовать anyRequest().permitAll() как временный «фикс» и забыть вернуть назад.
Это классика разработки: «мне мешает security, выключу на минутку», а потом проходит три дня, вы делаете коммит, пушите — и внезапно у вас в репозитории «минутка» стала постоянной. Если уж вы временно открываете всё ради диагностики, делайте это очень осознанно и возвращайте обратно сразу после проверки.
Ошибка №4: не воспринимать конфигурацию как текст правил.
Если вы смотрите на authorizeHttpRequests как на «ну это такой DSL, я его не понимаю, просто скопирую», то дальше будет тяжело: появятся несколько зон, исключения, порядок правил, и вы начнёте ломать систему случайными движениями. Привычка проговаривать каждую строку («для этих запросов такое условие») кажется занудной, но она спасает от магии.
Ошибка №5: забывать, что authorization — это отдельная задача от authentication.
Иногда ожидают, что когда написали authenticated(), Spring Security «как-то сам поймёт, где логин». На самом деле authenticated() — это требование, а не механизм входа. Механизм аутентификации живёт в других частях Spring Security. Сегодня мы специально держим фокус на правилах доступа и учимся не смешивать эти два смысла в голове.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ