Java for Beginner
777 subscribers
773 photos
220 videos
12 files
1.3K links
Канал от новичков для новичков!
Изучайте Java вместе с нами!
Здесь мы обмениваемся опытом и постоянно изучаем что-то новое!

Наш YouTube канал - https://www.youtube.com/@Java_Beginner-Dev

Наш канал на RUTube - https://rutube.ru/channel/37896292/
Download Telegram
Создание собственных фильтров

Реализация GatewayFilter через AbstractGatewayFilterFactory

Базовый фильтр с конфигурацией:
@Component
public class LoggingGatewayFilterFactory extends
AbstractGatewayFilterFactory<LoggingGatewayFilterFactory.Config> {

public LoggingGatewayFilterFactory() {
super(Config.class);
}

@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
// PRE-обработка
if (config.isLogRequest()) {
logRequest(exchange, config);
}

long startTime = System.nanoTime();

// POST-обработка через then()
return chain.filter(exchange)
.then(Mono.fromRunnable(() -> {
if (config.isLogResponse()) {
long duration = System.nanoTime() - startTime;
logResponse(exchange, config, duration);
}
}));
};
}

public static class Config {
private boolean logRequest = true;
private boolean logResponse = true;
private LogLevel level = LogLevel.INFO;

// Геттеры и сеттеры
}

@Override
public List<String> shortcutFieldOrder() {
return Arrays.asList("logRequest", "logResponse", "level");
}
}


Использование в YAML:
filters:
- Logging=true,true,DEBUG


Глобальные фильтры через GlobalFilter

Глобальные фильтры применяются ко всем маршрутам автоматически.

Пример: фильтр для добавления заголовка трассировки:
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class TraceIdGlobalFilter implements GlobalFilter {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Генерация или извлечение TraceId
String traceId = exchange.getRequest().getHeaders()
.getFirst("X-Trace-Id");

if (traceId == null || traceId.isEmpty()) {
traceId = UUID.randomUUID().toString();
}

// Добавление в заголовки запроса
ServerHttpRequest mutated = exchange.getRequest().mutate()
.header("X-Trace-Id", traceId)
.build();

// Добавление в атрибуты для использования в других фильтрах
exchange.getAttributes().put("traceId", traceId);

return chain.filter(exchange.mutate().request(mutated).build());
}
}


Фильтры с порядком выполнения

Порядок может быть задан через:
Аннотацию @Order
Реализацию интерфейса Ordered
Метод getOrder() в GatewayFilterFactory


Фильтр с динамическим порядком:

@Component
public class DynamicOrderFilter implements GlobalFilter, Ordered {

private final Environment environment;
private int order = 0;

public DynamicOrderFilter(Environment environment) {
this.environment = environment;
// Динамическое определение порядка из конфигурации
this.order = environment.getProperty("gateway.filter.order",
Integer.class, 0);
}

@Override
public int getOrder() {
return order;
}

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Логика фильтра
return chain.filter(exchange);
}
}



#Java #middle #Spring_Cloud_Gateway #Filters
👍2
Сложный фильтр с асинхронными операциями

Пример: фильтр с внешним вызовом для валидации:
@Component
public class ExternalValidationFilter implements GlobalFilter {

private final WebClient webClient;
private final CircuitBreaker circuitBreaker;

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. Извлечение данных для валидации
String apiKey = exchange.getRequest().getHeaders()
.getFirst("X-API-Key");

if (apiKey == null) {
return unauthorized(exchange);
}

// 2. Асинхронная валидация через внешний сервис
return circuitBreaker.run(
webClient.get()
.uri("/validate?apiKey={key}", apiKey)
.retrieve()
.bodyToMono(ValidationResponse.class)
.timeout(Duration.ofSeconds(5))
.onErrorResume(e -> Mono.just(new ValidationResponse(false))),
throwable -> Mono.just(new ValidationResponse(false))
)
.flatMap(validationResponse -> {
// 3. Обработка результата валидации
if (validationResponse.isValid()) {
// Добавление информации о пользователе в заголовки
ServerHttpRequest mutated = exchange.getRequest().mutate()
.header("X-User-Id", validationResponse.getUserId())
.header("X-User-Roles",
String.join(",", validationResponse.getRoles()))
.build();

return chain.filter(exchange.mutate().request(mutated).build());
} else {
return unauthorized(exchange);
}
});
}

private Mono<Void> unauthorized(ServerWebExchange exchange) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}


Фильтр с доступом к телу запроса

Безопасная работа с телом запроса:

@Component
public class RequestBodyLoggingFilter implements GlobalFilter {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Только для определенных Content-Type
MediaType contentType = exchange.getRequest().getHeaders().getContentType();
if (contentType == null || !contentType.includes(MediaType.APPLICATION_JSON)) {
return chain.filter(exchange);
}

// Ограничение размера для логирования
final int maxBodySize = 1024 * 10; // 10KB

return DataBufferUtils.join(exchange.getRequest().getBody(), maxBodySize)
.defaultIfEmpty(EMPTY_BUFFER)
.flatMap(dataBuffer -> {
try {
// Чтение и логирование
byte[] bytes = new byte[Math.min(dataBuffer.readableByteCount(), maxBodySize)];
dataBuffer.read(bytes);

String body = new String(bytes, StandardCharsets.UTF_8);
log.debug("Request body (first {} bytes): {}", bytes.length, body);

// Восстановление тела
DataBuffer restoredBuffer = exchange.getResponse().bufferFactory()
.wrap(bytes);

// Продолжение цепочки с восстановленным телом
ServerHttpRequest mutated = exchange.getRequest().mutate()
.body(Flux.just(restoredBuffer)
.concatWith(exchange.getRequest().getBody()
.skipUntil(buffer -> false))) // Пропускаем уже прочитанное
.build();

return chain.filter(exchange.mutate().request(mutated).build());

} finally {
DataBufferUtils.release(dataBuffer);
}
});
}
}



#Java #middle #Spring_Cloud_Gateway #Filters
👍2
Оптимизация и best practices

Избегание блокирующих операций

Неправильно (блокирующий вызов):
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Блокирующий вызов в реактивном контексте
String result = blockingService.call(); // ОПАСНО!

return chain.filter(exchange);
}


Правильно (асинхронная обёртка):
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// Обёртка блокирующего вызова в отдельном пуле потоков
return Mono.fromCallable(() -> blockingService.call())
.subscribeOn(Schedulers.boundedElastic()) // Выделенный пул для блокирующих операций
.flatMap(result -> {
// Использование результата
return chain.filter(exchange);
});
}


Управление памятью и ресурсами

Освобождение DataBuffer:
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
try {
// Работа с буфером
return processBuffer(dataBuffer)
.flatMap(result -> chain.filter(exchange));
} finally {
// Обязательное освобождение
DataBufferUtils.release(dataBuffer);
}
});
}



Мониторинг и метрики фильтров

@Component
public class MetricsGlobalFilter implements GlobalFilter {

private final MeterRegistry meterRegistry;
private final Map<String, Timer> timerCache = new ConcurrentHashMap<>();

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
long startTime = System.nanoTime();
String routeId = exchange.getAttribute(ROUTE_ATTRIBUTE_ID);

return chain.filter(exchange)
.doOnSuccess(v -> recordSuccess(routeId, startTime))
.doOnError(e -> recordError(routeId, startTime, e));
}

private void recordSuccess(String routeId, long startTime) {
long duration = System.nanoTime() - startTime;

Timer timer = timerCache.computeIfAbsent(routeId, id ->
Timer.builder("gateway.request.duration")
.tag("route", id)
.tag("status", "success")
.register(meterRegistry));

timer.record(duration, TimeUnit.NANOSECONDS);
}
}


#Java #middle #Spring_Cloud_Gateway #Filters
👍2
Раздел 6. Коллекции в Java

Глава 6. Итераторы

Практика: В «Библиотеке» реализовать метод обхода всех книг с помощью Iterator и for-each. Сделать возможность удаления книги во время обхода

Подготовка

Убедитесь, что ваш проект готов, и вспомните ключевые концепции итераторов:
for-each — синтаксический сахар над Iterator.
Iterator позволяет безопасно удалять элементы во время перебора (метод remove()).
Прямое удаление через list.remove() во время for-each — ConcurrentModificationException.


Откройте проект «Библиотека»: Запустите IntelliJ IDEA и откройте проект LibraryProject. Убедитесь, что класс Library содержит List<Book> books (ArrayList) и методы addBook, findBookByTitle и т.д.
Импортируйте необходимые пакеты: В файле
Library.java импортируйте java.util.Iterator (IDE подскажет при написании).


Реализация методов обхода книг

Мы добавим два метода в класс Library: один с явным Iterator, второй с for-each, и покажем, как безопасно удалять во время итерации.

Создайте метод printAllBooksWithIterator():
Метод должен быть публичным и void.
Получите Iterator<Book> из списка books с помощью books.iterator().
В цикле while используйте hasNext() для проверки наличия следующего элемента.
Внутри цикла получите текущую книгу через next().
Вызовите printDetails() на книге или выведите её информацию.
(Опционально) Добавьте нумерацию (int index = 0; index++).


Создайте метод printAllBooksWithForEach():
Метод аналогичен предыдущему, но используйте for-each цикл: for (Book book : books).
Внутри цикла вызовите printDetails() на книге.


Создайте метод removeBookDuringIteration(String titleToRemove):
Метод должен безопасно удалять книгу по названию во время итерации.
Получите Iterator<Book>.

В цикле while (iterator.hasNext()):
Получите текущую книгу через next().
Сравните её title с titleToRemove (equalsIgnoreCase для удобства).
Если совпадение — вызовите iterator.remove() (не list.remove!).
Выведите сообщение "Книга удалена: [title]".
После цикла выведите "Обход завершён".
Важно: Используйте именно iterator.remove() — это единственный безопасный способ удаления во время итерации. Прямой books.remove(book) вызовет ConcurrentModificationException.


Обновление класса Main для тестирования

Теперь протестируем новые методы в Main.

Добавьте книги:
Создайте объект Library.
Добавьте 4-5 книг через addBook().


Протестируйте обход:
Вызовите printAllBooksWithIterator() — увидите все книги.
Вызовите printAllBooksWithForEach() — тот же результат.


Протестируйте удаление во время итерации:
Вызовите removeBookDuringIteration() с названием существующей книги.
После удаления вызовите printAllBooks...() — книга должна исчезнуть.
Попробуйте удалить несуществующую книгу — ничего не удалится, но обход завершится.


Демонстрация ошибки (опционально):
Создайте отдельный метод с for-each и внутри цикла books.remove(book) — запустите и поймайте ConcurrentModificationException (в реальном коде не делайте так!).


Тестирование и отладка

Запустите проект:
Run 'Main.main()' — наблюдайте за выводом книг до и после удаления.

Проверьте поведение:
Удаление работает только через iterator.remove().
После remove список уменьшается, последующие книги выводятся корректно.


Отладка:
Установите breakpoint в цикле iterator.hasNext() и next().
Шагайте (F8) — увидите, как iterator перемещается и как remove() влияет на список.
В watch добавьте books.size() — увидите уменьшение.


Эксперименты:
Попробуйте модифицировать книгу во время итерации (book.setTitle(...)) — это разрешено.
Добавьте несколько удалений в одном проходе — iterator.remove() безопасен.



#Java #для_новичков #beginner #for_each #fail_fast #fail_safe #Практика
Полезные советы для новичков

Iterator vs for-each: for-each проще, но не позволяет удалять. Iterator — для модификации.
remove() итератора: Вызывается после next(), удаляет последний полученный элемент.
ConcurrentModificationException: Избегайте прямого list.remove() в for-each.
Альтернатива: Для удаления нескольких элементов соберите индексы/объекты в другой список, затем удалите после итерации.
Импорты: import
java.util.Iterator;


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

Задача 1:
Добавьте в Library метод removeAllByAuthor(String author), который использует Iterator для удаления всех книг данного автора.
Задача 2: Реализуйте метод printBooksBackwards() — используйте ListIterator (books.listIterator(books.size())) и previous() для обратного обхода.
Задача 3: Протестируйте удаление во время итерации с несколькими совпадениями — убедитесь, что все удаляются безопасно.


Реализуйте эти задачи самостоятельно — это закрепит работу с Iterator и понимание итерации.


#Java #для_новичков #beginner #for_each #fail_fast #fail_safe #Практика
🔥1🆒1
Что выведет код?

import java.util.*;

public class Task181225 {
public static void main(String[] args) {
List<String> list = new ArrayList<>(Arrays.asList("a", "b", "c"));

try {
list.forEach(item -> {
System.out.print(item + " ");
if (item.equals("b")) {
list.add("d");
}
});
} catch (Exception e) {
System.out.print("Exception ");
}

System.out.println(list.size());
}
}


#Java
🔥2
Интеграция Spring Cloud Gateway с Spring Security: Архитектура реактивной безопасности

Двойная цепочка фильтров: концептуальная модель

При интеграции Spring Security с Spring Cloud Gateway формируется двухуровневая архитектура обработки запросов, где каждая цепочка фильтров выполняет свою специализированную роль. Spring Security оперирует на уровне SecurityWebFilterChain, отвечая за аутентификацию и авторизацию, в то время как Spring Cloud Gateway управляет GatewayFilterChain, специализируясь на маршрутизации и трансформации трафика.

Архитектурно это представляет собой последовательный pipeline, где запрос сначала проходит через механизмы безопасности, и только после успешной проверки попадает в систему маршрутизации. Такой подход обеспечивает принцип "security first" — ни один запрос не достигает бизнес-логики без предварительной проверки прав доступа.

Реактивная реализация обеих цепочек означает, что они построены на неблокирующей модели, где каждый фильтр описывает отложенные вычисления в виде реактивных потоков (Mono/Flux), а не выполняет синхронные операции. Это позволяет системе эффективно обрабатывать тысячи одновременных соединений с минимальным количеством потоков.



Реактивный контекст безопасности

В отличие от традиционного Spring Security, который хранит контекст аутентификации в ThreadLocal, реактивная версия использует ReactiveSecurityContextHolder, основанный на реактивном контексте (Context). Этот контекст распространяется через реактивный pipeline с помощью операторов subscriberContext() или contextWrite().

Когда запрос проходит через цепочку безопасности, аутентификационный объект помещается в реактивный контекст, который затем автоматически распространяется на все последующие стадии обработки, включая фильтры Gateway и вызовы downstream сервисов. Это обеспечивает сквозную передачу информации об аутентификации без необходимости явной передачи токенов или других идентификаторов между компонентами.



#Java #middle #Spring_Cloud_Gateway
👍2
Принципы Token Relay в распределённых системах

Концептуальные модели передачи токенов

Token Relay (передача токенов) — это архитектурный паттерн, при котором API Gateway передаёт токены аутентификации от клиента к внутренним сервисам. Существует три основные стратегии реализации этого паттерна, каждая со своей областью применения и компромиссами.

Pass-through модель — наиболее простая стратегия, при которой Gateway передаёт исходный токен клиента (обычно JWT) без изменений. Этот подход сохраняет оригинальную сигнатуру токена и все claims, что упрощает аудит, но создаёт потенциальную уязвимость: внутренние сервисы получают полный доступ к claims, которые могут содержать конфиденциальную информацию, не предназначенную для их уровня доступа.

Token Exchange модель предполагает замену клиентского токена на новый, с ограниченным набором claims и scope, специфичным для целевого сервиса. Gateway выступает в роли trusted party, которая аутентифицирует клиента, а затем запрашивает у сервера авторизации новый токен с уменьшенными привилегиями. Этот подход реализует принцип минимальных привилегий, но увеличивает задержку и создаёт зависимость от сервиса авторизации.

Token Enrichment модель — гибридный подход, при котором Gateway добавляет дополнительные claims к исходному токену, не изменяя его базовую структуру. Например, Gateway может добавить информацию о маршруте, уровне обслуживания или других контекстных данных. Этот подход балансирует между безопасностью и производительностью, но требует тщательной проверки целостности токена.


Безопасность передачи токенов

При реализации Token Relay критически важны механизмы защиты от атак. Gateway должен проверять целостность токенов (подпись JWT), срок действия, издателя (issuer) и аудиторию (audience). Для предотвращения replay-атак может использоваться механизм одноразовых номеров (nonce) или проверка времени выпуска токена (iat claim).

Распространённой практикой является использование двойной проверки токенов: Gateway проверяет токен при поступлении запроса, а внутренние сервисы могут выполнять дополнительную, более детальную проверку, специфичную для их домена. Это создаёт defense-in-depth архитектуру, где компрометация одного компонента не приводит к полному нарушению безопасности системы.



#Java #middle #Spring_Cloud_Gateway
👍2
Интеграция с внешними системами аутентификации

Архитектурные паттерны интеграции

Интеграция Gateway с внешними провайдерами аутентификации (Keycloak, Auth0, Okta) следует одному из двух архитектурных паттернов: делегированной аутентификации или федерации идентификации.


При делегированной аутентификации Gateway перенаправляет клиента к внешнему провайдеру для аутентификации, а затем получает токен, который использует для доступа к внутренним сервисам. Gateway в этом случае выступает как OAuth2 client или OpenID Connect Relying Party. Этот паттерн обеспечивает максимальную безопасность, так как учётные данные никогда не проходят через Gateway, но создаёт дополнительное взаимодействие с внешним сервисом.


Федерация идентификации предполагает, что Gateway самостоятельно проверяет токены, выпущенные внешним провайдером, используя публичные ключи или эндпоинты обнаружения. Gateway загружает конфигурацию провайдера (например, из .well-known/openid-configuration) и кэширует публичные ключи для проверки подписи. Этот паттерн уменьшает зависимость от внешнего сервиса во время обработки каждого запроса, но требует тщательной настройки механизмов обновления ключей и конфигурации.


Обработка ошибок безопасности в реактивном контексте

Теория обработки исключений в реактивных цепочках

В реактивном программировании исключения обрабатываются иначе, чем в императивном коде. Вместо блоков try-catch используются операторы onErrorResume, onErrorReturn и doOnError, которые позволяют обрабатывать ошибки в потоке данных без прерывания выполнения цепочки.

Когда фильтр безопасности обнаруживает нарушение (например, невалидный токен или недостаточные права), он не бросает исключение в традиционном смысле, а возвращает Mono.error(), который создаёт сигнал ошибки в реактивном потоке. Этот сигнал затем перехватывается обработчиками ошибок, которые преобразуют его в соответствующий HTTP-ответ.

Архитектурно важно различать ошибки аутентификации (401) и авторизации (403). Ошибки аутентификации указывают на проблему с установлением личности пользователя, в то время как ошибки авторизации возникают, когда аутентифицированный пользователь пытается выполнить действие, на которое у него нет прав. Gateway должен различать эти случаи, даже когда внешний провайдер возвращает унифицированные коды ошибок.


Каскадная обработка ошибок безопасности

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

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

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



#Java #middle #Spring_Cloud_Gateway
👍2
RBAC и ABAC в контексте Gateway

Теоретические основы контроля доступа

Role-Based Access Control (RBAC) и Attribute-Based Access Control (ABAC) представляют собой две парадигмы контроля доступа, которые могут быть реализованы на уровне Gateway.

RBAC основывается на концепции ролей — именованных наборов разрешений. Пользователям назначаются роли, а ролям — разрешения на выполнение операций. В контексте Gateway роли могут быть определены как в терминах бизнес-функций (ADMIN, USER, GUEST), так и в терминах технических возможностей (READ_ONLY, WRITE_ACCESS, CONFIGURATION_MANAGER). Gateway проверяет наличие у пользователя роли, необходимой для доступа к конкретному маршруту.

ABAC использует атрибуты для принятия решений об авторизации. Атрибутами могут быть свойства пользователя (отдел, уровень доступа), ресурса (категория, чувствительность), действия (время суток, метод HTTP) и окружения (расположение, тип устройства). ABAC обеспечивает более гибкий контроль, чем RBAC, но требует более сложной инфраструктуры для оценки политик.

На практике часто используется гибридный подход: RBAC для грубой фильтрации на уровне Gateway и ABAC для детального контроля на уровне бизнес-сервисов. Gateway проверяет базовые роли, необходимые для доступа к группе эндпоинтов, а внутренние сервисы выполняют более детальные проверки на основе атрибутов.


Декларативные политики доступа

Современные подходы к авторизации в Gateway стремятся к декларативным, конфигурируемым политикам, отделённым от кода приложения. Политики доступа могут быть определены в конфигурационных файлах (YAML), базах данных или внешних системах управления политиками.

Пример декларативной политики в YAML:
access-policies:
- resource: /api/users/**
methods: [GET, POST]
required-roles: [USER, ADMIN]
required-permissions: [users:read]
conditions:
- time: "09:00-17:00"
- day-of-week: [MON-FRI]

- resource: /api/admin/**
methods: [*]
required-roles: [ADMIN]
ip-whitelist: [192.168.1.0/24]


Такие политики могут быть динамически загружены и применены без перезапуска Gateway, что позволяет оперативно реагировать на изменения требований безопасности.


CORS как механизм безопасности

Теория Cross-Origin Resource Sharing


CORS (Cross-Origin Resource Sharing)
— это механизм безопасности браузеров, который контролирует доступ к ресурсам с разных источников (доменов). Хотя CORS часто рассматривается как препятствие для разработки, на самом деле это критически важный механизм защиты от межсайтовых атак.

Когда браузер выполняет cross-origin запрос, он сначала отправляет preflight запрос (OPTIONS) для проверки, разрешён ли фактический запрос. Gateway должен правильно обрабатывать эти preflight запросы, возвращая соответствующие заголовки CORS без выполнения бизнес-логики.

Теоретически, CORS политики должны быть максимально строгими: разрешать только необходимые домены, методы и заголовки. Распространённая ошибка — использование wildcard (*) для Access-Control-Allow-Origin, которая открывает API для любого сайта в интернете. Вместо этого Gateway должен динамически определять разрешённый origin на основе домена запроса или конфигурации маршрута.

Сочетание CORS с другими механизмами безопасности
CORS не заменяет, а дополняет другие механизмы безопасности. Даже при правильной настройке CORS Gateway всё равно должен проверять аутентификацию и авторизацию, так как CORS защищает только от атак через браузер, но не от прямых HTTP-запросов (через curl, Postman или другие сервисы).

Важным аспектом является согласование CORS политик между Gateway и внутренними сервисами. В идеале, Gateway должен быть единственным компонентом, который устанавливает заголовки CORS, а внутренние сервисы не должны дублировать эту функциональность. Это обеспечивает централизованное управление политиками и предотвращает конфликты конфигураций.


#Java #middle #Spring_Cloud_Gateway
👍3