JavaRush /Курсы /Spring Security /Моделируем пользователя через

Моделируем пользователя через user()

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

1. user() в MockMvc

Когда мы пишем security-тесты, у нас почти всегда есть желание проверить правило вида «в личную зону можно только после аутентификации» или «в admin-зону можно только администратору». Звучит просто… ровно до момента, пока вы не понимаете, что тесту нужно как-то «стать» пользователем. Причём быстро, без реального логина и без танцев с паролями.

В реальном приложении пользователь проходит полноценный auth flow: фильтры, AuthenticationManager, провайдеры, проверка пароля, заполнение SecurityContext. Но в тестах на authorization мы часто хотим проверить другое: «Если пользователь уже аутентифицирован и у него роль EDITOR, то запрос в editor-зону проходит». В этом месте user() — как чит-код разработчика, но честный: он не ломает security, он позволяет быстро смоделировать входные данные для authorization-решения.

На проекте Secure Content Platform API это особенно полезно, потому что наша access matrix довольно «живая»: есть public endpoints (/api/public/**), личная зона (/api/me/**), editor endpoints (/api/editor/**) и admin endpoints (/api/admin/**). И тестировать это хочется так же системно, как и проектировали.

RequestPostProcessor и user()

Прежде чем писать код, полезно понять, где user() вообще «вставляется» в картину мира. В MockMvc запрос создаётся билдерами (get(...), post(...) и т.д.), а потом проходит через целую цепочку инфраструктуры, очень похожую на реальную обработку запроса в Spring MVC. И вот прямо между созданием запроса и запуском его через фильтры у нас есть точка, где можно этот запрос немного «докрутить».

Эта точка и называется RequestPostProcessor. Это объект, который получает MockHttpServletRequest, может добавить туда заголовки, атрибуты, и — главное для нас — может подготовить security-контекст так, будто запрос пришёл от конкретного пользователя. Ровно это и делает user() из spring-security-test.

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

flowchart TD
    A["MockMvc: build request"] --> B["RequestPostProcessor .with(user(...))"]
    B --> C["SecurityFilterChain (реальная!)"]
    C --> D["Controller / Handler"]
    D --> E["Assertions (status, headers, body)"]

Ключевая мысль: user() не «обходит» SecurityFilterChain. Наоборот, он помогает подготовить запрос так, чтобы цепочка безопасности увидела его как запрос от аутентифицированного пользователя и приняла нормальное решение об авторизации.

2. Базовый сценарий: /api/me с user()

Начнём с самого прикладного кейса: endpoint /api/me по нашей модели доступа является личной зоной, а значит anonymous туда ходить не должен. В первых тестах мы уже проверяли 401 для anonymous. Теперь нам нужно проверить позитивный сценарий: обычный пользователь (USER) может открыть /api/me и получить 200 OK.

Предположим, что в тестовом классе у вас уже есть внедрённый MockMvc mockMvc (мы сделали это в прошлой лекции). Тогда самый «в лоб» вариант с user() выглядит так:

import org.junit.jupiter.api.Test;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@Test
void meIsOkForAuthenticatedUser() throws Exception {
    mockMvc.perform(
            // Собираем GET-запрос к личной зоне
            get("/api/me")
                    // Подставляем в запрос аутентифицированного пользователя
                    .with(user("anna").roles("USER"))
    )
    // Проверяем, что правило доступа пропустило запрос
    .andExpect(status().isOk()); // Ожидаем HTTP 200
}

Здесь полезно остановиться и проговорить, что именно происходит. Метод user("anna") создаёт тестового пользователя с именем anna, а roles("USER") добавляет ему роль пользователя. Дальше .with(...) применяет этот post-processor к одному конкретному запросу: другие запросы в этом же тесте или в соседних тестах от этого не «заражаются».

В результате security-цепочка видит: «да, пользователь аутентифицирован», затем применяет ваши правила доступа (в SecurityFilterChain и, если у вас включена method security, ещё и на уровне сервисов), и endpoint возвращает 200.

Если вы любите, чтобы тест читался как правило, то название meIsOkForAuthenticatedUser — уже половина успеха. Примерно так и должен звучать regression pack: не «test1», а «условие → ожидаемый результат».

3. Роли и префикс ROLE_ в тестах

Когда вы впервые встречаете Spring Security, кажется, что роль — это просто строка. Потом вы узнаёте, что роль внутри Spring часто превращается в authority с префиксом ROLE_. А потом в тестах вы начинаете гадать: писать ADMIN или ROLE_ADMIN? И вот здесь user().roles(...) пытается спасти вашу психику, но при одном условии: вы не будете ему мешать.

У roles(...) есть важное правило: роли указываются без префикса ROLE_. То есть правильно так: roles("ADMIN"), roles("EDITOR"), roles("USER"). Spring Security Test сам добавит префикс туда, где он нужен, и в SecurityContext окажется authority ROLE_ADMIN (или ROLE_EDITOR, ROLE_USER).

Если же написать roles("ROLE_ADMIN"), вы, скорее всего, получите authority ROLE_ROLE_ADMIN. Да, это не шутка, это прямой результат «префикс уже был, а мы ещё раз добавили». И потом вы сидите и думаете, почему hasRole("ADMIN") не работает. Спойлер: потому что у вас не ROLE_ADMIN, а ROLE_ROLE_ADMIN.

Чтобы закрепить логику, удобно держать в голове маленькую табличку:

Как правило задано в security Что оно ожидает в Authentication Как удобнее задать в тесте через user()
hasRole("ADMIN") authority ROLE_ADMIN .roles("ADMIN")
hasRole("EDITOR") authority ROLE_EDITOR .roles("EDITOR")
hasAuthority("user:read") authority user:read .authorities(new SimpleGrantedAuthority("user:read"))

Теперь применим это к проекту. Например, admin endpoint /api/admin/users должен быть доступен администратору, но запрещён обычному пользователю. Тест с user() сразу подчёркивает разницу между 401 и 403: пользователь есть, но прав не хватает.

import org.junit.jupiter.api.Test;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@Test
void adminUsersForbiddenForRegularUser() throws Exception {
    mockMvc.perform(
            // Пытаемся зайти в admin-зону под обычным пользователем
            get("/api/admin/users").with(user("anna").roles("USER"))
    )
    // Пользователь аутентифицирован, но прав не хватает → 403
    .andExpect(status().isForbidden()); // Ожидаем HTTP 403
}

Этот тест на самом деле делает очень важное дело: он фиксирует, что admin-зона закрыта не «на удачу», а строго. И если кто-то завтра переставит matcher’ы в SecurityFilterChain в неудачном порядке, тест довольно быстро вернёт вас в реальность.

4. Authorities в user()

Роли — удобные, человеческие и довольно грубые. В нашем проекте это USER, EDITOR, ADMIN. Но в реальном приложении (и в нашем учебном проекте ближе к финалу) правила доступа часто выражаются и через более точные permissions, то есть authorities вроде draft:publish, draft:review, user:manage. И тут возникает вопрос: как это моделировать в тесте?

Ответ простой: через authorities(...). Важно лишь помнить, что authority — это строка «как есть», без магии префиксов. Если security-правило ожидает draft:review, то в тесте нужно дать именно draft:review.

Ниже пример того, как это выглядит, если ваш endpoint (или метод сервиса) защищён именно authority:

import org.junit.jupiter.api.Test;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@Test
void reviewQueueOkForDraftReviewAuthority() throws Exception {
    mockMvc.perform(
            // Идём в editor-endpoint, выдавая пользователю точечное permission
            get("/api/editor/review-queue")
                    .with(user("ed")
                            // Authority задаётся «как есть», без ROLE_-магии
                            .authorities(new SimpleGrantedAuthority("draft:review")))
    )
    // Если конфигурация действительно завязана на authority, ожидаем 200
    .andExpect(status().isOk()); // Ожидаем HTTP 200
}

Здесь есть тонкость, о которую часто спотыкаются новички. Если ваша реальная конфигурация защищает /api/editor/** через hasRole("EDITOR"), то такой тест может вернуть 403, потому что у пользователя нет ROLE_EDITOR. И это не «плохой тест», это сигнал: вы моделируете не те права, которые реально требуются.

Иногда вам нужны и роль, и permissions. В таком случае лучше не пытаться «склеить» .roles(...) и .authorities(...) в одну цепочку вызовов, потому что вы легко перезатрёте одно другим. Более надёжный подход — явно перечислить всё в authorities(...), включая роль как authority с префиксом:

import org.junit.jupiter.api.Test;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@Test
void editorRoleAndPermissionWorkTogether() throws Exception {
    mockMvc.perform(
            get("/api/editor/review-queue")
                    .with(user("ed")
                            // Явно задаём и роль (как authority), и permission
                            .authorities(
                                    new SimpleGrantedAuthority("ROLE_EDITOR"),
                                    new SimpleGrantedAuthority("draft:review")))
    )
    .andExpect(status().isOk()); // Ожидаем HTTP 200
}

Да, тут мы руками написали ROLE_EDITOR, и это как раз тот случай, когда это уместно: мы работаем с authorities «как есть» и сами контролируем итоговый набор прав.

5. Хелперы для тестовых пользователей

Первые пару тестов с .with(user("anna").roles("USER")) обычно выглядят мило. На десятом тесте вы начинаете подозревать, что Анна вот-вот получит роль тимлида, потому что она везде. На двадцатом — вы понимаете, что копипаста пользователей превращает тесты в шум: они перестают быть «правилами доступа», а становятся «вёрсткой запроса».

Поэтому хороший стиль — вынести повторяющихся тестовых пользователей в маленькие helper-методы, которые возвращают RequestPostProcessor. Это сохраняет и явность, и читабельность, и при этом не заставляет вас писать 300 одинаковых строк.

Например, так:

import org.springframework.test.web.servlet.request.RequestPostProcessor;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.user;

private RequestPostProcessor regularUser() {
    // Хелпер возвращает post-processor, который можно переиспользовать в тестах
    return user("anna").roles("USER");
}

И тогда тест становится приятнее глазу:

import org.junit.jupiter.api.Test;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@Test
void meIsOkForRegularUser() throws Exception {
    mockMvc.perform(
            // В тесте остаётся только суть правила доступа
            get("/api/me").with(regularUser())
    )
    .andExpect(status().isOk()); // Ожидаем HTTP 200
}

Здесь важно не перегнуть палку. Наша цель — сделать тесты читаемыми, а не построить мини-фреймворк поверх MockMvc. Обычно 2–4 helper-метода (user, editor, admin) уже достаточно, чтобы regression pack перестал выглядеть как «скопируй и вставь».

6. Границы применения user()

Это самый важный момент лекции, потому что именно здесь рождаются честные ожидания от тестов. user() — это инструмент для моделирования уже аутентифицированного пользователя. Он не проверяет пароль, не ходит в БД за UserAccount, не запускает AuthenticationManager, не прогоняет DaoAuthenticationProvider. Если вы ждёте от него проверки логина — вы ждёте от микроволновки, что она научится варить борщ.

Зато user() отлично тестирует то, что нам нужно в базовом regression pack: правила авторизации и границы доступа. То есть: какие endpoints доступны anonymous, какие требуют аутентификацию, какие требуют ROLE_EDITOR или ROLE_ADMIN, какие проверяют authority. Это быстрые, дешёвые тесты, которые ловят регрессии в конфигурации или в правилах доступа.

Полезная ментальная модель такая: user() — это как если бы в тесте вы «вручили охраннику пропуск» и сказали: «Считай, что этот человек уже внутри системы, проверь только, пускаешь ли ты его в эту дверь». А проверка логина — это другая история: там вы проверяете, как человек вообще получил этот пропуск, кто его выдал, и не поддельный ли он.

В контексте Spring Security это различие соответствует двум вопросам: authentication и authorization. user() в первую очередь помогает тестировать authorization-слой: доступы, роли, permissions, 401/403-поведение. И это абсолютно нормально, потому что без такого слоя вы будете каждый раз “руками” перепроверять доступ через Postman и надеяться, что всё ещё работает.

7. Типичные ошибки при использовании user()

Ошибка №1: ожидание, что user() тестирует логин и пароль.
Очень легко начать думать, что раз мы «создали пользователя», то мы проверили аутентификацию. На самом деле user() лишь создаёт контекст, в котором запрос выглядит аутентифицированным. Это хорошо для проверки 403/200, но никак не проверяет ваш реальный UserDetailsService, PasswordEncoder и account states.

Ошибка №2: путаница с ROLE_ и запись roles("ROLE_ADMIN").
Это классика, достойная музейной витрины рядом с «почему у меня NullPointerException». В roles(...) вы пишете роль без префикса, иначе рискуете получить ROLE_ROLE_ADMIN и потом очень долго убеждать себя, что «Spring Security сломан». Он не сломан, он просто слишком честно выполняет вашу команду.

Ошибка №3: моделирование не тех прав, которые реально требуются конфигурацией.
Если endpoint защищён через hasRole("EDITOR"), а вы добавили только authority draft:review, вы получите 403. И это правильно. В таких случаях нужно или выдавать правильную роль через .roles("EDITOR"), или добавить ROLE_EDITOR в список authorities, если вы сознательно тестируете authority-based правила.

Ошибка №4: копипаста пользователей в каждом тесте до состояния “Анна как глобальная переменная”.
Когда .with(user("anna").roles("USER")) размазан по 30 тестам, они становятся шумными и плохо читаются. Вынесение в helper-методы делает regression pack гораздо более устойчивым: вы меняете имя пользователя или роль в одном месте и не ловите «снежную лавину» правок.

Ошибка №5: попытка проверить слишком много в одном тесте.
Иногда хочется одним тестом пройтись по public, me, editor и admin зонам, чтобы «быстрее». В итоге при падении такого теста непонятно, что именно сломалось. Гораздо надёжнее держать один тест на одно правило доступа: так regression pack будет точнее и проще в диагностике.

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