1. authenticated() и привилегированные зоны
Когда вы впервые ставите .anyRequest().authenticated(), возникает приятное чувство: «ура, мы закрыли приложение». Это полезный baseline, но он решает только первую часть задачи — отсекает анонимных. В реальном API обычно есть зоны, где «просто войти» недостаточно: админские операции, модерация контента, служебные ручки, экспериментальные endpoints.
Представьте офис: на входе турникет по пропуску — это authenticated(). Но серверная, бухгалтерия и комната с печеньками «для сотрудников месяца» требуют уже другого уровня допуска. В нашем проекте Secure Content Platform API это особенно видно: /api/me/** — личная зона пользователя, туда логично пускать любого вошедшего, а вот /api/admin/** — это уже управление пользователями, и там нужен отдельный «бейдж».
Технически в Spring Security эта «привилегированность» сводится к очень простой вещи: у запроса есть Authentication, а внутри него — коллекция строковых прав, GrantedAuthority. Методы hasRole / hasAuthority всего лишь проверяют, есть ли там нужная строка.
Небольшая схема, чтобы это «встало в голову», а не просто осталось набором методов:
flowchart TD
R[HTTP request] --> M[requestMatchers / anyRequest]
M --> RULE[Выбрали правило доступа]
RULE -->|permitAll| OK[Доступ разрешён]
RULE -->|authenticated| A[Есть аутентификация?]
RULE -->|hasRole/hasAuthority| G[Есть нужная authority?]
RULE -->|denyAll| NO[Доступ запрещён всем]
A -->|да| OK
A -->|нет| NO2[Доступ запрещён]
G -->|да| OK
G -->|нет| NO3[Доступ запрещён]
2. hasRole(...): проверка роли и ROLE_
hasRole("...") — это самый дружелюбный способ сказать: «в эту зону пускаем только пользователей с такой-то ролью». Важно понимать одну вещь, которая сначала кажется мелкой, а потом экономит часы жизни: внутри Spring Security роли исторически являются частным случаем authority и обычно хранятся как строки с префиксом ROLE_.
То есть роль ADMIN в реальности почти всегда выглядит как authority-строка ROLE_ADMIN. Метод hasRole("ADMIN") делает вам маленькую услугу: он сам добавляет префикс ROLE_. Поэтому код читается нормально: hasRole("ADMIN"), а не hasAuthority("ROLE_ADMIN").
Пример конфигурации, где ролью закрыта отдельная зона, выглядит так:
http.authorizeHttpRequests(auth -> auth
// В админскую зону пускаем только пользователей с ролью ADMIN
// (Spring Security будет искать authority "ROLE_ADMIN")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
// Всё остальное — просто требует аутентификацию (без доп. привилегий)
.anyRequest().authenticated()
);
Человеческим языком это читается так: «всё приложение требует входа, но админские URL — только для тех, у кого роль ADMIN».
Теперь давайте прямо проговорим то, на чём новички чаще всего спотыкаются. Если вы напишете hasRole("ROLE_ADMIN"), Spring Security добавит префикс ROLE_ ещё раз и начнёт искать ROLE_ROLE_ADMIN. Это примерно как надеть бейдж «VIP» на бейдж «VIP» и удивляться, что охранник смотрит на вас с подозрением.
3. hasAuthority(...): точная проверка права (строка — это контракт)
hasAuthority("...") — более «низкоуровневый», но при этом очень честный инструмент. Он означает: «у пользователя в Authentication.getAuthorities() должна быть ровно такая строка». Никаких автоматических префиксов, никаких «догадайся сам».
Это полезно, когда вы хотите проверять не широкую роль («ADMIN»), а конкретное право («можно публиковать», «можно модерировать», «можно управлять пользователями»). В нашем проекте дальше появится минимальный набор authorities вроде draft:publish, draft:review, user:manage — и вот для таких строк hasAuthority подходит идеально, потому что проверка буквальная.
Здесь важно отделить механику от текущей карты путей проекта. Нам нужен пример буквальной проверки authority, а не новая каноническая зона проекта. Поэтому возьмём отдельный путь, который не пересекается с зоной, построенной на ролях:
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
http.authorizeHttpRequests(auth -> auth
// Требуем конкретное право (строка сравнивается буквально, без префиксов)
.requestMatchers("/api/review/**").hasAuthority("draft:review")
// На всё остальное достаточно быть залогиненным
.anyRequest().authenticated()
);
Тут правило звучит так: «в review-зону пускаем только тех, у кого есть право draft:review».
И вот тут рождается важная инженерная привычка: строка authority — это контракт, почти как имя поля в JSON. Если вы сегодня назвали право draft:review, а завтра случайно написали в конфиге draft:reviews, у вас не «почти работает», а «не работает вообще». Spring Security не угадывает и не исправляет опечатки, и в этом, честно говоря, есть своя красота: ошибки хотя бы детерминированные.
4. hasRole("ADMIN") vs hasAuthority("ROLE_ADMIN")
Сейчас полезно притормозить и разложить всё по полочкам. Визуально эти три варианта выглядят похожими, а ведут себя по-разному.
hasRole("ADMIN") — это фактически «проверь authority ROLE_ADMIN», только с удобным синтаксисом. hasAuthority("ROLE_ADMIN") проверяет ту же строку, но вы берёте ответственность за префикс на себя. А вот hasAuthority("ADMIN") — это уже совсем другая история: вы ищете строку ADMIN без префикса, и она обычно не существует, если вы следуете стандартной role-конвенции Spring Security.
Посмотрите на три правила рядом — разница буквально в кавычках, но смысл сильно меняется:
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
http.authorizeHttpRequests(auth -> auth
// Ищем authority "ROLE_ADMIN" (префикс подставится автоматически)
.requestMatchers("/api/admin/**").hasRole("ADMIN")
// Ищем authority "ROLE_ADMIN" (префикс вы указали явно)
.requestMatchers("/api/admin-a/**").hasAuthority("ROLE_ADMIN")
// Ищем authority "ADMIN" (обычно такой строки у ролей нет)
.requestMatchers("/api/admin-b/**").hasAuthority("ADMIN")
);
Если у пользователя authority ROLE_ADMIN, то он пройдёт первые два правила, а третье — нет.
Чтобы закрепить, вот короткая таблица:
| Проверка | Что реально ищем в GrantedAuthority | Типичный пример значения у пользователя |
|---|---|---|
| hasRole("ADMIN") | ROLE_ADMIN | ROLE_ADMIN |
| hasAuthority("ROLE_ADMIN") | ROLE_ADMIN | ROLE_ADMIN |
| hasAuthority("ADMIN") | ADMIN | почти никогда, если вы используете роль-конвенцию |
Если хотите запомнить с юмором: hasRole("ADMIN") — это «я знаю, что ты имел в виду роль», а hasAuthority("...") — «говори точно, что проверяем, без намёков».
5. Endpoint «кто я»: роли и права
Новичкам очень помогает одна простая практика: научиться быстро смотреть, какие authorities реально лежат в Authentication. Потому что половина проблем с hasRole/hasAuthority — это не «Spring Security сломан», а «у меня не те строки в authorities».
Сделаем маленький debug endpoint. Он не обязан жить в продакшене, но в учебном проекте очень полезен: вы буквально глазами видите, что вам «выдали» как текущему пользователю.
package com.example.securecontent.common;
import java.util.stream.Collectors;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class WhoAmIController {
@GetMapping("/api/whoami")
public String whoAmI(Authentication auth) {
// Authentication сюда подставляет Spring Security (это текущий пользователь)
// В учебном проекте это удобнее всего для диагностики authorities
return auth.getAuthorities().stream()
// Берём строковое представление authority, которое реально проверяют hasRole/hasAuthority
.map(a -> a.getAuthority())
// Склеиваем в одну строку для простого просмотра в браузере/HTTP-клиенте
.collect(Collectors.joining(", "));
}
}
Если вы сейчас используете default user из Spring Security starter, то на практике в ответ часто увидите что-то вроде:
ROLE_USER
И это автоматически объясняет, почему запросы в /api/admin/** с hasRole("ADMIN") будут «не пущать»: у пользователя просто нет такой authority.
Обратите внимание на важный момент: мы не создавали пользователей и не обсуждали хранение паролей. Мы просто научились диагностировать уже имеющуюся картину: «что внутри Authentication прямо сейчас».
6. denyAll(): запрет как стратегия
denyAll() — метод, который звучит как «злой админ в плохом настроении», но на самом деле это довольно полезный инструмент. Он означает: «этот URL запрещён всем, всегда». Даже если пользователь супер-админ, даже если он аутентифицирован, даже если пришёл с правильным заголовком — всё равно нет.
Зачем вообще нужен такой «вечный запрет»? Потому что в реальных проектах бывают зоны «в разработке», «служебные», «только для внутреннего теста», «deprecated, но пока не удалили». И если вы их не закрыли явно, вы надеетесь на удачу. А удача — плохой security-фреймворк.
Простейший пример:
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
http.authorizeHttpRequests(auth -> auth
// Полный запрет: сюда не зайдёт никто (даже админ)
.requestMatchers("/internal/**").denyAll()
// Остальные URL требуют только входа в систему
.anyRequest().authenticated()
);
В голове это читается легко: «всё требует входа, но internal — закрыто вообще для всех».
И есть ещё один классный сценарий, который мы уже видели в предыдущих лекциях, но сейчас он звучит ещё логичнее. В конце конфигурации можно поставить anyRequest().denyAll(), чтобы всё, что вы не описали, было автоматически закрыто. Это такой «страховочный ремень»: вы добавили новый контроллер, забыли прописать правило — и вместо случайно открытого endpoint получите отказ.
Психологически это неприятно, потому что «ой, опять 403». Инженерно это приятно, потому что «ой, мы не протекли наружу».
7. Мини-конфигурация: зоны доступа
Сейчас соберём небольшой цельный фрагмент SecurityFilterChain, где будут все три героя лекции: hasRole, hasAuthority, denyAll. Это не итоговая карта путей проекта, а компактный кусок конфигурации, на котором удобно увидеть рядом три разных типа предикатов.
http.authorizeHttpRequests(auth -> auth
// Админская зона: роль ADMIN (внутри это проверка authority "ROLE_ADMIN")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
// Отдельная review-зона: здесь показываем именно буквальную authority-check
.requestMatchers("/api/review/**").hasAuthority("draft:review")
// Внутренние URL: запрещены вообще всем (в т.ч. администраторам)
.requestMatchers("/internal/**").denyAll()
// Остальное: достаточно быть аутентифицированным
.anyRequest().authenticated()
);
Даже если у вас сейчас ещё нет пользователей с ролью ADMIN и правом draft:review, конфигурация уже полезна: вы видите, как выглядят разные типы правил. А ещё можете проверить, что обычный пользователь (например, default ROLE_USER) гарантированно не попадёт туда, куда не должен. Это тоже важная часть обучения: иногда «не пускает» — это не баг, а первая хорошая новость за день.
8. Типичные ошибки при использовании hasRole, hasAuthority и denyAll
Эти методы выглядят очень простыми, и именно поэтому особенно коварны: мозг расслабляется и начинает «додумывать» за Spring Security. Ошибки тут обычно не синтаксические (IDE всё подсветит), а смысловые: не та строка, не тот префикс, не тот уровень правила. Разберём самые частые грабли, на которые наступают даже аккуратные люди.
Ошибка №1: hasRole("ROLE_ADMIN") и эффект «двойного префикса».
Это классика жанра. Разработчик видит, что в Authentication роль хранится как ROLE_ADMIN, и решает «быть точным». В итоге hasRole("ROLE_ADMIN") начинает искать ROLE_ROLE_ADMIN, и доступ ломается загадочно. Если используете hasRole, аргумент пишется без ROLE_.
Ошибка №2: ожидать, что hasAuthority("ADMIN") эквивалентен hasRole("ADMIN").
По смыслу человеку кажется, что роль и authority — «почти одно и то же», а значит и проверка должна быть похожей. Но hasAuthority ищет строку буквально. Если у вас authority ROLE_ADMIN, то hasAuthority("ADMIN") не сработает. Либо используйте hasRole("ADMIN"), либо пишите hasAuthority("ROLE_ADMIN").
Ошибка №3: превращать authority-строки в случайный набор аббревиатур.
Authority — это строковый контракт. Если вы сегодня называете право CONTENT_REVIEW, завтра REVIEW_CONTENT, а послезавтра content.review, то через неделю уже сами не вспомните, что и где проверяется. На fundamentals-уровне лучше выбрать один стиль (например, draft:review, draft:publish, user:manage) и держаться его, даже если пока эти права ещё не выдаются реальным пользователям.
Ошибка №4: использовать authenticated() там, где нужна привилегия.
Иногда логика «ну пусть войдут, а там посмотрим» приводит к тому, что вы по ошибке открываете админскую ручку для любого вошедшего. В нашем проекте это особенно опасно для /api/admin/** и привилегированных зон вообще. Если зона привилегированная — так и пишем в конфигурации: hasRole(...) или hasAuthority(...), не откладывая решение «на потом».
Ошибка №5: использовать denyAll() как способ «быстро взять всё под контроль».
Иногда разработчик видит неожиданный отказ доступа и в панике ставит denyAll() «чтобы хотя бы понимать, что происходит». Но denyAll — это не инструмент отладки, а кирпичная стена. Если вы случайно заматчили туда важный endpoint, вы заблокируете его вообще для всех. denyAll хорош, когда вы точно знаете: эта зона не должна быть доступна никому.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ