JavaRush /Курсы /Spring Security /Роли USER /

Роли USER / EDITOR / ADMIN

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

1. Фиксируем роли без бюрократии

Если вы когда-нибудь видели проект, где доступ ограничивается фразой «ну это же очевидно, что сюда нельзя», то вы видели проект, который однажды будет чиниться ночью под звуки тихого плача. Роли нужны не для красоты и не для галочки, а чтобы превратить безопасность в договорённость, записанную в коде. И чем раньше мы это сделаем, тем меньше будет сюрпризов.

До этого момента у нас уже были правила вроде .hasRole("ADMIN"), но они были немного «в вакууме»: не было чёткого соглашения, какие роли вообще существуют, как они называются и что означают в домене проекта. В результате конфигурация легко превращается в набор строк, которые похожи на заклинания: работают, но страшно трогать.

Фиксация роль-модели на этом этапе особенно важна по методической причине: мы ещё не добавили БД, form login, сессии и всё остальное, что привнесёт дополнительные движущиеся части. Сейчас идеальный момент сделать одну простую вещь: договориться о трёх ролях и держать их стабильными по всему курсу. Это позволит нам дальше развивать проект без ощущения, что безопасность каждый день «перепридумывается».

Роль как бейджик зоны доступа

В слове role легко услышать «роль в театре»: один играет героя, другой злодея. Но в Spring Security роль — это намного более приземлённая вещь: это ярлык, который говорит системе, к какой зоне доступа относится пользователь. Роль отвечает на вопрос «в какой комнате этому человеку можно находиться», а не «какие именно кнопки ему можно нажимать». Это coarse-grained уровень.

Пока мы в fundamentals-части, нам важно сделать роль-модель короткой и читаемой. Для нашего проекта роль — это способ не расписывать на каждый URL отдельные мини-разрешения. Мы говорим: «это пользователь», «это редактор», «это администратор». И уже поверх этого дальше можно будет уточнять правила точечными правами (authorities), но сегодня мы фокусируемся именно на роли как на «пропуске в зону».

Ещё один важный момент: роль — это не «тип аккаунта в базе», не «класс пользователя» и не enum в домене, который всё решает. В Spring Security роль — это часть authorization (права доступа), а не authentication (подтверждение личности). То есть сначала мы узнаём, кто ты, а потом смотрим, какие у тебя роли.

2. Роль-модель проекта: USER, EDITOR, ADMIN

Чтобы роли не были абстракцией, привяжем их к нашей предметной области. Мы строим платформу контента: есть публичные статьи, личная зона пользователя, редакторская модерация и админское управление пользователями. На этом домене три роли действительно естественны и не выглядят искусственно. Важно, что они короткие, стабильные и одинаково читаются в коде и в разговоре.

Давайте зафиксируем смысл ролей так, чтобы потом не додумывать его «по вдохновению»:

Роль Что означает в проекте Пример зоны (путь)
USER обычный аутентифицированный пользователь, у которого есть личная зона /api/me/**, /api/drafts/**
EDITOR пользователь, который может модерировать и публиковать контент /api/editor/**
ADMIN пользователь, который управляет пользователями и их состояниями /api/admin/**

Обратите внимание: публичные эндпоинты (/api/public/**) роли не требуют вовсе — это отдельная зона, которая живёт по правилу permitAll(). То есть роль-модель начинается не с «публичного» (это не роль), а с первого защищённого уровняUSER.

Если хочется увидеть это как карту, можно представить очень простую схему зон:

flowchart TD
    A[anonymous] -->|GET /api/public/**| P[public zone]
    U[role USER] -->|/api/me/**, /api/drafts/**| M[personal zone]
    E[role EDITOR] -->|/api/editor/**| R["review/moderation zone"]
    AD[role ADMIN] -->|/api/admin/**| Z[admin zone]

В реальности редактор и админ обычно тоже «люди», у которых есть профиль и личная зона. Поэтому мы почти всегда хотим, чтобы EDITOR и ADMIN могли проходить проверки USER-зоны. Но — и это ключевой момент лекции — это не происходит автоматически.

3. ADMIN не обязан быть USER

Когда начинаешь работать с ролями, очень хочется мысленно построить пирамиду: USER < EDITOR < ADMIN, значит ADMIN может всё, что может USER. Логика человеческая, но Spring Security не обязан угадывать ваши намерения. Он честно сравнивает условия доступа с тем, что есть у пользователя. Если правило написано как .hasRole("USER"), то проверяется роль USER. И всё. Без телепатии.

В этом даже есть плюс: система получается предсказуемой. Минус — новичок часто ловит «почему админ не может зайти в /api/me?!», а потом начинает «чинить» это самым опасным способом: расширять правила доступа «на всякий случай», открывая лишнее.

Давайте прямо зафиксируем реальный сценарий. Представим, что мы написали правило:

// Доступ к личной зоне: требуется роль USER
.requestMatchers("/api/me/**").hasRole("USER")

А потом создали админского пользователя вот так:

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

// Создаём пользователя, который имеет ТОЛЬКО роль ADMIN (без USER)
UserDetails root = User.builder()
    .username("root")
    .password("{bcrypt}...")
    .roles("ADMIN")
    .build();

Такой пользователь не пройдёт проверку hasRole("USER"), потому что у него нет роли USER. И это не «особенность учебного примера», а базовая инженерная реальность: Spring Security не вводит иерархию ролей автоматически.

Да, в Spring Security существует механизм RoleHierarchy, который позволяет описать «ADMIN > EDITOR > USER». Но на fundamentals-этапе это часто делает хуже, потому что скрывает важную мысль: доступ должен быть явным. Сейчас мы хотим не «починить неудобство», а научиться видеть, какие правила действительно проверяются.

Поэтому наш текущий принцип простой: если аккаунт должен проходить несколько role-check правил, мы явно назначаем ему несколько ролей.

4. Роли для in-memory пользователей

Поскольку пользователи у нас сейчас живут в памяти, мы можем сделать очень удобную для обучения вещь: создать несколько сценарных аккаунтов, каждый из которых существует «по причине». Один нужен, чтобы проверять личную зону, второй — редакторскую, третий — админскую. Если вместо этого сделать одного «суперпользователя», проверка матрицы доступа превращается в гадание: «а это правда работает или я просто всегда хожу под root?».

Давайте заведём три пользователя:

  • alice — обычный пользователь с ролью USER;
  • eva — редактор с ролями USER и EDITOR;
  • root — администратор с ролями USER, EDITOR, ADMIN (да, это сильный админ, но для обучения удобно).

Пока смотрим на это именно как на role-модель. Формат хранения конкретных authority-строк здесь не главный вопрос; сейчас важно зафиксировать смысл ролей и убрать иллюзию, что иерархия появится сама.

Сначала покажу идею на уровне отдельных пользователей (маленькими кусками, чтобы глаз не утонул):

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;

UserDetails alice(PasswordEncoder encoder) {
    return User.builder()
        .username("alice")
        // Пароль всегда кодируем через PasswordEncoder, а не храним как plain text
        .password(encoder.encode("alice-pass"))
        // roles(...) принимает "человеческие" имена ролей без префикса ROLE_
        .roles("USER")
        .build();
}

Здесь важны две вещи. Во‑первых, пароль мы кодируем через PasswordEncoder, который уже настроен в приложении. Во‑вторых, роль задаём без префикса ROLE_ — просто "USER". В roles(...) мы пишем человеческие имена ролей, а сам префикс появится уже тогда, когда Spring Security превратит роль в реальную authority-строку.

Теперь редактор:

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;

UserDetails eva(PasswordEncoder encoder) {
    return User.builder()
        .username("eva")
        .password(encoder.encode("eva-pass"))
        // Явно выдаём обе роли: EDITOR не "включает" USER автоматически
        .roles("USER", "EDITOR")
        .build();
}

Обратите внимание: мы не надеемся, что EDITOR «включает» USER. Мы говорим об этом явно.

И админ:

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;

UserDetails root(PasswordEncoder encoder) {
    return User.builder()
        .username("root")
        .password(encoder.encode("root-pass"))
        // Для учебного проекта удобно, что root может проходить проверки всех зон
        .roles("USER", "EDITOR", "ADMIN")
        .build();
}

Снова то же правило: если мы хотим, чтобы админ мог ходить в личную зону, мы явно выдаём ему роль USER. Если хотим, чтобы он мог ходить в редакторскую зону (а в учебном проекте это удобно для проверки), даём EDITOR.

Теперь соберём пользователей в InMemoryUserDetailsManager. Я покажу вариант максимально близкий к реальному коду конфигурации, но без лишнего «шумового» окружения:

import org.springframework.context.annotation.Bean;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Bean
UserDetailsService userDetailsService(PasswordEncoder encoder) {
    // InMemoryUserDetailsManager хранит пользователей в памяти приложения (без БД)
    return new InMemoryUserDetailsManager(
        // USER: доступ только к user-зоне
        User.withUsername("alice").password(encoder.encode("alice-pass")).roles("USER").build(),
        // USER + EDITOR: доступ к user-зоне и editor-зоне
        User.withUsername("eva").password(encoder.encode("eva-pass")).roles("USER", "EDITOR").build(),
        // USER + EDITOR + ADMIN: доступ ко всем зонам
        User.withUsername("root").password(encoder.encode("root-pass")).roles("USER", "EDITOR", "ADMIN").build()
    );
}

Да, здесь строки плотные, но зато видна главная идея: каждый аккаунт явно получает набор ролей, и этот набор совпадает с тем, какие зоны мы хотим им открыть. В учебном проекте это почти идеальная повторяемость: каждый запуск приложения даёт одинаковых пользователей и одинаковое поведение.

Здесь мы фиксируем именно смысл ролей. Если потом посмотреть на те же аккаунты уже как на точные authority-строки, логика не изменится: alice остаётся обычным пользователем, eva — редактором, root — администратором. Так карта зон перестаёт плавать, даже когда запись прав становится более явной.

5. Роли и зоны в SecurityFilterChain

Теперь мы соединяем роли с тем, ради чего они вообще существуют — с правилами доступа к URL-ам. И здесь важный принцип: SecurityFilterChain должен читаться как «карта местности». Когда вы открываете файл безопасности через месяц, вы должны видеть, где public, где user, где editor, где admin. Если вместо этого там набор случайных matcher-ов, то это уже не security-конфигурация, а археологический памятник эпохи «копировал из туториала».

В нашем проекте роль-модель идеально ложится на зоны доступа:

  • /api/public/** — доступен всем;
  • /api/me/** и наши личные штуки — доступны USER;
  • /api/editor/** — доступны EDITOR;
  • /api/admin/** — доступны ADMIN;
  • всё остальное — закрыто (denyAll()), чтобы не открыть случайно что-то «забытое».

Пример конфигурации (коротко, но уже осмысленно):

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/public/**").permitAll()
            // Личная зона: требуется роль USER
            .requestMatchers("/api/me/**").hasRole("USER")
            // Редакторская зона: требуется роль EDITOR
            .requestMatchers("/api/editor/**").hasRole("EDITOR")
            // Админская зона: требуется роль ADMIN
            .requestMatchers("/api/admin/**").hasRole("ADMIN")
            // Всё остальное закрываем, чтобы случайно не оставить дырку
            .anyRequest().denyAll()
    ).build();
}

Сейчас здесь главное — не «идеальная финальная настройка на все случаи жизни», а читаемая базовая карта. Мы держим конфигурацию короткой. Мы не расписываем «по одному эндпоинту» (это быстро превратит конфиг в простыню). Мы используем роли по назначению: как доступ к зоне.

Если общий matcher стоит выше частных зон, он перехватывает запрос раньше, чем вы дойдёте до нужного правила. Поэтому здесь работает та же логика: более конкретные правила — выше, общий catch-all denyAll() — в конце. И именно в таком виде роль-модель начинает «ощущаться» не как теория, а как работающая система.

6. Константы для ролей вместо строк

Пока мы пишем роли строками "USER", "EDITOR", "ADMIN", всё кажется простым. Но у строк есть неприятная особенность: опечатка компилируется отлично. Написали "EDTIOR" — и привет, полчаса дебага, почему у вас «вроде есть роль», но доступ не работает. Поэтому хороший фундаментальный навык — централизовать имена ролей.

В учебном проекте это можно сделать очень просто: завести класс с константами. Не потому что «так надо по стандарту», а потому что вы хотите, чтобы IDE помогала вам автодополнением, а компилятор хотя бы косвенно ловил ошибки (вы не сможете сослаться на несуществующую константу).

Например:

package com.example.securecontent.security;

// Единый словарь ролей для всего проекта (чтобы не ловить опечатки в строках)
public final class AppRoles {
    public static final String USER = "USER";
    public static final String EDITOR = "EDITOR";
    public static final String ADMIN = "ADMIN";

    private AppRoles() {
    }
}

Теперь конфигурация становится менее «строчной магией» и более «кодом»:

import com.example.securecontent.security.AppRoles;

// ...
// IDE подсказывает значения, а вы меньше рискуете ошибиться в имени роли
.requestMatchers("/api/me/**").hasRole(AppRoles.USER)
.requestMatchers("/api/editor/**").hasRole(AppRoles.EDITOR)
.requestMatchers("/api/admin/**").hasRole(AppRoles.ADMIN)

И аналогично при создании пользователей:

import com.example.securecontent.security.AppRoles;
import org.springframework.security.core.userdetails.User;

// Явно перечисляем роли, которые должен иметь пользователь
User.withUsername("eva")
    .password(encoder.encode("eva-pass"))
    .roles(AppRoles.USER, AppRoles.EDITOR)
    .build();

Здесь мы делаем важную вещь: фиксируем словарь проекта. И уже меньше шанс, что в одном месте роль называется ADMIN, в другом ROLE_ADMIN, а в третьем (страшно сказать) SuperAdminBecauseWhyNot.

7. Границы роль-модели

В этот момент часто возникает соблазн: «О, роли удобные, давайте роль на каждое действие! Роль DRAFT_DELETE, роль DRAFT_CREATE, роль PROFILE_WRITE…». И вот вы внезапно построили свою мини-ACL-систему на ролях, и она будет неудобной почти всем: и разработчикам, и администраторам, и будущим вам.

Нормальная роль-модель держится на простом принципе: роль — это блок доступа, а не атомарное действие. Если вам нужно выразить атомарное действие, вы скорее всего захотите другой инструмент (authorities). Но сейчас достаточно удержать мысль: роль не должна размножаться как кролики, если вы не хотите однажды проводить «инвентаризацию зоопарка».

Для Secure Content Platform API мы намеренно держим роли минимальными. USER покрывает всё, что связано с личной зоной. EDITOR покрывает модерацию. ADMIN покрывает управление пользователями. И этого более чем достаточно, чтобы уже сейчас построить работающую матрицу доступа и проверить, что зоны действительно отделены.

Если через пару дней вы поймаете себя на мысли «мне нужно разрешить редактору публиковать, но запретить отклонять», это будет сигнал не «добавить роль PUBLISHER», а подумать про точечные права. Но сегодня мы фиксируем фундамент: три роли, три зоны, без скрытой иерархии.

8. Типичные ошибки при ролях USER/EDITOR/ADMIN

Ошибка №1: ожидать, что ADMIN автоматически проходит проверки USER.
Это самая частая путаница. Если у вас правило .hasRole("USER"), то пользователь должен иметь роль USER. Никакой «старшинство» ролей само по себе не учитывается. Решение простое: либо выдавайте администратору несколько ролей явно (как мы и делаем в учебном проекте), либо подключайте иерархию ролей осознанно, когда уже уверенно понимаете механику.

Ошибка №2: использовать одну роль на каждый endpoint и превратить роли в «псевдо-permissions».
На старте кажется, что так будет точнее. Но на практике это разрушает читаемость и переносит всю сложность permissions-модели в роли, которые для этого не предназначены. Гораздо здоровее держать роли крупными («зонами»), а точность добавлять другими инструментами, когда они действительно нужны.

Ошибка №3: непоследовательные имена ролей.
Сегодня вы пишете EDITOR, завтра CONTENT_EDITOR, послезавтра ROLE_EDITOR, а потом удивляетесь, что вам нужен психолог, а не дебаггер. Роли должны быть короткими, стабильными и одинаковыми везде: в пользователях, в SecurityFilterChain, в обсуждении команды и в документации.

Ошибка №4: хранить имена ролей как строки в десяти местах.
Опечатка в строке — это «баг без красной лампочки»: проект собирается, приложение стартует, но доступ ломается. Константы (или enum) здесь дают огромный выигрыш в стабильности. Особенно в курсе, где мы хотим, чтобы через месяц вы читали код и не переводили его в голове каждый раз.

Ошибка №5: делать всех пользователей «суперпользователями», чтобы “не мешало тестировать”.
Так вы убиваете сам смысл матрицы доступа: вы больше не видите, где зона USER, где EDITOR, где ADMIN, потому что всё всегда работает. Учебный проект ценен именно тем, что у вас есть сценарные аккаунты, и вы быстро проверяете границы ролей на практике. Если всё время ходить под root, то безопасность превращается в декорацию.

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