Русский
Русский
English
Статистика
Реклама

Spring boot 2

Перевод Okta безопасный доступ к приложениям на Angular Spring Boot

30.04.2021 20:15:00 | Автор: admin

Перевод статьи подготовлен в рамках набора учащихся на курс Разработчик на Spring Framework.

Приглашаем также всех желающих на открытый демо-урок Конфигурация Spring-приложений. На занятии рассмотрим, как можно конфигурировать Spring Boot приложения с помощью свойств:
- properties vs. YAML
- @Value + SpEL
- @ConfigurationProperties
- Externalized конфигурация.
Присоединяйтесь!


Разрабатывая корпоративные веб-приложения, каждый из нас рано или поздно сталкивается с необходимостью обеспечить их безопасность и защитить их ресурсы от несанкционированного доступа. Нужно определить роли для управления доступом, которые будут назначаться пользователям или группам и наделять их полномочиями выполнять те или иные операции в приложении.

Okta это облачный сервис, который позволяет управлять удостоверениями и доступом. Согласно документации Okta, это сервис корпоративного уровня, который создан для управления удостоверениями в облачной среде, но при этом совместим и со многими локальными приложениями. С его помощью ИТ-специалисты могут управлять доступом сотрудников к любому приложению или устройству. Сервис развернут на надежной и защищенной облачной платформе, которая обладает широкими возможностями аудита и тесно интегрируется с локальными приложениями, службами каталогов и системами управления удостоверениями. Подробную информацию о том, как Okta интегрируется с приложениями, можно получить по этой ссылке.

Для авторизации пользователей в Okta используется протокол OAuth2. Подробнее о протоколе OAuth2 и стандарте Open ID Connect (OIDC) можно почитать в блоге Okta.

В этой статье мы рассмотрим демонстрационное приложение Контакты и настройку разрешений на просмотр, создание, редактирование и удаление контактов. Это вторая часть цикла статей о приложении Контакты, посвященная его защите с помощью Okta. О базовом приложении подробно написано в первой статье цикла: Разработка веб-приложения на Spring Boot + Angular.

Полный код, фрагменты которого мы рассматриваем в этой статье, доступен на GitHub.

В итоге наше приложение будет иметь следующую архитектуру:

Компонентная архитектура приложения Контакты, использующего протокол OAuthКомпонентная архитектура приложения Контакты, использующего протокол OAuth

О том, как контейнеризировать это приложение и развернуть его в кластере Kubernetes, можно узнать по ссылкам:

Kubernetes: развертывание приложения на Angular + Java/ Spring Boot в Google Kubernetes Engine (GKE)

Kubernetes: развертывание приложения на Angular + Spring Boot в облаке Microsoft Azure

Конфигурация на портале разработчика Okta

Для начала нам нужно настроить учетную запись Okta и добавить приложение в Okta. Здесь можно создать учетную запись разработчика бесплатно.

После создания учетной записи мы можем добавить наше приложение в консоль администрирования Okta. Для этого выберем вкладку Applications (Приложения) и щелкнем Add Application (Добавить приложение).

Выберем в качестве платформы Single Page App (Одностраничное приложение) и настроим его так, как показано на скриншоте ниже. Здесь мы отметим только опцию Authorization code (Код авторизации), поскольку нам не нужно, чтобы токен возвращался непосредственно в URL-адресе (о связанных с этим рисках безопасности написано в этой статье в блоге Okta).

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

OAuth предлагает различные потоки авторизации (authorization code flows) выбор конкретного потока зависит от сценария использования приложения. О том, какой поток OAuth выбрать, можно прочитать в этой статье на Medium или в документации Okta. Пользовательский интерфейс нашего демонстрационного приложения представляет собой одностраничное приложение на Angular, поэтому выберем неявный поток (implicit flow) с дополнительной проверкой для обмена кодом (Proof Key Code Exchange, PKCE).

Авторизация запросов через сервер авторизации

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

Чтобы увидеть сервер авторизации на портале Okta, нужно выбрать пункт меню, показанный ниже на скриншоте.

Добавление заявлений о группах (groups claims) в приложение Контакты

Для демонстрации создадим группу Admin. Администратор будет обладать правами на создание/редактирование/удаление.

О том, как создать группу и идентифицировать ее с помощью токена доступа, можно узнать в блоге Okta. В официальном блоге Okta описана автономная конфигурация приложения на Spring Boot в качестве веб-проекта Okta. В нашем случае менять тип проекта не нужно. Мы просто создадим группы и включим заявления о группах в токен доступа, выдаваемый сервером авторизации.

На скриншотах ниже показано, как мы создали группу, связали ее с нашим приложением и включили заявление о группах в токен доступа.

Управление группамиУправление группамиНазначение групп/пользователей для приложенияНазначение групп/пользователей для приложенияДобавление заявления о группах в токен доступа, выдаваемый сервером авторизацииДобавление заявления о группах в токен доступа, выдаваемый сервером авторизации

Настроив проект через консоль администрирования Okta описанным выше способом, можно приступать к его интеграции с приложением Контакты для защиты доступа к приложению.

Настройка Okta в клиентской части приложения

Для защиты клиентской части приложения Контакты воспользуемся SDK-библиотекой Okta для Angular. Мы будем авторизовывать пользователя, перенаправляя его на конечную точку авторизации, настроенную для нашей организации в Okta. Библиотеку можно установить с помощью npm:

npm install @okta/okta-angular --save

Чтобы реализовать этот промежуточный слой, после установки нужно будет указать значения из клиента OIDC, полученные на предыдущем этапе. Также понадобится URL-адрес организации в Okta его можно узнать на домашней странице консоли разработчика Okta.

В файле app.module.ts создадим объект конфигурации и в нем установим true для параметра PKCE.

const oktaConfig = {issuer: 'https://{yourOktaDomain}/oauth2/default',redirectUri: window.location.origin + '/implicit/callback',clientId: 'your clientId',pkce: true};

Этот объект нужно добавить в раздел providers. Также импортируем OktaAuthModule:

import { OktaAuthModule, OktaCallbackComponent } from '@okta/okta-angular';import {OKTA_CONFIG} from '@okta/okta-angular';....imports:[...OktaAuthModule...]providers: [...{ provide: OKTA_CONFIG, useValue: oktaConfig }...]

В модуле обработки маршрутов приложения зададим защиту маршрута, воспользовавшись Route Guard из Angular. Вот фрагмент кода из app-routing-module.ts:

import { OktaCallbackComponent } from '@okta/okta-angular';...const routes: Routes = [{ path: 'contact-list',canActivate: [ OktaAuthGuard ],component: ContactListComponent },{ path: 'contact',canActivate: [ OktaAuthGuard, AdminGuard ],component: EditContactComponent },{path: 'implicit/callback',component: OktaCallbackComponent},{ path: '',redirectTo: 'contact-list',pathMatch: 'full'},];

Здесь мы защищаем просмотр контактов с помощью AuthGuard, а создание, обновление, удаление контактов с помощью AdminGuard и OktaAuthGuard.

Метод canActivate в OktaAuthGuard используется для проверки того, прошел ли пользователь процедуру аутентификации. Если нет, он перенаправляется на страницу входа Okta; если да, мы позволяем ему просматривать страницу, на которую ведет маршрут.

async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {this.authenticated = await this.okta.isAuthenticated();if (this.authenticated) { return true; }// Redirect to login flow.this.oktaAuth.loginRedirect();return false;}

Аналогичным образом метод canActivate в AdminGuard проверяет, принадлежат ли вошедшие в систему пользователи к группе Admin; если нет, запрос на доступ к маршруту отклоняется и выводится предупреждение.

async canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {this.user = await this.okta.getUser();const result = this.user.groups.find((group) => group === 'Admin');if (result) {return true;} else {alert('User is not authorized to perform this operation');return false; }}

Ниже показана последовательность операций, которые выполняются, когда пользователь пытается обратиться к маршруту /contact-list для просмотра списка контактов.

Последовательность операций при выводе контактов на экранПоследовательность операций при выводе контактов на экран

Ниже представлена последовательность операций, демонстрирующая принцип работы PKCE. В случае одностраничного приложения, если токен возвращается непосредственно как часть URL-адреса перенаправления, любое расширение браузера может получить доступ к токену еще до того, как его увидит приложение. Это представляет потенциальную угрозу безопасности. Для снижения этого риска в PKCE вычисляется хэш SHA256 от случайной строки верификатора кода (code verifier), и этот хэш включается в запрос кода доступа как контрольное значение (code challenge). Когда поступает запрос на обмен токена доступа на код доступа, сервер авторизации может снова вычислить хэш SHA256 от верификатора кода и сопоставить результат с сохраненным контрольным значением.

Библиотека для Angular производит все эти операции за кадром: вычисляет хэш от верификатора кода с помощью алгоритма SHA256 и обменивает токен на код доступа, сопоставляя верификатор кода с контрольным значением.

Неявное разрешение на доступ (implicit grant) с использованием PKCE в приложении КонтактыНеявное разрешение на доступ (implicit grant) с использованием PKCE в приложении Контакты

Настройка Okta в серверной части приложения

Для того чтобы приложение Spring Boot поддерживало Okta, нужно установить следующие зависимости:

<dependency><groupId>com.okta.spring</groupId><artifactId>okta-spring-boot-starter</artifactId><version>1.2.1</version><exclusions><exclusion><groupId>org.springframework.security.oauth</groupId><artifactId>spring-security-oauth2</artifactId></exclusion></exclusions></dependency><dependency><groupId>org.springframework.security.oauth</groupId><artifactId>spring-security-oauth2</artifactId><version>2.4.0.RELEASE</version></dependency>

Spring Security OAuth2 и Okta Spring Boot Starter следует добавить в classpath, тогда Spring настроит поддержку Okta во время запуска.

Укажем следующие значения конфигурации для настройки и подключения Spring к приложению Okta:

okta.oauth2.client-id=<clientId>okta.oauth2.issuer=https://{yourOktaDomain}/oauth2/defaultokta.oauth2.groups-claim=groups

В классе SpringSecurity нужно указать, что приложение Spring Boot является сервером ресурсов:

@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled = true)@Profile("dev")@Slf4jpublic class SecurityDevConfiguration extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception{http.cors().and().csrf().disable();http.cors().configurationSource(request -> new CorsConfiguration(corsConfiguratione()));// http.authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll()// .and().http.antMatcher("/**").authorizeRequests().antMatchers(HttpMethod.OPTIONS, "/**").permitAll().antMatchers("/").permitAll().anyRequest().authenticated();http.oauth2ResourceServer();}

При такой конфигурации конечные точки нашего приложения защищены. Если попробовать обратиться к конечной точке /contacts по адресу http://localhost:8080/api/contacts, будет возвращена ошибка 401, поскольку запрос не пройдет через фильтр аутентификации.

Для того чтобы предоставить права на создание, обновление и удаление контактов только администратору, нужно включить защиту методов в классе SpringSecurityConfig при помощи следующей аннотации:

@EnableGlobalMethodSecurity(prePostEnabled = true)

На конечных точках, которые мы хотим защитить, можно выполнять проверку ролей с помощью PreAuthorize следующим образом:

@PreAuthorize("hasAuthority('Admin')")public Contact saveContact(@RequestBody Contact contact,@AuthenticationPrincipal Principal userInfo) {

Для получения сведений о вошедшем в систему пользователе можно использовать объект Principal (@AuthenticationPrincipal).

Защита доступа к API из клиентской части приложения с помощью токена доступа

Теперь Okta защищает и клиентскую, и серверную часть нашего приложения. Когда клиентская часть направляет запрос к API, она должна передавать токен доступа в заголовке авторизации. В Angular для этого можно использовать перехватчик HttpInterceptor. В классе перехватчика авторизации можно задать токен доступа для любого HTTP-запроса следующим образом:

private async handleAccess(request: HttpRequest<any>, next: HttpHandler): Promise<HttpEvent<any>> {const accessToken = await this.oktaAuth.getAccessToken();if ( accessToken) {request = request.clone({setHeaders: {Authorization: 'Bearer ' + accessToken}});}return next.handle(request).toPromise();}}

Прямой доступ к API приложения через внешний сервер API

Если все настроено правильно, пользовательский интерфейс нашего приложения сможет получать доступ к API серверной части. В корпоративных приложениях часто присутствует несколько микросервисов, к которым обращается пользовательский интерфейс приложения, а также ряд микросервисов, доступ к которым открыт непосредственно для других приложений или пользователей. Так как наше приложение настроено как одностраничное, в настоящее время оно поддерживает доступ только из JavaScript-кода. Чтобы открыть доступ к микросервисам как к API для других потребителей API, нужно создать в Okta серверное приложение с типом веб-приложение, а затем создать микросервисы как одностраничные приложения и разрешить единый вход для доступа к ним (технически мы имеем дело с двумя разными приложениями в Okta).

Я разработал небольшое решение, позволяющее обойти эту проблему. Мы по-прежнему будем использовать неявный поток с PKCE, но на этот раз не станем перенаправлять пользователя на страницу входа (при направлении запроса к API войти в Okta в ручном режиме невозможно, поскольку в этом случае пользователем будет пользователь API). Вместо этого будем следовать следующему алгоритму.

Последовательность операций при прямом доступе к серверному APIПоследовательность операций при прямом доступе к серверному API

Я создал сервер API, тоесть микросервис-обертку, предоставляющий список API, к которым мы хотим открыть доступ как к серверным API.

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

Она выступает в роли прокси-сервера для основного серверного API нашего приложения и открывает доступ к необходимым сервисам. В ней используется библиотека Spring Cloud OpenFeign, предназначенная для написания декларативных REST-клиентов. Такая обертка представляет собой очень простой способ создания клиентов для веб-сервисов. Она позволяет сократить количество кода при написании потребителей веб-сервисов на базе REST, а также поддерживает перехватчики, позволяющие реализовать любые промежуточные операции, которые должны выполняться до направления запроса к API. В нашем случае мы будем использовать перехватчик для задания токена доступа и распространения прав доступа пользователей. Дополнительные сведения о Feign доступны по этой ссылке.

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

Kubernetes: развертывание приложения на Angular + Java/ Spring Boot в Google Kubernetes Engine (GKE)

Kubernetes: развертывание приложения на Angular + Spring Boot в облаке Microsoft Azure


Узнать подробнее о курсе Разработчик на Spring Framework

Смотреть вебинар Конфигурация Spring-приложений

Подробнее..

Перевод Обеспечение границ компонент чистой архитектуры с помощью Spring Boot и ArchUnit

15.11.2020 20:17:49 | Автор: admin

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

Большинство - возможно, даже все - из этих возможностей идут рука об руку с чистыми зависимостями между компонентами.

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

Со временем границы компонентов в кодовой базе имеют тенденцию размываться.Появляются плохие зависимости, что усложняет работу с кодом.Это имеет всевозможные плохие последствия.В частности, разработка замедляется.

Это тем более важно, если мы работаем над монолитной кодовой базой, охватывающей множество различных областей бизнеса или ограниченных контекстов, если использовать жаргон Domain-Driven Design.

Как мы можем защитить нашу кодовую базу от нежелательных зависимостей?С тщательным проектированием ограниченных контекстов и постоянным соблюдением границ компонентов.В этой статье показан набор практик, которые помогают в обоих случаях при работе со Spring Boot.

Пример кода

Эта статья сопровождается примером рабочего кодана GitHub.

Видимость Package-Private

Что помогает с соблюдением границ компонентов?Уменьшение видимости.

Если мы используем Package-Private видимость для внутренних классов, доступ будут иметь только классы в одном пакете.Это затрудняет добавление нежелательных зависимостей извне пакета.

Итак, просто поместите все классы компонента в один и тот же пакет и сделайте общедоступными только те классы, которые нам нужны вне компонента.Задача решена?

Нет, на мой взгляд.

Это не работает, если нам нужны подпакеты в нашем компоненте.

Нам пришлось бы сделать классы в подпакетах общедоступными, чтобы их можно было использовать в других подпакетах, открыв их для всего мира.

Я не хочу ограничиваться одним пакетом для моего компонента!Возможно, у моего компонента есть подкомпоненты, которые я не хочу показывать снаружи.Или, может быть, я просто хочу отсортировать классы по отдельным сегментам, чтобы упростить навигацию по базе кода.Мне нужны эти дополнительные пакеты!

Так что, да, package-private видимость помогает избежать нежелательных зависимостей, но само по себе это, в лучшем случае, неполноценное решение.

Модульный подход к ограниченным контекстам

Что мы можем с этим поделать?Мы не можем полагаться только на package-private видимость.Давайте рассмотрим подход к сохранению нашей кодовой базы чистой от нежелательных зависимостей с использованием интеллектуальной структуры пакета, package-private видимости, где это возможно, и ArchUnit в качестве средства обеспечения там, где мы не можем использовать package-private видимость.

Пример использования

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

Компонент биллинга предоставляет внешний вид калькулятора счетов.Калькулятор счетов генерирует счет для определенного клиента и определенного периода времени.

Чтобы использовать язык Domain-Driven Design (DDD): компонент биллинга реализует ограниченный контекст, который предоставляет варианты использования биллинга.Мы хотим, чтобы этот контекст был как можно более независимым от других ограниченных контекстов.В остальной части статьи мы будем использовать термины компонент и ограниченный контекст как синонимы.

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

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

Классы API и внутренние классы

Давайте посмотрим на структуру пакета, который я предлагаю для нашего биллингового компонента:

billing api internal     batchjob    |    internal     database         api         internal

У каждого компонента и субкомпонента естьinternalпакет, содержащий, ну, внутренние классы, и дополнительныйapiпакет, содержащий, как вы угадали, классы API, которые предназначены для использования другими компонентами.

Такое разделение пакетов междуinternalиapiдает нам несколько преимуществ:

  • Мы можем легко вкладывать компоненты друг в друга.

  • Легко догадаться, что классы внутриinternalпакета нельзя использовать извне.

  • Легко догадаться, что классы внутриinternalпакета можно использовать из его подпакетов.

  • Пакеты apiиinternalдают нам инструмент для обеспечения соблюдения правил зависимостей с ArchUnit (подробнее об этомпозже).

  • Мы можем использовать столько классов или подпакетов впакетеapiилиinternal, сколько захотим, при этом границы наших компонентов по-прежнему четко определены.

Если возможно, классы внутриinternalпакета должны быть package-private.Но даже если они являются public (и они должны быть public, если мы используем подпакеты), структура пакета определяет четкие и легко отслеживаемые границы.

Вместо того, чтобы полагаться на недостаточную поддержку Java для package-private видимости, мы создали архитектурно выразительную структуру пакета, которая может быть легко реализована с помощью инструментов.

Теперь давайте посмотрим на эти пакеты.

Инверсия зависимостей для предоставления доступа к Package-Private функциям

Начнем сdatabaseподкомпонента:

database api|    + LineItem|    + ReadLineItems|    + WriteLineItems internal     o BillingDatabase

+означает, что класс является public,oозначает, что он является package-private.

databaseКомпонент выставляет API с двумя интерфейсамиReadLineItemsиWriteLineItems, которые позволяют читать и записвысать строку заказа клиента из и в базу данных, соответственно.ТипLineItemдомена также является частью API.

Внутриdatabaseподкомпонент имеет класс,BillingDatabaseреализующий два интерфейса:

@Componentclass BillingDatabase implements WriteLineItems, ReadLineItems {  ...}

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

Обратите внимание, что это применение принципа инверсии зависимостей.

Вместо использованияapiпакета, зависящего отinternalпакета, использется инверсия зависимость.Это дает нам свободу делать вinternalпакете все, что мы хотим, пока мы реализуем интерфейсы вapiпакете.

В случаеdatabaseподкомпонента, например, нам все равно, какая технология базы данных используется для запроса к базе данных.

Давайте также заглянем вbatchjobподкомпонент:

Подкомпонент batchjob не предоставляет API доступа к другим компонентам.У него просто есть классLoadInvoiceDataBatchJob(и, возможно, несколько вспомогательных классов), который ежедневно загружают данные из внешнего источника, преобразуют их и передают их в базу данных биллингового компонента черезWriteLineItemsинтерфейс:

@Component@RequiredArgsConstructorclass LoadInvoiceDataBatchJob {  private final WriteLineItems writeLineItems;  @Scheduled(fixedRate = 5000)  void loadDataFromBillingSystem() {    ...    writeLineItems.saveLineItems(items);  }}

Обратите внимание, что мы используем@ScheduledаннотациюSpring,чтобы регулярно проверять наличие новых элементов в биллинговой системе.

Наконец, содержимое компонента верхнего уровняbilling:

billing api|    + Invoice|    + InvoiceCalculator internal     batchjob     database     o BillingService

Компонент billingпредоставляет доступ к интерфейсу InvoiceCalculatorи доменному типуInvoice.Опять же, интерфейс InvoiceCalculatorреализован внутренним классом, который вызываетсяBillingServiceв примере.BillingServiceобращается к базе данных черезReadLineItemsAPI базы данных для создания счета-фактуры клиента из нескольких позиций:

@Component@RequiredArgsConstructorclass BillingService implements InvoiceCalculator {  private final ReadLineItems readLineItems;  @Override  public Invoice calculateInvoice(        Long userId,         LocalDate fromDate,         LocalDate toDate) {        List<LineItem> items = readLineItems.getLineItemsForUser(      userId,       fromDate,       toDate);    ...   }}

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

Соединяем все вместе с помощью Spring Boot

Чтобы связать все вместе с приложением, мы используем функцию Spring Java Config и добавляемConfigurationкласс вinternalпакеткаждого модуля:

billing internal     batchjob    |    internal    |        o BillingBatchJobConfiguration     database    |    internal    |        o BillingDatabaseConfiguration     o BillingConfiguration

Эти конфигурации говорят Spring внести набор компонентов Spring в контекст приложения.

Полкомпонент конфигурации databaseвыглядит следующим образом:

@Configuration@EnableJpaRepositories@ComponentScanclass BillingDatabaseConfiguration {}

С помощью аннотации @Configurationмы сообщаем Spring, что это класс конфигурации, который вносит компоненты Spring в контекст приложения.

Аннотация @ComponentScanговорит Spring, что нужно включить все классы ,которые находятся в том же пакете, что икласс конфигурации (или подпакет) и аннотированные с@Componentкак бины в контекст приложения.Это загрузит нашBillingDatabaseкласс, приведенный выше.

Вместо @ComponentScanмы могли бы также использовать@Beanаннотированные фабричные методы внутри@Configurationкласса.

Под капотом для подключения к базе данныхdatabaseмодуль использует репозитории Spring Data JPA.Мы включаем их с помощью аннотации @EnableJpaRepositories.

Конфигурация batchjobвыглядит также:

@Configuration@EnableScheduling@ComponentScanclass BillingBatchJobConfiguration {}

Только аннотация @EnableSchedulingдругая.Нам это нужно, чтобы включить аннотацию @Scheduledв нашем bean-компонентеLoadInvoiceDataBatchJob.

Наконец, конфигурация компонента верхнего уровняbillingвыглядит довольно скучно:

@Configuration@ComponentScanclass BillingConfiguration {}

С помощью аннотации @ComponentScanэта конфигурация гарантирует, что подкомпоненты@Configurationбудут обнаружены Spring и загружены в контекст приложения вместе с их bean-компонентами.

Благодаря этому у нас есть четкое разделение границ не только по измерению пакетов, но и по измерению Spring конфигураций.

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

  • Загрузить только один (под) компонент в контекст приложения в рамкахинтеграционного теста SpringBootTest.

  • Включить или отключить определенные (под) компоненты, добавиваннотацию @Conditional...к конфигурации этого подкомпонента.

  • Заменить компоненты, внесенные в контекст приложения, на (под) компонент, не затрагивая другие (под) компоненты.

Однако у нас все еще есть проблема: классы вbilling.internal.database.apiпакете являются public, то есть к ним можно получить доступ извнеbillingкомпонента, что нам не нужно.

Давайте решим эту проблему, добавив в игру ArchUnit.

Обеспечение границ с помощью ArchUnit

ArchUnit- это библиотека, которая позволяет нам проверять утверждения о нашей архитектуре.Они включает проверку того, действительны ли зависимости между определенными классами в соответствии с правилами, которые мы можем сами определить.

В нашем случае мы хотим определить правило, согласно которому все классы вinternalпакете не используются извне этого пакета.Это правило гарантирует, что классы внутриbilling.internal.*.apiпакетов недоступны извнеbilling.internalпакета.

Маркировка внутренних пакетов

Чтобы управлять нашимиinternalпакетами при создании правил архитектуры, нам нужно как-то пометить их как внутренние.

Мы могли бы сделать это по имени (то есть рассматривать все пакеты с именем internal как внутренние пакеты), но мы также можем отметить пакеты другим именем, для чего создадим аннотацию@InternalPackage:

@Target(ElementType.PACKAGE)@Retention(RetentionPolicy.RUNTIME)@Documentedpublic @interface InternalPackage {}

Затем во все наши внутренние пакеты мы добавляемpackage-info.javaфайл с этой аннотацией:

@InternalPackagepackage io.reflectoring.boundaries.billing.internal.database.internal;import io.reflectoring.boundaries.InternalPackage;

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

Проверка отсутствия доступа к внутренним пакетам извне

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

class InternalPackageTests {  private static final String BASE_PACKAGE = "io.reflectoring";  private final JavaClasses analyzedClasses =       new ClassFileImporter().importPackages(BASE_PACKAGE);  @Test  void internalPackagesAreNotAccessedFromOutside() throws IOException {    List<String> internalPackages = internalPackages(BASE_PACKAGE);    for (String internalPackage : internalPackages) {      assertPackageIsNotAccessedFromOutside(internalPackage);    }  }  private List<String> internalPackages(String basePackage) {    Reflections reflections = new Reflections(basePackage);    return reflections.getTypesAnnotatedWith(InternalPackage.class).stream()        .map(c -> c.getPackage().getName())        .collect(Collectors.toList());  }  void assertPackageIsNotAccessedFromOutside(String internalPackage) {    noClasses()        .that()        .resideOutsideOfPackage(packageMatcher(internalPackage))        .should()        .dependOnClassesThat()        .resideInAPackage(packageMatcher(internalPackage))        .check(analyzedClasses);  }  private String packageMatcher(String fullyQualifiedPackage) {    return fullyQualifiedPackage + "..";  }}

ВinternalPackages(), мы используем reflection библиотеку для сбора всех пакетов, аннотированных нашей@InternalPackageаннотацией.

Затем для каждого из этих пакетов мы вызываемassertPackageIsNotAccessedFromOutside().Этот метод использует API-интерфейс ArchUnit, подобный DSL, чтобы гарантировать, что классы, которые находятся вне пакета, не должны зависеть от классов, которые находятся внутри пакета.

Этот тест теперь завершится ошибкой, если кто-то добавит нежелательную зависимость к publicклассу во внутреннем пакете.

Но у нас все еще есть одна проблема: что, если мы переименуем базовый пакет (io.reflectoringв данном случае) в процессе рефакторинга?

Тогда тест все равно пройдет, потому что он не найдет никаких пакетов в (теперь несуществующем)io.reflectoringпакете.Если у него нет пакетов для проверки, он не может потерпеть неудачу.

Итак, нам нужен способ сделать этот тест безопасным при рефакторинге.

Обеспечение безопасного рефакторинга правил архитектуры

Чтобы сделать наш тестовый рефакторинг безопасным, мы проверяем наличие пакетов:

class InternalPackageTests {  private static final String BASE_PACKAGE = "io.reflectoring";  @Test  void internalPackagesAreNotAccessedFromOutside() throws IOException {    // make it refactoring-safe in case we're renaming the base package    assertPackageExists(BASE_PACKAGE);    List<String> internalPackages = internalPackages(BASE_PACKAGE);    for (String internalPackage : internalPackages) {      // make it refactoring-safe in case we're renaming the internal package      assertPackageIsNotAccessedFromOutside(internalPackage);    }  }  void assertPackageExists(String packageName) {    assertThat(analyzedClasses.containPackage(packageName))        .as("package %s exists", packageName)        .isTrue();  }  private List<String> internalPackages(String basePackage) {    ...  }  void assertPackageIsNotAccessedFromOutside(String internalPackage) {    ...  }}

Новый методassertPackageExists()использует ArchUnit, чтобы убедиться, что рассматриваемый пакет содержится в классах, которые мы анализируем.

Мы делаем эту проверку только для базового пакета.Мы не выполняем эту проверку для внутренних пакетов, потому что знаем, что они существуют.В конце концов, мы идентифицировали эти пакеты по@InternalPackageаннотации внутриinternalPackages()метода.

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

Вывод

В этой статье представлен оригинальный подход к использованию пакетов для модульного построения Java-приложения и сочетается его с Spring Boot как механизмом внедрения зависимостей и с ArchUnit для сбоя тестов, когда кто-то добавил недопустимую межмодульную зависимость.

Это позволяет нам разрабатывать компоненты с четкими API и четкими границами, избегая большого шара грязи.

Дайте мне знать свои мысли в комментариях!

Вы можете найти пример приложения, использующего этот подход,на GitHub.

Если вас интересуют другие способы работы с границами компонентов с помощью Spring Boot, вам может бытьинтересен проект moduliths.

Подробнее..
Категории: Java , Testing , Best practices , Spring boot 2

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru