hasRole / hasAuthority / denyAll

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

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 хорош, когда вы точно знаете: эта зона не должна быть доступна никому.

1
Задача
Spring Security, 5 уровень, 3 лекция
Недоступна
Админ-зона через `hasRole("ADMIN")`
Админ-зона через `hasRole("ADMIN")`
1
Задача
Spring Security, 5 уровень, 3 лекция
Недоступна
Точная authority и полностью закрытая internal-зона
Точная authority и полностью закрытая internal-зона
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ