Безпека by design. Частина 6. (Забезпечення цілісності стану)

10 вересня 2023 1 хвилина Автор: Lady Liberty

Мінливий стан в системах: Від основ до практики

Мінливий стан є важливим аспектом системи. Насправді багато з них створено саме для зміни стану, як у випадку з онлайн-книгарнею в розділі 2. Система намагається відстежувати різноманітні зміни стану: розміщення книг у кошику, оплата замовлень, надсилання книги покупцям. Якщо держава не зміниться, нічого цікавого не буде. З технічної точки зору змінний стан можна представити різними способами. Ми розглянемо деякі з них і продемонструємо наш кращий підхід, специфічну для сутності модель проектування, змодельовану в розділі 3. Оскільки сутності містять стан, який представляє ваш домен, важливо, щоб вони дотримувалися бізнес-правил під час створення.

Сутності можуть бути створені в конфліктному стані, що може спричинити помилки програмного забезпечення та вразливі місця безпеки, які важко ідентифікувати. Однак дотримання всіх обмежень при створенні може бути непростим завданням. Складність залежить від їх тяжкості та складності. Ми обговоримо деякі техніки, придатні для роботи зі змінними станами в більшості випадків. Почнемо з технік, які задовольняють прості обмеження, і закінчимо шаблоном проектування Builder, який може впоратися навіть із досить складними ситуаціями. Коли сутності створюються послідовно, вони повинні бути узгодженими. Ми проілюструємо поширені підводні камені, які загрожують цілісності вашої організації, і порадимо вам, як найкраще створити організацію для її захисту. Давайте почнемо з різних способів керування станом, щоб ви зрозуміли, чому ми віддаємо перевагу роботі з сутностями. Для деяких мов програмування, таких як Haskell, існує цікава концепція, згідно з якою весь код складається лише з незмінних структур. Однак більшість мов, включаючи Java, C, C#, Ruby та Python, використовують змінні компоненти як об’єкти з внутрішнім станом.

Державне управління з суб’єктами господарювання

Основне завдання більшості систем – відстежити, як змінюються різні речі. Коли валізу завантажують на борт літака, він, як і літак, змінює свій стан. При цьому несподівано вступають в силу нові правила, яких не було до завантаження. Валізу вже не можна відкрити, але тепер її можна розвантажити; Збільшується завантаженість літака. Це те, що в інформатиці називається змінним станом. Ви повинні стежити за змінами стану вашої системи і переконатися, що всі вони відповідають правилам.

Якщо система, яку ви пишете, не керує змінами належним чином, рано чи пізно у вас виникнуть проблеми з безпекою, можливо, незначні або, можливо, серйозні. Система управління багажем в аеропорту повинна відстежувати валізи. Якщо валіза не відсканований, він не повинен потрапити на борт літака, система зобов’язана цього не допустити. Вона також повинна знати, кому належить багаж і чи знаходиться на борту розглянутий пасажир. Якщо пасажир не з’явився, багаж необхідно розвантажити, інакше це загрожує серйозними ризиками для безпеки.

Всі підходи до проектування, розглянуті в цьому розділі, передбачають  моделювання змін як сутності (якщо використовувати термінологію DDD). Як описано в главі 3, DDD  визначає сутність як щось, що має стабільний ідентифікатор і стан, який може змінюватися протягом свого життя. Хорошим прикладом цього є багаж в аеропорту. Його можна зареєструвати, відсканувати, завантажити на борт і вивантажити назад,  Але в нашому розумінні це той самий чемодан, стан якого змінюється. Сутності є кращим засобом реалізації змінного стану, але давайте коротко розглянемо альтернативи (рис. 6.1).

Під час проектування системи зміни стану можна відстежувати та обробляти різними способами.

  • Ви можете зберігати стан усередині cookie.

  • Ви можете вносити зміни до бази даних безпосередньо за допомогою SQL або збережених процедур.

  • Ви можете використовувати програму, яка завантажує стан із сервера, оновлює його та відправляє назад.

Всі ці підходи цілком здійсненні і мають різні переваги. Але, на жаль, багато систем використовують безладну їх суміш. Це ризиковано. Якщо відповідальність за зберігання та управління змінами станів розподілена між багатьма різними компонентами, існує небезпека, що ці компоненти погано поєднуються. Саме дрібні логічні протиріччя роблять можливими дірки в безпеці. Тому ми віддаємо перевагу архітектурі, в якій легко зрозуміти, які стани дійсні і які зміни можливі в кожному з них.

З нашого досвіду, для забезпечення безпечного управління державою найкраще моделювати її як суб’єкти в стилі DDD. В процесі моделювання потрібно підбирати найважливіші поняття. Можливо, щоб краще зрозуміти правила, підійде валізу, рейс і пасажир, а може, варто розглянути цю ситуацію з точки зору реєстрації, завантаження і людей, які сіли на борт. У першому випадку ділове правило може звучати так: «Валіза повинен бути на тому ж рейсі, що і пасажир, який його зареєстрував». У другому випадку правило можна перефразувати: «Завантажувати можна тільки перевірені валізи, що належать пасажирам на борту». Ви, напевно, погодитеся, що перше речення легше читається, тому воно повинно лягти в основу вашої моделі. Але, як ви бачили в Главі 3, іноді варто витратити час на вивчення інших моделей, а іноді варто краще зрозуміти предметну область, як показав приклад у Главі 2.

Однією з переваг суб’єктів є те, що все, що ми знаємо про державу і зміни, які в ній відбуваються, зосереджено в одному місці. Ми також вважаємо за краще реалізовувати змінюваний стан, використовуючи клас, який містить як дані, так і пов’язані з ними поведінки (у вигляді методів). Такий підхід буде проілюстрований багато разів не тільки в цьому розділі, але і в наступному розділі, який присвячений спрощенню.

Існує нескінченна кількість способів проектування об’єктів, і ми поділимося методами та методами, які допоможуть зробити вашу архітектуру та код зрозумілими та безпечними. Далі в цьому розділі ми дізнаємося, як створювати та підтримувати організації в послідовному стані, який не ставить під загрозу цілісність бізнесу. По-перше, давайте подивимося на безпечне створення сутностей.

Узгодженість на момент створення

Організація, яка порушує правила ведення бізнесу, є проблемою безпеки. Особливо це актуально для сутностей в момент їх створення, тому необхідно, щоб механізм створення спочатку гарантував їх узгодженість. Ця рада може здатися очевидним, але іноді він сприймається як формальність, і результати його можуть бути плачевними.

Одного разу один з наших колег працював з великим азіатським банком. Він виявив кілька вразливостей безпеки, але всі вони були сприйняті як незначні технічні недоліки. Лише коли йому вдалося створити обліковий запис без власника, проблему почали сприймати серйозно. Банківський рахунок без власника – це щось нечуване, і сам факт його існування міг коштувати банку ліцензії. Про проблему відразу ж доповіли вищому керівництву, яке присвоїло їй найвищий пріоритет.

Сутність, яка не відповідає вимогам, є ризиком для безпеки, і, з нашого досвіду, найкращий спосіб впоратися з нею – наполягати на тому, що всі об’єкти сутності повинні бути послідовними на момент створення. У  цьому розділі ми покажемо небезпеку найпоширенішої помилки, безаргументного конструктора, і розглянемо деякі альтернативні рішення. Крім того, ми обговоримо різні способи створення сутності,  від найпростіших до найсучасніших. Збільшення складності обмежень призводить до ускладнення процедури створення. Якщо ваші обмеження банальні, досить буде простої процедури. Почнемо з елементарного випадку використання конструктора без аргументів. Він настільки простий, що практично марний. В кінці буде продемонстрована схема конструкції «Будівельник», розрахована на використання в найскладніших ситуаціях.

Сутності часто представляють інформацію, яка зберігається і змінюється протягом тривалого періоду часу, тому нерідко вони зберігаються в базі даних. Коли реляційна база даних використовується спільно з об’єктно-реляційним відображенням (ORM), таким як JPA або Hibernate, може виникнути плутанина, яка призводить до поганих і небезпечних архітектурних рішень. Ми поговоримо про те, як використовувати такі фреймворки без шкоди для безпеки.

Вступний приклад цієї глави взятий зі сфери фінансів, але проблема поширюється і на інші сфери. Сутності, які виявляються суперечливими відразу після створення, зустрічаються в коді різних проектів. Багато з них мають дещо спільне: створення об’єктів за допомогою конструктора, який не приймає аргументів.

Небезпека конструкторів, які не мають аргументів

Найпростіший спосіб створити сутність, безсумнівно, використовувати конструктор. Що може бути простіше, ніж викликати конструктора без аргументів? Проблема в тому, що такі конструктори рідко забезпечують створення повністю узгоджених і готових до використання об’єктів.

Якщо задуматися, конструктор без аргументів – це щось дивне. Він обіцяє створити не просто об’єкт, а об’єкт, для якого не потрібно вказувати ніяких атрибутів. Наприклад, якщо взяти машину, у неї не буде ні кольору, ні кількості дверей, ні навіть марки. В крайньому випадку, якщо який-небудь з цих атрибутів все ж встановлений, він буде мати значення за замовчуванням, яке повинно підходити для всіх автомобілів при їх створенні. На практиці конструктори без аргументів не виконують своїх прямих обов’язків – вони не створюють узгоджених об’єктів, готових до використання.

Ми часто стикаємося з сутностями, для створення яких існує якесь узгодження: спочатку викликається конструктор без аргументів, а потім серія методів сеттера, які ініціалізують об’єкт і готують його до використання. Але в кодексі немає нічого, що гарантувало б дотримання цієї угоди. І, на жаль, вона часто забувається або виконується неправильно, що призводить до появи суперечливих сутностей.

ОБЕРЕЖНО. Якщо сутність має конструктор без аргументів, це, швидше за все, означає, що його ініціалізація заснована на сеттері. І це стає потенційним джерелом проблем. Ініціалізація на основі сетера може бути неповною, а неповна ініціалізація робить об’єкти суперечливими.

Давайте подивимося, з яким кодом ми часто стикаємося. У лістингу 6.1 ви бачите  клас Account з  декількома атрибутами: банківський рахунок повинен мати номер, власника та процентну ставку. Він може мати необов’язковий кредитний ліміт, що дозволяє власнику рахунку зайняти певну суму, і резервний рахунок (частіше накопичувальний), з якого при необхідності знімаються кошти, щоб залишок на основному рахунку не став нульовим або негативним. У методі AccountService.openAccount показує, як планується використовувати цей конструктор без аргументів. Слідом за конструктором послідовно викликаються методи setter для заповнення  об’єкту Account  даними.

Такий підхід дозволяє створити абсолютно порожній об’єкт з подальшим заповненням обов’язкових полів. Однак немає гарантії, що  об’єкт Облікового запису  зможе відповідати навіть самим фундаментальним і важливим бізнес-обмеженням. Більш того, такий підхід ненадійний, так як доводиться заново виконувати всі дії при створенні кожного об’єкта. Якщо умови зміняться, оновлення коду перетвориться на кошмар. Уявіть собі,  Наприклад, в рамках Міжнародної антикорупційної ініціативи багато країн мають спеціальні фінансові положення для політиків. Наприклад, державні чиновники більш схильні до корупції та хабарництва через свій вплив. Кожен акаунт повинен мати інформацію про те, чи належить він політику.

Уявіть, що ви працюєте  з класом Account і, згідно з новими вимогами, він повинен мати додаткове поле  boolean political exposedPerson. Крім того, його потрібно встановлювати вручну кожного разу, коли створюється сутність. Тепер ви повинні знайти кожен розділ з новим конструктором  Account() у вашому коді і переконатися, що setPolitical ExposedPerson також викликається в них.

Компілятор не повідомить про помилку, як це було б, якби ви додали параметр до списку аргументів конструктора. Добре налагоджений набір тестів зможе виявити помилку, але наш досвід показує, що кодові бази з конструкторами, які не мають аргументів, рідко надають такі тести. На жаль, з кожним новим атрибутом деякі фрагменти коду будуть пропускатися, і кожен раз вони можуть бути різними. Зазвичай з часом такий процес призводить до суперечливості кодової бази, в безпеці якої рано чи пізно з’являються дірки.

Фреймворки та конструктори ORM без аргументів

Якщо ви використовуєте фреймворк об’єктно-реляційного відображення, такий як JPA (Java Persistence API) або Hibernate, ви можете відчувати, що змушені використовувати безаргументні конструктори для своїх сутностей. Вправи на ці фреймворки завжди починаються зі створення сутності таким чином, і схоже, що код повинен бути написаний саме так. Але це не зовсім так. Якщо ви працюєте з таким фреймворком, у вас є два варіанти, щоб уникнути проблем безпеки, спільних для конструкторів без аргументів: або відокремити модель домену від моделі сховища, або переконатися, що фреймворк зберігання не може надати доступ до суперечливих об’єктів.

Перший варіант – відмежуватися від моделі зберігання на концептуальному рівні. Якщо ви це зробите, модель сховища буде в окремому пакеті разом з іншим кодом інфраструктури. При отриманні записів з бази даних фреймворк завантажує їх в об’єкти моделі зберігання. Потім ми використовуємо ці об’єкти для створення об’єктів домену, які потім оброблятимуть виклики бізнес-логіки. Це дає вам повний контроль над створенням будь-яких об’єктів домену, а всі JPA-анотації залишаються в окремому пакеті разом з моделлю зберігання даних.

Якщо ви не робите цього розрізнення і безпосередньо зіставляєте об’єкти домену за допомогою фреймворку зберігання даних, вам доведеться використовувати цей фреймворк дуже обережно. З нашого досвіду, цей стиль досить поширений і може бути безпечним, тому ми хотіли б поділитися кількома хитрощами, які допоможуть вам.

Фреймворки зберігання даних, такі як Hibernate і JPA, дійсно потребують конструкторів без аргументів. Справа в тому, що ці фреймворки повинні створювати об’єкти при отриманні записів з бази даних. Для цього вони цілеспрямовано створюють порожній об’єкт і наповнюють його інформацією за допомогою рефлексії. Тому їм спочатку потрібен конструктор без аргументів. Однак він не обов’язково повинен бути публічним — і Hibernate, і JPA можуть чудово працювати з приватними будівельниками. Крім того, ці фреймворки не потребують методів сетера для вбудовування даних — якщо ви вказуєте стиль зберігання в анотаціях приватних полів, вони можуть застосувати відображення, ініціалізувавши ці поля безпосередньо.

Всі обов’язкові поля як аргументи конструктора

Розглянемо просте рішення проблем безпеки, викликаних суперечливими сутностями: замість конструктора без аргументів, який не передає достатньо інформації для послідовного створення, скористаємося конструктором, який приймає всі необхідні дані (рис. 6.2).

Давайте розширимо список параметрів конструктора, щоб він включав всю необхідну інформацію. На даний момент вам не потрібні ніякі параметри з додатковою інформацією. Переконайтеся, що сутність створено в повністю узгодженому стані. Необов’язкові дані можуть бути задані після створення, викликаючи окремі методи.

Лістинг 6.2 показує результат застосування цього підходу до попереднього прикладу з  класом Account. Конструктору потрібно передати номер рахунку, інформацію про власника та процентну ставку, які є обов’язковими атрибутами. Додаткові атрибути кредитного ліміту та резервного рахунку не включаються до списку аргументів конструктора і задаються пізніше шляхом виклику окремих методів.

Список аргументів конструктора містить лише обов’язкові поля, тому ми очікуємо, що жодне з них ніколи не буде нульовим. До конструктора можна додати відповідні перевірки.

Історія правил іменування getter/setter в JavaBeans  У списку 6.2 ви дали імена сетерів і геттерів, які краще відображають їх роль у домені. Як згадувалося в попередньому розділі, існує помилкова думка, що геттери і сетери необхідні для правильної роботи фреймворків зберігання даних. Це не так. Їх можна замінити польовими анотаціями. Фреймворки, такі як Hibernate і Spring Data JPA,  Застосовують рефлексію, яка дозволяє їм знаходити приватні поля. З цієї причини нам не потрібні публічні методи, названі певним чином.

Ми також хотіли б розповісти вам, звідки взялися правила іменування геттерів/сеттерів. Вони були розроблені в 1996 році як частина специфікації JavaBeans. Основна ідея цього проекту полягала в тому, щоб створити структуру, яка дозволила б постачальникам надавати готові  компоненти, відомі як квасоля. Ці компоненти можна було придбати окремо та зібрати разом за допомогою графічних інструментів. Однак ця концепція виявилася невдалою, і її специфікація перестала розвиватися після виходу версії 1.01. Однак існують дивні правила щодо використання набору і  отримання приставок в іменах  З якоїсь причини вони увійшли в побут. Більш цікавою стороною цього фреймворку був механізм взаємодії компонентів за допомогою подій, але, на жаль, він не набув такої популярності.

Окрім правил щодо іменування сетерів та геттерів, специфікацію JavaBeans можна вважати мертвою. Ми не бачимо великої користі в дотриманні правил, які втратили своє первісне призначення. Ми вважаємо за краще називати методи в коді відповідно до традиційної об’єктної ідеєю, згідно з якою методи пов’язані з діями в предметній області: наприклад, об’єкт може мати метод обробки отриманих ним повідомлень.

Ми поступово переходимо від елементарних об’єктів, які мають тільки необхідні атрибути, до більш складних умов, можливо, з додатковими атрибутами. Передавати все це конструктору у вигляді аргументів буде незручно. Вам, напевно, вже доводилося мати справу з конструкторами, що складаються з 20 параметрів. Крім того, дизайнеру складно дотримати всі умови, які поширюються відразу на кілька атрибутів.

Для надійного створення найскладніших сутностей необхідний шаблон проектування Builder. Але перш ніж з ним познайомитися, давайте розглянемо ще один цікавий спосіб створення об’єктів з обов’язковими і додатковими полями, який допомагає зробити код на стороні клієнта читабельним. Йдеться про плавних інтерфейсах.

Створення об’єктів за допомогою інтерфейсів Fluent

Створення складних сутностей з великою кількістю обмежень вимагає більш потужного інструменту. Ми поговоримо про шаблон дизайну Builder за мить, але щоб вам було легше зрозуміти, давайте розглянемо стиль дизайну, який полегшує читання клієнтського коду та забезпечує його правильність. Йдеться про плавному інтерфейсі.

Назва цього стилю було запропоновано в 2005 році Еріком Евансом і Мартіном Фаулером, хоча коріння він сягає спільноти Smalltalk 1970-х років. Вільний інтерфейс був створений для того, щоб код можна було читати так, ніби це текст природною мовою, і це часто досягається методами об’єднання.

Щоб проілюструвати цей стиль, давайте застосуємо його на практиці і покажемо, як він впливає на код, необхідний для підготовки сутності. Лістинг 6.3 показує  клас Account, адаптований для забезпечення вільного інтерфейсу. Конструктор залишається тим самим, але зверніть увагу на методи для додаткових полів – кредитного ліміту та резервного рахунку. Ці методи повертають посилання на змінений екземпляр самого об’єкта. У AccountService.openAccount  Ви можете побачити, як це дозволяє клієнтському коду викликати методи в ланцюжку, так що код може бути прочитаний майже як звичайний текст.

При використанні рідинних інтерфейсів код, безсумнівно, виглядає не так, як ми звикли: він зручніше читається. Але у цього стилю є свої недоліки. Найголовніше, що він порушує варіацію принципу поділу команд-запитів (CQS), який вимагає, щоб метод був або командою, або запитом.

Зазвичай вона трактується так: команда повинна змінити стан, нічого не повертаючи, а запит повинен повернути відповідь, нічого не змінюючи. У прикладі вільного інтерфейсу методи with*  змінюють стан, але не використовують void як тип повернення. Це може бути не найсерйознішим порушенням, але ігнорувати його точно не варто.

Вільні інтерфейси хороші в ситуаціях, коли ви хочете конкретизувати об’єкт в момент його створення, крок за кроком (спочатку кредитний ліміт, потім резервний рахунок). Однак після кожного кроку об’єкт повинен залишатися послідовним. Рідин як таких недостатньо для забезпечення дотримання складних обмежень. Сам по собі цей підхід не підтримує обмеження, які охоплюють кілька властивостей одночасно, наприклад, якщо об’єкт повинен мати або кредитний ліміт, або резервний рахунок, але не обидва. У таких ситуаціях ми будемо використовувати вільні інтерфейси в поєднанні з шаблоном Builder. Але спочатку давайте розглянемо, як можуть виглядати складні обмеження.

Дотримання складних обмежень в коді

Складні обмеження, накладені на сутності, можуть застосовуватися до декількох атрибутів одночасно. Якщо один атрибут має певне значення, інший певним чином обмежений. Зміна першого атрибута впливає на обмеження другого. Такі нетривіальні обмеження часто приймають форму інваріантів або властивості, які повинні залишатися вірними протягом усього життєвого циклу об’єкта. Інваріанти необхідно спостерігати з моменту створення і під час всіх змін стану об’єкта[1].

У прикладі банківського рахунку є два необов’язкових атрибути: кредитний ліміт і резервний рахунок. Обидва вони можуть бути частиною складного обмеження. Розглянемо ситуацію, коли об’єкт повинен володіти одним з цих атрибутів, але не обома відразу (рис. 6.3).

Як старанний програміст, ви ніколи не повинні допускати, щоб в об’єкті порушувалися інваріанти. Ми вважаємо за краще збирати всі такі інваріанти в певний метод, який можна викликати, якщо ми хочемо переконатися, що об’єкт знаходиться в узгодженому стані. Зокрема, він викликається в кінці будь-якого публічного методу, перш ніж повернути управління абоненту. Лістинг 6.4 має метод checkInvariants що містить ці перевірки. Він гарантує, що встановлено або кредитний ліміт, або резервний рахунок, але не обидва одночасно. У разі невиконання цієї умови Validate.validState генерує IllegalStateException.

Немає  необхідності викликати цей метод ззовні Account, так як, з точки зору зовнішнього спостерігача, об’єкти цього класу завжди повинні бути узгодженими. Але навіщо потрібен метод, який перевіряє те, що завжди має бути істинним? Зверніть увагу на дрібницю в попередньому висловлюванні: інваріанти завжди повинні спостерігатися з точки зору зовнішнього коду.

Після того, як метод поверне управління абоненту за межі об’єкта, всі інваріанти повинні бути виконані. Однак в процесі експлуатації методу можуть виникати області, де інваріанти не задовольняються. Наприклад, при переході з кредитного ліміту на резервний рахунок може бути короткий проміжок часу, протягом якого кредитний ліміт вже знятий, а резервний рахунок ще не встановлений. Цей момент проілюстровано в Лістингу 6.5:  після скидання creditLimit, але до встановлення backbackAccount, об’єкт Account не виконує свої інваріанти.  Але, оскільки обробка ще не завершена, інваріанти не порушуються. Метод ще має шанс виправити ситуацію до повернення управління абоненту.

Поєднання методу перевірки з вільним інтерфейсом значно спрощує код. Однак в деяких ситуаціях цього недостатньо. Основним інструментом у вирішенні цієї проблеми є шаблон проектування «Будівельник», про який піде мова в наступному розділі.

Застосування складних обмежень за допомогою шаблону конструктора

Ви вже знаєте, що для створення об’єкта в узгодженому стані потрібно просто додати всі його обов’язкові поля в конструктор, а необов’язкові ініціалізувати за допомогою сеттерів. Але якщо обмеження стосуються кількох необов’язкових атрибутів, це не допоможе.

Давайте ще раз розглянемо приклад, в якому банківський рахунок повинен мати або кредитний ліміт, або резервний рахунок, але не і те, і інше. Ми хотіли б створити об’єкт поетапно, але таким чином, щоб задовольнити всі обмеження, перш ніж до нього буде звернено за допомогою іншого коду. Це саме те, що робить шаблон Builder.

Основна ідея цього шаблону полягає в тому, щоб інкапсулювати складну процедуру створення сутності всередині іншого об’єкта, будівельника. На рис. На рисунку 6.4 показано, як такий підхід дозволяє приховати виробництво автомобіля до його повної готовності. Коли збірка завершена, автомобіль стає доступним.

Повернемося до прикладу банківського рахунку і подивимося, як він виглядає в коді. Лістинг 6.6 показує код клієнта. Спочатку створюємо  об’єкт AccountBuilder, робимо з ним деякі маніпуляції (просимо створити всередині себе банківський рахунок) і, задоволені результатом, запитуємо   готовий аккаунт у AccountBuilder.

Якщо нам потрібен резервний обліковий запис, ми зателефонуємо зFallbackAccount перед завершенням збірки.  Цей шаблон також добре справляється зі складними випадками. Вам просто потрібно виконати додаткові маніпуляції з конструктором, налаштувавши продукт перед викликом збірки, щоб отримати кінцевий результат. Немає необхідності в великій кількості конструкторів або перевантажених методів. Ви можете зробити свій код ще більш елегантним, надавши  об’єкт AccountBuilder з  вільним інтерфейсом, так що  метод withCreditLimit  повертає посилання на сам конструктор:

Найскладнішою частиною цієї моделі проектування є реалізація будівельника. AccountBuilder повинен мати можливість маніпулювати об’єктом облікового запису, навіть якщо він не узгоджується (хоча спочатку цього слід уникати). Але пам’ятайте: будівельник не повинен залишати виріб в суперечливому стані, так як він більше не зможе працювати з ним ззовні.

Класичним рішенням цієї проблеми є розміщення обох класів (Account і  його конструктор) в одному модулі, а потім надання Аккаунту двох інтерфейсів  : один буде видно зовнішньому коду, а інший зможе працювати з конструктором. Це дає результати, але рано чи пізно вам набридне робити всі ці дії.

Лістинг 6.7 показує, як вирішити цю дилему за допомогою внутрішніх класів Java. Клас Builder  розміщується всередині  класу Account і  має доступ до його внутрішніх механізмів без необхідності використання будь-яких спеціальних методів. Оскільки Builder є статичним класом, він може створити неповний екземпляр  облікового запису за допомогою приватного конструктора і працювати з ним, поки зовнішній клієнт не викличе  метод  збіркиотримати готовий  об’єкт Account.

Роблячи  конструктор Account  приватним, ми гарантуємо, що банківський рахунок може бути створений лише за допомогою класу Builder та  методу збірки. Під час процесу створення об’єкт облікового запису може бути суперечливим, але це нормально, оскільки доступ до нього має лише будівельник, який його робить. Однак на момент повернення банківського рахунку від забудовника повинні бути задоволені всі інваріанти.

Коли  метод build() повертає абоненту Account, конструктор  повинен позбутися посилання на кінцевий результат (Account  ). Це робиться для того, щоб виклик збірки знову  не  повертав  чергове посилання на той же  екземпляр облікового запису. Після завершення робіт  будівельник самознищується. Теоретично, з точки зору об’єктно-орієнтованого програмування, поняття внутрішніх класів трохи дивно, але в У даній конкретній ситуації він виявляється цілком практичним.

Фреймворки ORM і складні обмеження

Маючи справу з такими складними сутностями, як описано раніше (з обмеженнями, що охоплюють різні атрибути), необхідно подбати про їх зв’язок з базою даних. Фреймворки ORM, такі як JPA і Hibernate, дозволяють відображати об’єкти домену безпосередньо в базі даних.

Якщо домен невеликий, а доступ до бази даних отримує лише один застосунок, яким ви маєте повний контроль, можна вважати базу даних частиною довіреної області. У цьому випадку можна припустити, що вміст бази даних відповідає вашим бізнес-правилам і може бути безпечно завантажено в додаток без валідації. Це дозволяє вам зробити ваш код простим, але ви також повинні мати жорсткий контроль над своїми даними.

Якщо предметна область більш велика або ви не можете гарантувати, що хтось інший не отримає доступ до бази даних, рекомендуємо розглядати базу даних як окрему систему і перевіряти її вміст при завантаженні. Якщо безпосередньо зіставити сутності вашого домену з базою даних за допомогою ORM, можуть виникнути певні нюанси. Це здійсненно, але радимо почитати про фреймворки зберігання даних.

Якщо ви працюєте зі складними обмеженнями, то після завантаження вмісту бази даних потрібно переконатися в тому, що інваріанти дотримані. Саме для цього і призначений метод checkInvariants. Досить подбати про те, щоб він викликався при завантаженні. Ви можете зробити це, анотувавши @PostLoad, як показано в списку 6.8. Він працює як в JPA, так і в Hibernate.

Коли використовувати той чи інший метод створення

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

Шаблон Builder дозволяє розбити процес створення на багато викликів, але ми рекомендуємо зробити життєвий цикл об’єкта конструктора якомога коротшим — це головне обмеження всіх трьох підходів. Що ховається за словами «максимально коротко» на практиці залежить від ситуації, але в веб-системах ми рекомендуємо завершити створення в тому ж запиті або при поверненні відповіді клієнту. Якщо процедура створення настільки складна, що потрібно взаємодіяти з клієнтом кілька разів, краще ввести окремий стан ініціалізації всередині сутності.

Після того, як ви створите послідовну сутність, ви повинні переконатися, що її стан не змінюється. Його цілісність не повинна бути порушена, інакше його безпека буде залежати від коду виклику.

Цілісність сутності

Сутності, створені за всіма правилами бізнесу – це добре. Але буде ще краще, якщо вони продовжать дотримуватися цих правил. Однією з переваг проектування сутностей як об’єктів є те, що вся важлива бізнес-логіка може бути інкапсульована поруч з даними. На цьому етапі ви повинні переконатися, що ці дані не просочуються вашим клієнтам і що клієнти не можуть змінити їх в обхід бізнес-правил. Ось що  Що фахівці в області інформаційної безпеки називають захистом цілісності інформації.

Якщо дані сутностей можуть бути змінені без їх контролю, будьте впевнені, що рано чи пізно станеться помилка або цілеспрямована атака, яка призведе до зміни даних з порушенням правил. У цьому випадку погана цілісність суб’єктів стає ризиком для безпеки. Основна техніка забезпечення цілісності сутностей полягає в тому, щоб ніколи не робити змінюваний стан доступним ззовні.

Добувачі та сетери

Більшість розробників погодяться, що змінюване поле публічних даних є поганою ідеєю, яка майже табуйована для обговорення. Але, на наш подив, багато розробників надають безкоштовний доступ до своїх полів за рахунок необмеженої кількості геттерів і сеттерів. Можливо, з естетичної точки зору такий підхід здається більш елегантним, але в плані безпеки він нічим не краще. Давайте розберемося, чим загрожує використання геттеров і сеттерів.

У Лістингу 6.9 платний атрибут, що належить замовленню, має форму приватного поля. Але нею можна маніпулювати ззовні, як якщо б нею не було захищено, оскільки вона має необмеженого сетера і добувача.

Давайте трохи попрацюємо над цим кодом. Ми хочемо захистити  платне поле від довільних  змін. Дійсно хорошим першим кроком було зробити його приватним, як  логічне платне поле даних в  класі Order. Але захист самого поля не допоможе, якщо дані доступні для довільних модифікацій через сетер. Іноді у сетерів і геттерів є особлива логіка, яка дозволяє підвищити безпеку шляхом інкапсуляції поведінки. Але Часто вони забезпечують необмежений доступ до полів даних.

Які аспекти поведінки має сенс інкапсулювати? Повернемося  до платної сфери. Чи варто дозволяти необмежені зміни його вартості? Напевно, ні. У цьому випадку змінити його можна тільки з false на true і тільки після отримання гонорару. Не існує бізнес-сценарію, в якому відбуваються протилежні зміни.

Щоб зробити це архітектурне рішення більш безпечним, ми можемо обмежити зміну даних. Очевидним способом досягнення цього є використання замість  setPaid, методу, розробленого спеціально для позначення замовлення як оплаченого:

Тепер ми можемо бути впевнені, що платний атрибут  буде змінений тільки відповідно до бізнес-правил. Слід зазначити, що інкапсуляція полягає в об’єднанні даних та правилах/інтерпретаціях, які їх стосуються, а не просто забороні прямого доступу до поля.

Відмова від відокремлення змінюваних об’єктів

Суб’єкт господарювання повинен якимось чином відокремити свої дані від свого середовища. Сутність замовлення в  лістингу 6.9 рано чи пізно повинна буде надати адресу відправки. Найбезпечніший спосіб зробити це – відокремити примітиви домену, так як вони незмінні (це було пояснено в главі 5).

Розділення об’єкта, який можна змінити, несе ризик того, що посилання на нього буде використано для зміни стану, який він представляє. У Лістингу 6.10 атрибут Person.name представлений незмінним рядком, а атрибут Person.title є  модифікованим полем типу StringBuffer. Хоча для доступу до них використовується схожий код, між ними є принципова різниця. При використанні  незмінного атрибута  name, об’єкт Person зберігає  свою цілісність. Однак  атрибут title, який ви змінюєте,  дозволяє ненавмисно змінити вигляд, за допомогою якого  Особа  зберігає свій стан. Це порушує цілісність  об’єкта Person.

Саме тому, що змінювані об’єкти можуть бути змінені в полях даних, ми рекомендуємо замість цього використовувати незмінні примітиви домену не тільки для полів, але також для аргументів і типів повернення.

Якщо з якихось причин вам доводиться працювати з об’єктом, який ви змінюєте, ви можете скористатися одним прийомом: перед тим, як повернути посилання на інкапсульований об’єкт з методу, клонуйте його. Таким чином, ваш модифікований об’єкт не буде змінено кимось іншим. Якщо зовнішній код використовує повернуте посилання, він змінить лише копію об’єкта, а не оригінал. Лістинг 6.11 показує  як цю техніку можна застосувати до java.util.Date.

У більшості випадків уникнути поділу змінюваних об’єктів не так вже й складно, так як багато сучасних бібліотек надають хороші незмінні класи. Однак є один виняток, який часто викликає проблеми: колекції.

Забезпечення цілісності колекцій

Навіть якщо клас добре розроблений і не пропускає змінювані об’єкти, існує одна проблемна область — набори, такі як списки або набори. Наприклад, об’  єкт замовлення   в книжковому інтернет-магазині може містити список товарів замовлення, кожен елемент якого описує придбану книгу та кількість її копій. Цей список зберігається як  поле даних List<OrderLine> orderItems . Такі колекції мають кілька нюансів, на яких хотілося б акцентувати вашу увагу.

По-перше, цей список явно не повинен бути доступний ззовні. Якщо  поле OrderItems  загальнодоступне, будь-хто може підставити в нього свій список. Ви також не можете використовувати такі установки, як void setOrderItems(List<OrderLine> orderItems), які роблять те саме. Замість того, щоб експортувати колекцію назовні, дозволяючи клієнтам працювати зі списком, вам потрібно інкапсулювати те, що відбувається всередині  сутності.

Наприклад, щоб додати елементи до  списку, потрібно мати недійсний метод addOrderItem(OrderLine orderItem). Якщо потрібно дізнатися загальну кількість товарів в замовленні, не варто повертати список клієнту, щоб він міг сам розрахувати суму – розрахунки необхідно виконувати всередині  методу nrItems(). Образно кажучи, сутність повинна залучати функціональність і поглинати обчислення. Це значно покращить узгодженість і цілісність правил бізнесу з часом Дані. Можливо, вам взагалі не потрібно ділитися списком, оскільки всі операції з ним тепер знаходяться всередині сутності.

Якщо зовнішній код дійсно повинен працювати зі списком замовлених товарів, цей список потрібно якось надати – наприклад, за допомогою кнопки

List<OrderItem> orderItems(). Але це повертає нас до знайомої проблеми. Список є змінюваним об’єктом, і ніщо не заважає клієнту отримати посилання на нього і змінити його вміст, додавши нові товари або видаливши існуючі. Лістинг 6.12 показує  порожній метод addFreeShipping(Order), який працює безпосередньо зі списком замовлених товарів.

У цьому прикладі метод orderItems  повертає посилання на список, у якому зберігаються впорядковані елементи. Клієнт безпосередньо змінює список, а об’єкт Order  не може контролювати зміни. Такі ситуації зустрічаються досить часто, і це явна діра в безпеці.

Щоб захистити це архітектурне рішення, потрібно переконатися, що дані, повернуті сутністю, є незмінною копією. Для полів даних, які мають примітивні типи, зробити це не складно. У попередньому прикладі  логічний метод isPaid()  повернув копію логічного значення, збереженого в полі. Приймаюча сторона може робити з нею все, що захоче, жодним чином не впливаючи на Замовлення. Захист поля Список<Елемент_замовлення> orderItems(), слід подбати про те, щоб повернута копія не могла бути використана для внесення змін до внутрішнього списку. Список можна клонувати так само, як ми це робили  з Date, але у випадку з колекціями варто скористатися спеціальним трюком, заснованим на об’єкті проксі-сервера, доступному тільки для читання.

Для клонування колекцій замість методу клону зазвичай використовуються так звані конструктори копій. Кожен клас у бібліотеці колекції Java має конструктор, який приймає іншу колекцію як аргумент і створює її копію. Лістинг 6.13 показує, як це працює з методом  orderItems, який повертає копію списку замовлених елементів.

Код, який викликає orderItems, отримує копію колекції, і будь-які зміни вносяться до неї, а не до  списку всередині замовлення. Недоліком такого підходу є те, що викликаючий код ще може працювати зі списком і думати, що він змінює стан. Це може привести до помилок програмного забезпечення, які важко виявити. Але, як вже було сказано, є один цікавий прийом по роботі з колекціями.

Клас утиліти Collections  містить багато корисних статичних методів, один з яких: статичний  <T> List<T> unmodifiableList(List<? розширює T> list)

Лістинг 6.14 ілюструє його використання. Цей метод повертає проксі-об’єкт, доступний лише для читання, призначений для роботи з вихідним списком. Будь-яка спроба викликати метод змінення з цього об’єкта призведе до  UnsupportedOperationException.

Хочемо попередити: незважаючи на те, що  список не може бути змінений ззовні, його не можна вважати незмінним. Список елементів замовлення  все ще можна змінювати всередині  об’єкта Order  — наприклад, ми можемо додати до нього новий елемент. Такий підхід перешкоджає клієнтам вносити зміни, але не робить список незмінним. Як би там не було, скопіювавши список товарів або повернувши незмінний об’єкт проксі-сервера,  Ви забезпечили цілісність списку. Його не можна змінити зовні, випадково чи навмисно, і саме цього ми хотіли досягти, коли зробили поле даних приватним.

Цим ми захистили вміст списку, який складається з посилань на об’єкти. Зовнішній код не може видаляти наявні посилання або додавати нові. Тепер потрібно переконатися, що міняти самі об’єкти неможливо. Найкращий спосіб зробити це – зробити елементи списку незмінними, як описано вище в цьому розділі.

Проблема з елементами списку, які можна редагувати

Інтернет-магазин металу зберігав ціну кожного товару, проданого в списку. Сам цей список не міг бути змінений ззовні. Але її елементи не були захищені. Клієнт може додати 100 кг мідного дроту в кошик за 9 доларів за 1 кг, дотримуючись стандартної процедури. Але потім він міг змінити ціну на 0,01 долара за 1 кг. Цілісність списку не допоможе, якщо не забезпечити цілісність його елементів.

Цей розділ був зосереджений на безпечному представленні змінюваного стану за допомогою сутностей. Зокрема, ми детально розглянули важливі аспекти того, як створювати суб’єкти господарювання в державі, яка відповідає обмеженням бізнесу, і як зберегти їх цілісність і послідовність. Знайомі тут шаблони макета наведено в таблиці. 6.1.

Резюме

  • Для роботи зі станами, що змінюються, варто використовувати сутності.

  • Сутності мають бути узгодженими у момент створення.

  • Конструктори, які не приймають аргументи, небезпечні.

  • За допомогою шаблону «Будівельник» можна створювати сутності із складними обмеженнями.

  • Ви повинні забезпечувати цілісність атрибутів під час доступу до них.

  • Приватне поле даних з необмеженими геттером і сеттером нічим не без небезпечнішого публічного поля.

  • Ви повинні уникати поділу змінних об’єктів і використовувати замість них доменні примітиви.

  • Доступ потрібно відкривати не до всієї колекції, а до якоїсь її корисної якості.

  • Колекції можна захищати, надаючи доступ до їх версій, які не підлягають зміні.

  • Ви повинні подбати про те, щоб дані в колекції не можна було змінювати зовні.

Ми використовували матеріали з книги “Безпека by design”, які  написали Дэн Берг Джонсон, Дэниел Деоган, Дэниел Савано.

Інші статті по темі
ОсвітаСамонавчання
Читати далі
Безпека by design. Частина 1. (Роль проектування у безпеці)
Проектування відіграє надзвичайно важливу роль у гарантуванні безпеки в різних сферах, від технологій до інфраструктури та бізнесу. Цей процес є фундаментальною складовою для створення рішень, які ефективно запобігають загрозам, забезпечують конфіденційність та зберігають цінності. Розглянемо ключові аспекти ролі проектування у забезпеченні безпеки.
247
0
ОсвітаСамонавчання
Читати далі
Безпека by design. Частина 2. (Антракт: анти-“Гамлет”)
У цій статті описується роль глибокого моделювання у забезпеченні безпеки інформаційних систем та бізнес-цілісності. Розглядаються ризики поверхового моделювання, які можуть призвести до недостатнього рівня захисту та дефективної безпеки.
187
0
ОсвітаСамонавчання
Читати далі
Безпека by design. Частина 3. (Основні концепції предметно-орієнтованого проектування)
Основні Концепції Предметно-Орієнтованого Проектування полягає в поясненні важливих концепцій цього підходу, включаючи абстракцію, глибокий аналіз предметної області та покращення процесів розробки. Виокремлені аспекти покращення якості та продуктивності роботи, що досягаються завдяки використанню предметно-орієнтованого проектування.
206
0
ОсвітаСамонавчання
Читати далі
Безпека by design. Частина 4. (Концепції програмування, що сприяють безпеки)
Застосування цих концепцій у програмуванні сприяє підвищенню рівня інформаційної безпеки. Вони допомагають уникнути загроз та вразливостей, забезпечуючи захист даних та надійність програмних продуктів.
184
0
ОсвітаСамонавчання
Читати далі
Безпека by design. Частина 5. (Доменні притиви)
У цій частині. Як доменні примітиви допомагають писати безпечний код. Боротьба з витоком даних за допомогою об'єктів одноразового читання. Поліпшення сутностей з допомогою доменних примітивів. Ідеї, запозичені із аналізу помічених даних.
184
0
ОсвітаСамонавчання
Читати далі
Безпека by design. Частина 8. (Роль процесу доставки коду у безпеці)
У цій частині. Модульні випробування, спрямовані на безпеку. Перемикачі функціональності з погляду безпеки. Написання автоматизованих тестів безпеки. Чому важливими є тести доступності. Як неправильна конфігурація призводить до проблем безпеки.
215
0
Знайшли помилку?
Якщо ви знайшли помилку, зробіть скріншот і надішліть його боту.