JavaRush /Курсы /Spring Security /Baseline до/после Spring Security

Baseline до/после Spring Security

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

1. Понятие baseline проекта

Если вы хоть раз пытались «починить security», не понимая, что именно сломалось, то уже знаете цену одного простого навыка: уметь фиксировать baseline. Baseline — это снимок поведения приложения в конкретном состоянии: какие endpoint’ы доступны, какие статусы возвращаются, что видит клиент и какие первые выводы отсюда следуют. Это как фотография до и после ремонта: спорить потом можно долго, но фото не врёт.

К этому моменту у нас уже есть все куски мозаики: мы увидели, что spring-boot-starter-security меняет доступ без правок контроллеров, что платформа закрывает анонимный вход по умолчанию, что baseline остаётся проверяемым через временного пользователя и что браузер с API-клиентом смотрят на один и тот же protected endpoint по-разному. Теперь полезно собрать всё это в один снимок «до/после» проекта. Это и будет наш baseline на этом шаге.

Важно и другое: baseline не равен вашей бизнес-модели доступа. У проекта уже есть карта того, как должно быть в продукте: публичные статьи доступны всем, /api/me — только вошедшему, /api/admin/** — только администратору. А baseline сегодняшнего дня — это то, как приложение фактически ведёт себя сразу после подключения spring-boot-starter-security, пока мы не написали ни одного правила доступа.

Чтобы не превратить эту лекцию в гадание на кофейной гуще, всё время будем держать в голове две карты: (1) access matrix проекта как «желание бизнеса» и (2) baseline-поведение Spring Security как «политику платформы по умолчанию». Сегодня они будут конфликтовать — и это нормально. Более того, в этом конфликте и есть учебная ценность.

2. Реперные точки для сравнения

Когда мы говорим «проверим проект до/после», очень легко скатиться в хаос: дёргать двадцать URL, запутаться, а потом закончить фразой «ну короче всё закрылось». Так тоже можно, но это, скажем честно, не инженерный стиль, а стиль «я нажал кнопки и надеюсь на лучшее». Поэтому выберем несколько реперных зон, которые хорошо отражают смысл проекта.

Ниже — упрощённая карта зон проекта Secure Content Platform API. Не всей функциональности, а именно тех мест, по которым удобно сравнивать baseline. Мы специально берём зоны с разным бизнес-смыслом, чтобы увидеть: starter по умолчанию не пытается угадывать, «опасный» это endpoint или «безобидный».

Зона (по смыслу проекта) Примеры endpoint’ов Ожидаемая идея доступа (из access matrix)
Public GET /api/public/articles,
GET /api/public/articles/{slug}
Должно быть доступно анонимно
Authenticated (личная зона) GET /api/me Только аутентифицированному
Owner-only (черновики) GET /api/drafts Только аутентифицированному, и ещё важна логика «свой/чужой» (пока её нет)
Editor-only GET /api/editor/review-queue Должно быть доступно «привилегированным» пользователям (пока нет ролей)
Admin-only GET /api/admin/users Только администратору (пока нет ролей)

Сегодня мы не будем спорить, какие именно статусы правильны для каждой зоны в финальном API, и не будем проектировать правила. Мы просто зафиксируем: что происходило до security и что стало происходить после подключения starter’а — на этих реперных точках.

3. Insecure baseline без Spring Security

До того как мы подключили spring-boot-starter-security, проект ведёт себя так, как ведёт себя большинство учебных и «пока ещё внутренних» API: всё, что вы написали в контроллерах, доступно всем, кто может достучаться до порта. Как временное состояние это не «плохо», но очень плохо, если забыть, что оно временное. Именно здесь и рождается легендарный жанр багов «случайно открыли админку наружу».

Посмотрим на простые контроллеры-заглушки, на которых удобно иллюстрировать baseline. Они нарочно возвращают строки, чтобы мы не отвлекались на DTO, базы данных и другую красоту.

package com.example.securecontent.publicapi;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/public/articles") // Публичная зона по бизнес-смыслу (до Security она реально публичная)
class ArticleController {

    @GetMapping // GET /api/public/articles
    String list() {
        // Заглушка: возвращаем строку, чтобы сфокусироваться на baseline, а не на DTO/БД
        return "articles";
    }
}
package com.example.securecontent.profile;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
class MeController {

    @GetMapping("/api/me") // Личная зона по бизнес-смыслу, но без Security технически она тоже будет публичной
    String me() {
        // Заглушка: в реальном проекте здесь было бы "кто я" из SecurityContext/сессии/токена
        return "current user";
    }
}

Если Security starter не подключён, оба этих endpoint’а будут отдавать 200 OK любому клиенту. Пример на curl до security:

# До подключения Security: ожидаем 200 OK без каких-либо credentials
curl -i http://localhost:8080/api/public/articles
# HTTP/1.1 200
# ...
# articles
# До подключения Security: даже "личный" endpoint будет отвечать 200 OK всем подряд
curl -i http://localhost:8080/api/me
# HTTP/1.1 200
# ...
# current user

И вот здесь важно поймать мысль: endpoint /api/me по смыслу должен быть приватным, но технически он абсолютно публичный. Приложение не может «догадаться», что речь идёт о личных данных, потому что для него это просто строка URL. Именно поэтому безопасники не верят в «ну оно же очевидно».

4. Secure baseline после подключения starter

Теперь делаем то, что к этому моменту курса уже должно вызывать у вас уважение и лёгкую тревогу: ничего не меняем в контроллерах, но меняем dependency graph. В Gradle это часто выглядит как одна строка.

dependencies {
    // Подключаем автоконфигурацию Security: после этого baseline приложения резко меняется
    implementation("org.springframework.boot:spring-boot-starter-security")
}

После этого начинается не магия, а политика платформы: Spring Boot поднимает приложение уже с security auto-configuration, и появляется поведение «по умолчанию». Для нас ключевой факт сегодняшней лекции звучит так: по умолчанию защищённым становится практически всё.

Что это означает на практике для наших реперных точек:

- GET /api/public/articles перестаёт быть публичным, хотя в URL написано public.
- GET /api/me тоже закрывается — и это уже выглядит более ожидаемо.
- GET /api/admin/users закрывается тоже — но не потому, что система «поняла смысл», а потому, что сработало общее правило.
- Разница между зонами public/me/admin сейчас не в доступности, а только в нашей голове и в access matrix. Платформа пока просто «выставила охранника» на вход во всё здание, но ещё не дала ему список, кого пускать в какую комнату.

Если попросить curl показать «сырые» заголовки, вы увидите, что сервер начинает отвечать по-другому. Причём это может быть либо 302 — redirect на login page, — либо 401 Unauthorized. И здесь важно: это не случайность, а результат того, что разные клиенты и разные заголовки запроса подталкивают платформу к разной форме ответа.

Пример «похоже на браузер»: попросили HTML — сервер, скорее всего, предложит login page через redirect:

# "Как браузер": просим HTML, Spring Security может ответить редиректом на /login
curl -i -H "Accept: text/html" http://localhost:8080/api/public/articles
# HTTP/1.1 302
# Location: http://localhost:8080/login

Пример «похоже на API-клиента»: попросили JSON — чаще увидите 401:

# "Как API-клиент": просим JSON, чаще получаем 401 вместо редиректа
curl -i -H "Accept: application/json" http://localhost:8080/api/public/articles
# HTTP/1.1 401

Внутренний маршрут запроса здесь пока не нужен. Нам важнее сначала научиться различать сами реакции. Это как учиться водить: сначала замечаете, что машина поворачивает, а уже потом лезете в устройство рулевой рейки.

5. Разница браузера и curl

Чтобы правильно читать сводную таблицу ниже, держите в голове один факт. Колонка «как браузер» показывает браузерный вход: защищённый ресурс чаще приводит к 302 и переходу на /login. Колонка «как API» показывает тот же baseline глазами клиента, которому важны статус и заголовки, поэтому там чаще виден 401.

Это не две разные модели доступа и не два разных endpoint’а. Это одна и та же защита, просто в разной форме общения с неаутентифицированным запросом. Поэтому для baseline полезно использовать минимум два инструмента: браузер — чтобы увидеть login flow, и curl/Postman — чтобы честно смотреть на статус, Location и WWW-Authenticate.

6. Before/after: таблица и проверка доступа

Сейчас соберём то, ради чего мы вообще затеяли эту лекцию: один понятный снимок «до/после» проекта Secure Content Platform API. Возьмём те же зоны, что и в access matrix, и посмотрим, что было «до» — без starter’а — и что стало «после» — со starter’ом. Это и будет наш baseline, который дальше поможет не путаться, когда мы начнём писать собственные правила доступа.

Сводная таблица — упрощённо, но достаточно, чтобы не потеряться:

Endpoint Зона (по смыслу проекта) До starter-security После starter, “как браузер” (Accept: text/html) После starter, “как API” (Accept: application/json)
GET /api/public/articles public 200 обычно
302
на /login
обычно
401
GET /api/public/articles/{slug} public 200 обычно
302
обычно
401
GET /api/me authenticated 200 обычно
302
обычно
401
GET /api/drafts owner-only 200 обычно
302
обычно
401
GET /api/editor/review-queue editor-only 200 обычно
302
обычно
401
GET /api/admin/users admin-only 200 обычно
302
обычно
401

Теперь проговорим, почему это важно, обычным человеческим языком. До security вы могли различать зоны только логически: «вот это public, а вот это admin». После подключения starter’а платформа перестаёт доверять вашему «логическому смыслу URL» и говорит: «Пока я не знаю ваших правил, лучше закрою всё». Это и есть secure-by-default в действии.

На этом этапе студент часто спрашивает: «Так зачем тогда мы вообще делали /api/public/**, если оно всё равно закрыто?» Ответ спокойный: /api/public/** — это часть дизайна API, языка вашей access matrix и будущих правил. Просто сами правила мы ещё не написали. Название пути — это табличка на двери, а замок — security-слой. Табличка сама по себе дверь не открывает.

Чтобы закрепить, возьмём пример admin-зоны.

package com.example.securecontent.admin;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/admin/users") // Админская зона по смыслу: без Security она случайно станет "публичной админкой"
class AdminUsersController {

    @GetMapping // GET /api/admin/users
    String users() {
        // Заглушка: в реальном мире тут были бы данные пользователей, а значит цена ошибки выше
        return "users";
    }
}

До starter:

# До подключения Security: даже админский endpoint отвечает 200 OK любому клиенту
curl -i http://localhost:8080/api/admin/users
# HTTP/1.1 200
# users

После starter (API-ожидание):

# После подключения Security: без аутентификации получаем 401 (если запрос выглядит как API)
curl -i -H "Accept: application/json" http://localhost:8080/api/admin/users
# HTTP/1.1 401

И вот это — отличный холодный душ, который делает backend-разработчика взрослее. Мы внезапно видим, что «админка» не защищается названием пакета admin и не защищается словом admin в URL. Она защищается правилами и механизмами. Сегодня правил ещё нет, но сам механизм уже появился.

Default user в действии

Справедливый вопрос здесь такой: «Окей, всё закрыто. Значит, контроллеры мёртвые?» Нет. Чтобы это проверить, достаточно одного входа временным пользователем Spring Boot. В логах старта обычно есть generated password, а username по умолчанию — user. Этого хватает, чтобы один раз пройти baseline-проверку и увидеть: после успешной аутентификации тот же endpoint начинает отвечать.

# Демонстрация: Basic-аутентификация с временным пользователем Spring Boot
curl -i -u user:6c5f7b8b-1d2e-4f40-9d63-1c6d0f8a1234 \
  -H "Accept: application/json" \
  http://localhost:8080/api/me
# HTTP/1.1 200
# current user

Этого напоминания достаточно: default user здесь нужен только как проверочный пропуск, а не как будущая модель пользователей проекта.

7. Типичные ошибки при сравнении baseline “до/после”

Ошибка №1: «Я проверил только браузером, значит, понял поведение API».
Браузер, как хороший актёр, делает всё драматичнее: редиректы, страницы логина, HTML. Но для API-мира вам хотя бы иногда нужно смотреть на «сырой» ответ, иначе вы будете путать «мне нужен login» с «контроллер не вызывается». Спасает простая привычка: хотя бы один раз повторять проверку через curl -i.

Ошибка №2: «/api/public/** должен быть публичным, значит, Spring Security работает неправильно».
Это очень естественная мысль, потому что мы привыкли доверять именам. Но URL — это табличка, а не правило доступа. Secure-by-default намеренно не пытается угадывать ваш бизнес-смысл. Если сейчас /api/public/articles закрыт, это не баг, а ожидаемое поведение baseline. Ваша задача позже — осмысленно открыть нужные зоны, а не ругаться на замок за то, что он закрыт.

Ошибка №3: «401 означает, что endpoint не существует».
Новичок иногда видит 401 и делает вывод «маршрут не найден». На самом деле «не найден» — это чаще 404. А 401 — это «я вижу маршрут, но не пущу тебя дальше без подтверждения личности». Привыкайте отличать ошибки маршрутизации от ошибок доступа: это разные миры и разные причины.

Ошибка №4: «Postman показывает HTML/login page, значит, Postman сломан».
Чаще всего Postman здесь не сломан. Просто он может отправлять заголовки, которые сервер воспринимает как «похоже на браузер». Попробуйте явно поставить Accept: application/json и посмотрите на разницу. Это не про «правильный Postman», а про понимание того, что поведение зависит от контекста запроса.

Ошибка №5: «Я увидел generated password и записал его в README, чтобы не забыть».
Понимаю: мы все любим комфорт. Но это учебный аналог того, как люди случайно коммитят секреты в репозиторий. Сегодня пароль живёт только в логах старта и каждый раз может меняться — и это хорошо. Лучше сразу вырабатывать привычку: секреты не «документируются в открытом виде», даже если это всего лишь учебный проект.

1
Задача
Spring Security, 2 уровень, 4 лекция
Недоступна
Before/after snapshot для зон Secure Content Platform API
Before/after snapshot для зон Secure Content Platform API
1
Задача
Spring Security, 2 уровень, 4 лекция
Недоступна
Baseline matrix для anonymous и authenticated
Baseline matrix для anonymous и authenticated
1
Опрос
Spring Security, 2 уровень, 4 лекция
Недоступен
Spring Security
Базовые правила доступа
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ