JavaRush /Курсы /Spring Security /ROLE_: роли и autho...

ROLE_: роли и authorities

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

1. Значение префикса ROLE_

Когда вы впервые видите ROLE_ADMIN, хочется спросить: «Это что, пароль от админки? Или секретная надпись на двери в серверную?» На самом деле всё прозаичнее: это конвенция именования. Spring Security внутри работает со строками прав, и роли исторически договорились хранить как обычные “authorities”, но с префиксом ROLE_.

К этому месту у нас уже есть role-модель зон и точечные authorities для действий. Осталась последняя путаница: почему один и тот же доступ в одном месте выглядит как ADMIN, а в другом — как ROLE_ADMIN.

В Spring Security есть понятие GrantedAuthority — это буквально «выданное право». И почти везде, где речь про доступ, в итоге сравниваются строки. Роль — это не отдельная сущность в вакууме, а просто authority со специальным «ярлыком» в виде префикса. Поэтому ROLE_ADMIN — это строка, которая означает: «у пользователя есть роль ADMIN».

Важно запомнить одно ключевое правило чтения: если вы видите ROLE_..., это не “другая” система прав, это тот же самый список прав, просто роль выделена префиксом. А методы hasRole(...) существуют в том числе для удобства, чтобы вы писали человеческое имя роли без ROLE_, а Spring Security добавлял префикс сам. В официальной документации прямо сказано, что hasRole — это shortcut к проверке authority с автоматическим префиксом ROLE_.

Небольшой пример «на пальцах», чтобы увидеть, что такое authority как строка:

import org.springframework.security.core.authority.SimpleGrantedAuthority;

// Authority в Spring Security — это буквально строка, которую потом сравнивают при проверке доступа
SimpleGrantedAuthority a = new SimpleGrantedAuthority("ROLE_ADMIN");

// Возвращается та же строка, которую мы передали в конструктор
System.out.println(a.getAuthority()); // ROLE_ADMIN

Здесь нет никакой магии: “право” — это строка "ROLE_ADMIN". Дальше Spring Security сравнивает такие строки с тем, что вы попросили в правилах доступа.

2. Два режима: role-style и exact-string

В этой теме главный источник путаницы — то, что Spring Security даёт вам два “стиля”, и они выглядят похоже, но ведут себя по-разному. Это как два режима у одного и того же инструмента: один делает за вас маленькую работу, другой воспринимает всё буквально. Если перепутать — получите ситуацию «я точно админ, но меня не пускает», а дальше начинается стадия “попробую ещё раз и отключу security”.

Первый режим — role-style. Вы пишете “чистое имя роли”, например ADMIN, и Spring Security сам понимает, что на самом деле сравнивать нужно с "ROLE_ADMIN". Это относится к hasRole("ADMIN") и к roles("ADMIN") у билдера пользователя.

Второй режим — exact-string. Вы пишете строку “как есть”, и Spring Security сравнивает ровно её, без догадок. Это относится к hasAuthority(...) и к authorities(...) при создании пользователя.

Важная подсказка: hasRole — это не «проверка какой-то другой сущности». Это всё тот же GrantedAuthority, просто с автоматическим префиксом.

Чтобы было проще читать глазами, держите в голове мини-таблицу (как “переводчик” из role-style в exact-string):

Где вы пишете правило Что вы пишете Что реально проверяется/хранится
hasRole("ADMIN") ADMIN "ROLE_ADMIN"
hasAuthority("ROLE_ADMIN") ROLE_ADMIN "ROLE_ADMIN"
hasAuthority("draft:publish") draft:publish "draft:publish"

Обратите внимание: draft:publish вообще не роль. Это просто точечное право, и к нему никакой ROLE_ не приклеивается. Оно сравнивается как обычная строка.

3. roles(...) и authorities(...) у пользователя

Когда мы создаём in-memory пользователей, мы фактически вручную формируем “паспорт” пользователя для Spring Security. И тут важно не перепутать, какой метод ожидает какой формат. Новички часто думают: «ну это же просто список прав, какая разница». Разница есть — и она как раз в ROLE_.

Если вы используете roles("ADMIN"), то вы пишете роль без префикса, а Spring Security под капотом превращает это в "ROLE_ADMIN". Это удобно, когда вы хотите хранить роли именно как роли и не видеть ROLE_ руками.

Если вы используете authorities("ROLE_ADMIN"), то вы кладёте строку в точности как она будет храниться и сравниваться. Это удобно, когда вы смешиваете в одном пользователе и роли, и точечные права — потому что точечные права всё равно пишутся exact-string’ами.

Вот минимальная демонстрация разницы.

Вариант A: role-style (префикс добавится автоматически)

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;

// roles("ADMIN") означает: фактически сохранить authority "ROLE_ADMIN"
UserDetails admin = User.builder()
    .username("root")
    .password("{bcrypt}...") // здесь просто пример: в реальном коде храните валидный хэш
    .roles("ADMIN") // префикс ROLE_ добавится автоматически
    .build();

Здесь роль указана как "ADMIN", а внутри Spring Security превратит её в "ROLE_ADMIN".

Вариант B: exact-string (вы пишете всё как есть)

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;

// authorities(...) — это exact-string: что написали, то и будет в списке прав
UserDetails admin = User.builder()
    .username("root")
    .password("{bcrypt}...") // здесь просто пример: в реальном коде храните валидный хэш
    .authorities("ROLE_ADMIN") // префикс вы добавляете сами
    .build();

А здесь вы уже сами отвечаете за префикс. И это нормально. Просто важно не забыть: если вы написали "ADMIN" в authorities(...), то это будет authority "ADMIN", а не роль "ROLE_ADMIN".

Кстати, в официальных примерах in-memory конфигурации можно увидеть, что роли как authority в итоге выглядят именно так: ROLE_USER, ROLE_ADMIN.

4. hasRole(...) и hasAuthority(...) в SecurityFilterChain

В SecurityFilterChain мы пишем правила так, как будто рисуем карту метро: здесь public-ветка, здесь user-ветка, здесь editor-ветка, а тут уже “станция Админская, осторожно, двери закрываются автоматически”. И в этом месте ROLE_ особенно больно стреляет по ногам: одно неверное слово — и у вас внезапно появляется секретный режим «админ не админ».

Ключевой факт: hasRole("ADMIN") автоматически добавляет ROLE_ к тому, что вы передали. Документация формулирует это очень прямо: роль не должна начинаться с ROLE_, потому что она и так будет автоматически дополнена этим префиксом.

Практически это означает:

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 {
    return http.authorizeHttpRequests(a -> a
        // hasRole("ADMIN") -> под капотом будет проверка authority "ROLE_ADMIN"
        .requestMatchers("/api/admin/**").hasRole("ADMIN")
        // hasAuthority(...) -> точная строка, без префиксов и догадок
        .requestMatchers("/api/editor/drafts/*/publish").hasAuthority("draft:publish")
    ).build();
}

Первая строка означает: «пускаем только того, у кого есть authority "ROLE_ADMIN"». Вторая строка означает: «пускаем только того, у кого есть authority "draft:publish"».

А теперь — самый классический “не работает, почему?” вариант:

// Неверно: hasRole сам добавит ROLE_ и получится ROLE_ROLE_ADMIN
.requestMatchers("/api/admin/**").hasRole("ROLE_ADMIN")

Почему неверно? Потому что hasRole добавит префикс ещё раз, и фактически будет искать "ROLE_ROLE_ADMIN". И да, такого права у пользователя, конечно, нет (если только вы не решили устроить маскарад).

Симметричная ошибка — в другую сторону:

// Почти всегда неверно: проверяется authority "ADMIN", а роль обычно хранится как "ROLE_ADMIN"
.requestMatchers("/api/admin/**").hasAuthority("ADMIN")

Это будет проверять authority "ADMIN", а у вас роли обычно живут как "ROLE_ADMIN". То есть вы попросили одно, а в списке лежит другое.

5. Алгоритм чтения ролей и authorities

Путаница с ROLE_ редко возникает из-за сложности Spring Security. Она возникает из-за того, что наш мозг любит «догадываться». Мы видим ADMIN и автоматически думаем про админа. А Spring Security не думает — он сравнивает строки. Поэтому полезно иметь маленький алгоритм чтения, как у опытного редактора: «если слово написано так — читаем так».

Представьте, что вы открыли конфигурацию через месяц и хотите понять, что происходит, не запуская приложение. Вы можете буквально следовать правилу: если вы видите hasRole, мысленно добавьте ROLE_; если вы видите hasAuthority, воспринимайте строку как точное значение; если вы видите roles(...) — префикс будет добавлен автоматически; если authorities(...) — префикса не будет, пока вы не напишете его сами.

Вот простая блок-схема, которая помогает не делать “двойной префикс” и “потерянный префикс”:

flowchart TD
    A["Смотрю на правило доступа"] --> B{"hasRole(...) ?"}
    B -- Да --> C["Переданное значение = имя роли без ROLE_"]
    C --> D["Фреймворк проверит ROLE_ + значение"]
    B -- Нет --> E{"hasAuthority(...) ?"}
    E -- Да --> F["Проверяется точная строка как есть"]
    E -- Нет --> G["Ищу другую проверку (permitAll/authenticated/denyAll)"]

Если вам кажется, что это слишком “детский” алгоритм — отлично. Ровно такие вещи спасают в реальном проекте, когда вы в пятницу вечером чините доступ на проде и очень хотите жить.

6. Naming здесь не меняется, меняется только перевод роли в строку

Сами соглашения уже зафиксированы и не требуют новой схемы. Роли остаются USER / EDITOR / ADMIN, а точечные authorities — exact-string вида resource:action, например draft:publish. Новое в этой лекции только одно: когда роль хранится или проверяется как authority-строка, она получает префикс ROLE_; точечные authorities живут как есть.

Поэтому практическое правило простое. Если вы работаете через hasRole(...) или roles(...), пишите человеческое имя роли без префикса. Если вы работаете через hasAuthority(...) или authorities(...), пишите ровно ту строку, которая должна лежать у пользователя: ROLE_ADMIN, ROLE_EDITOR, draft:publish. Этого уже достаточно, чтобы собрать общую матрицу без двойных префиксов и потерянных прав.

7. Пример: зоны и точечные права

Сейчас у нас учебная, но уже вполне “продуктовая” картина: есть публичная зона, личная зона пользователя, редакторская зона и админская зона. Для зон удобно использовать роли: они короткие и хорошо читаются. А вот для отдельных действий (например, публикации черновика) authority иногда выразительнее, потому что роль EDITOR может быть слишком широкой.

Ниже — небольшой фрагмент, который показывает именно этот подход: зоны через hasRole, а точечное действие — через hasAuthority. Обратите внимание на порядок: точечное правило публикации ставим выше общей editor-зоны, иначе общий matcher перехватит запрос раньше. И ещё одна важная мелочь: в hasRole мы передаём ADMIN, а не ROLE_ADMIN, потому что префикс добавится автоматически.

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 {
    return http.authorizeHttpRequests(auth -> auth
        // Зоны приложения (роуты) ограничиваем ролями
        .requestMatchers("/api/me/**").hasRole("USER")

        // Точечное действие ставим выше общей editor-зоны
        .requestMatchers("/api/editor/drafts/*/publish").hasAuthority("draft:publish")
        .requestMatchers("/api/editor/**").hasRole("EDITOR")

        // Админская зона — роль ADMIN (проверка будет по "ROLE_ADMIN")
        .requestMatchers("/api/admin/**").hasRole("ADMIN")
    ).build();
}

А чтобы это реально работало, in-memory пользователь должен иметь ровно те строки, которые ожидают эти проверки. Если вы выбрали exact-string стиль в authorities(...), то роли надо хранить как ROLE_... (и это нормально):

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;

// Здесь мы явно задаём authorities строками (exact-string)
UserDetails editor = User.builder()
    .username("eva")
    .password("{bcrypt}...") // здесь просто пример: в реальном коде храните валидный хэш
    // Роли в списке authorities храним с ROLE_, а точечное право — как есть
    .authorities("ROLE_USER", "ROLE_EDITOR", "draft:publish")
    .build();

И здесь всё читается без мистики: роль-строки начинаются с ROLE_, а точечное право — без префикса.

8. Типичные ошибки с ROLE_

Ошибка №1: писать hasRole("ROLE_ADMIN").
Эта запись кажется логичной, потому что вы видели ROLE_ADMIN где-то в логах или в authorities(...). Но hasRole сам добавляет ROLE_, и в итоге проверка превращается в ожидание ROLE_ROLE_ADMIN. Документация прямо предупреждает, что роль не должна начинаться с ROLE_, потому что префикс добавляется автоматически.

Ошибка №2: писать hasAuthority("ADMIN"), когда пользователь хранит роль как ROLE_ADMIN.
hasAuthority — это exact-string проверка. Она не “догадывается”, что вы имели в виду роль, и не добавляет ROLE_. Если у пользователя лежит "ROLE_ADMIN", а вы проверяете "ADMIN", то для Spring Security это два разных права. И он будет честно запрещать доступ, хотя вы “в душе” считаете пользователя админом.

Ошибка №3: смешивать naming без явного соглашения.
Иногда в одном месте проекта роль называют ADMIN, в другом — admin, в третьем — ROLE_admin. Строки становятся похожими, но не одинаковыми. В какой-то момент вы начинаете чинить доступ “перестановкой букв”, как будто это пароль от Wi‑Fi в кафе. Лучше один раз договориться: роли — USER/EDITOR/ADMIN, а роль как authority — ROLE_..., и дальше просто держать стиль.

Ошибка №4: пытаться “угадать”, что делает метод, вместо того чтобы помнить: hasRole = shortcut.
Если воспринимать hasRole как отдельную магию, то вы неизбежно запутаетесь, когда увидите рядом hasAuthority("draft:publish"). Правильная картина проще: hasRole — это просто более удобная запись проверки authority с ROLE_-префиксом.

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