Вступ
Хотілося б у цьому огляді обговорити таку тему, як безпека веб-додатків. У Java є кілька технологій, які забезпечують безпеку:-
" Java SE Platform Security Architecture ", докладніше про яку можна прочитати в Guide від Oracle: " JavaTM SE Platform Security Architecture ". Ця архітектура описує те, як необхідно захищати наші Java-додатки в Java SE середовищі виконання. Але це не є темою нашої сьогоднішньої розмови.
-
" Java Cryptography Architecture " - розширення (Java Extension), яке визначає шифрування даних. Докладніше про це розширення можна прочитати на JavaRush в огляді " Java Cryptography Architecture: Перше знайомство " або в Guide від Oracle: " Java Cryptography Architecture (JCA) Reference Guide ".
JAAS
JAAS - це розширення Java SE і воно описано в документі " Java Authentication and Authorization Service (JAAS) Reference Guide ". Як випливає з назви технології, JAAS описує те, як потрібно виконувати аутентифікацію та авторизацію:-
" Аутентифікація ": у перекладі з грецької "authentikos" означає "реальний, справжній". Тобто автентифікація – це автентифікація. Що той, хто проходить автентифікацію, справді той, за кого себе видає.
-
" Авторизація " : у перекладі з англійської означає " дозвіл " . Тобто авторизація — це контроль доступу після успішного проходження аутентифікації.
- його посвідчення водія, як подання людини як учасника дорожнього руху
- його паспорт, як подання людини як громадянина своєї країни
- його закордонний паспорт як подання людини як учасника міжнародних відносин
- його читацький квиток у бібліотеці, як подання людини як прикріпленого до бібліотеки читача
Веб-додаток
Отже, нам знадобиться веб-додаток. У його створенні нам допоможе система автоматичного збирання проектів Gradle. Завдяки використанню Gradle ми зможемо за допомогою виконання невеликих команд збирати Java проект у потрібному нам форматі, створювати автоматично потрібну структуру каталогів та багато іншого. Докладніше про Gradle можна прочитати в короткому огляді: " Коротке знайомство з Gradle " або в офіційній документації " Gradle Getting Started ". Нам потрібно виконати ініціалізацію проекту (Initialization), а для цього в Gradle є спеціальний плагін: " Gradle Init Plugin " (Init - скорочення від Initialization, легко запам'ятати). Щоб скористатися цим плагіном, виконаємо в командному рядку команду:gradle init --type java-application
Після успішного виконання у нас з'явиться проект Java. Відкриємо тепер на редагування білд-скрипт нашого проекту. Білд скрипт - це файл з назвою build.gradle
, який описує нюанси складання (білда) програми. Звідси й назва така, білд скрипт. Можна сказати, що це скрипт складання проекту. Gradle — це універсальний інструмент, базові можливості якого розширюються завдяки плагінам. Тому, насамперед звернемо увагу на блок "plugins" (плагіни):
plugins {
id 'java'
id 'application'
}
За замовчуванням Gradle, відповідно до вказаного нами --type java-application
, виставив набір деяких базових плагінів (core plugins), тобто тих плагінів, які входять у постачання самого Gradle. Якщо перейти на сайті gradle.org у розділ "Docs" (тобто документація), то ліворуч у списку тем у розділі "Reference" бачимо розділ " Core Plugins ", тобто. розділ із описом цих самих базових плагінів. Давайте оберемо саме ті плагіни, які нам потрібні, а не ті, що нам згенерував Gradle. Відповідно до документації, " Gradle Java Plugin " забезпечує базові операції з Java кодом, такі як компіляція вихідного коду. Також, згідно з документацією, " Gradle application pluginзабезпечує нас засобами для роботи з "executable JVM application", тобто з java додатком, які можна запустити як самостійний додаток (наприклад, консольний додаток або додаток з власним UI). Виходить, що плагін "application" нам не потрібен, тому що нам не потрібна самостійна програма, нам потрібна веб-додаток. Видалимо його. А так само налаштування "mainClassName", яка відома тільки цьому плагіну. Далі, в тому ж розділі "Packaging and distribution", де було наведено посилання на документацію по Application Plugin, є посилання на Gradle War Plugin ., як сказано в документації, надає підтримку створення Java веб-застосунків у форматі war. У форматі WAR означає, що замість JAR архіву буде створено архів WAR. Здається, це те, що нам потрібне. Крім того, як сказано в документації, "The War plugin extends the Java plugin". Тобто ми можемо замінити плагін java на плагін war. Отже, наш блок плагінів у результаті матиме такий вигляд:
plugins {
id 'war'
}
Так само в документації до Gradle War Plugin сказано, що плагін використовує додатковий Project Layout. Layout з англійської перекладається як розташування. Тобто war plugin за замовчуванням розраховує на існування деякого розташування файлів, які він використовуватиме для своїх завдань. Використовувати для зберігання файлів веб-програми він буде наступний каталог: src/main/webapp
Поведінка плагіна описано так:
web.xml
- це "Deployment Descriptor" або "описувач розгортання". Це такий файл, який описує, як потрібно налаштувати наш веб-додаток для роботи. У цьому файлі вказується, які запити оброблятиме наша програма, налаштування безпеки та багато іншого. За своєю суттю він чимось схожий на manifest файл із JAR файлу (див. Working with Manifest Files: The BasicsManifest файл розповідає, як працювати з Java Application (тобто з JAR архівом), а web.xml розповідає, як працювати з Java Web Application (тобто з WAR архівом). Саме поняття "Deployment Descriptor" виникло не саме собою, а описано в документі " Servlet API SpecificationТобто мільйон програмістів довелося б писати для однієї і тієї ж мети код знову і знову. Тому за частину взаємодії з користувачем та за обмін даними відповідає веб-сервер, а за формування цих даних відповідає веб-додаток та розробник. Щоб зв'язати ці дві частини, тобто. веб-сервер і додаток, необхідний договір їх взаємодії, тобто. за якими правилами вони це робитимуть. Щоб якось описати контракт, як має виглядати взаємодія між веб-програмою та веб-сервером і придуманий Servlet API. Цікаво, що навіть якщо ви використовуєте фреймворки на кшталт Spring, то під капотом все одно працює Servlet API. Тобто ви використовуєте Spring, а Spring за Вас працює із Servlet API. Виходить, що наш проект веб-застосування повинен залежати (depends on) від Servlet API. І тут Servlet API буде залежністю (dependency). Як ми знаємо, Gradle зокрема дозволяє декларативним чином описувати залежності проекту. А те, як можна управляти залежностями, описують плагіни. Наприклад, Java Gradle Plugin вводить спосіб управління залежностями "testImplementation", який каже, що така залежність потрібна лише для тестів. А ось Gradle War Plugin додає спосіб управління залежностями "providedCompile", який каже, що така залежність не буде включена до WAR архіву нашого веб-додатку. Чому ми не включаємо Servlet API у наш WAR архів? Тому що Servlet API буде надано нашому веб-застосунку самим веб-сервером. Якщо веб-сервер надає Servlet API, такий сервер називають контейнер сервлетів. Тому надати нам Servlet API – це обов'язок веб-сервера, а наш обов'язок надати ServletAPI лише на момент компіляції коду. Тому йprovidedCompile
. Таким чином, блок залежностей (dependencies) матиме такий вигляд:
dependencies {
providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
testImplementation 'junit:junit:4.12'
}
Отже, повернемося до файлу web.xml. За замовчуванням Gradle не створює ніякого Deployment Descriptor, тому нам потрібно зробити це самостійно. Створимо каталог src/main/webapp/WEB-INF
, а в ньому створимо XML-файл з назвою web.xml
. Тепер давайте відкриємо саму специфікацію "Java Servlet Specification" та розділ " CHAPTER 14 Deployment Descriptor ". Як сказано в "14.3 Deployment Descriptor", XML документ Deployment Descriptor'а описаний схемою http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd . XML схема описує, з яких елементів може складатися документ, у порядку вони мають йти. Які обов'язкові, а які ні. Загалом описує структуру документа і дозволяє перевірити, чи правильно XML документ складено. Тепер скористаємося прикладом із розділу "", але схему слід зазначити для версії 3.1, тобто.
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd
Наш порожній web.xml
буде виглядати так:
<?xml version="1.0" encoding="ISO-8859-1"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>JAAS Example</display-name>
</web-app>
Давайте тепер опишемо сервлет, який ми захищатимемо за допомогою JAAS. Раніше Gradle згенерував клас App. Давайте перетворимо його на сервлет. Як сказано в специфікації в " CHAPTER 2 The Servlet Interface ", щоб " For most purposes, Developers буде extend HttpServlet до implement their servlets ", тобто щоб зробити клас сервлетом необхідно успадкувати цей клас від HttpServlet
:
public class App extends HttpServlet {
public String getGreeting() {
return "Secret!";
}
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
response.getWriter().print(getGreeting());
}
}
Як ми говорабо, Servlet API — це контракт між сервером і нашим веб-додатком. Це контракт дозволяє описати, що коли користувач звернутися до сервера, сервер сформує запит від користувача як об'єкт HttpServletRequest
і передасть його в сервлет. А також надасть сервлету об'єкт HttpServletResponse
, щоб сервлет зміг записати відповідь для користувача. Коли сервлет відпрацює, сервер зможе на основі HttpServletResponse
надати відповідь користувачеві. Тобто сервлет безпосередньо не спілкується з користувачем, а лише із сервером. Щоб сервер знав, що у нас є сервлет і для яких запитів його потрібно задіяти, потрібно про це серверу розповісти в деплоймент дескрипторі:
<servlet>
<servlet-name>app</servlet-name>
<servlet-class>jaas.App</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>app</servlet-name>
<url-pattern>/secret</url-pattern>
</servlet-mapping>
В даному випадку всі запити /secret
будуть адресаовані нашому одному сервлету на ім'я app
, яке відповідає класу jaas.App
. Як ми раніше говорабо, веб-додаток може бути розгорнутий лише на веб-сервері. Веб-сервер можна встановити окремо (standalone). Але для цілей цього огляду підійде альтернативний варіант - запуск на вбудованому (embedded) сервері. Це означає, що сервер буде створено та запущено програмно (за нас це зробить плагін), а разом з цим на ньому буде розгорнуто наш веб-додаток. Система складання Gradle дозволяє для цих цілей використовувати плагін " Gradle Gretty Plugin ":
plugins {
id 'war'
id 'org.gretty' version '2.2.0'
}
Крім того, плагін Gretty має хорошу документацію . Почнемо з того, що плагін Gretty дозволяє перемикатися між різними веб-серверами. Докладніше це описано в документації: " Switching between servlet containers ". Переключимося на Tomcat, т.к. він є одним з найпопулярніших у використанні, а також має гарну документацію та безліч прикладів та розібраних проблем:
gretty {
// Переключаемся с дефолтного Jetty на Tomcat
servletContainer = 'tomcat8'
// Укажем Context Path, он же Context Root
contextPath = '/jaas'
}
Тепер ми можемо виконати "gradle appRun" і тоді наш веб-додаток буде доступний за адресаою http://localhost:8080/jaas/secret
Аутентифікація
Установки автентифікації часто складаються з двох частин: настройок на стороні сервера та настройок на стороні веб-програми, яка працює на цьому сервері. Установки безпеки веб-програми не можуть не взаємодіяти з налаштуваннями безпеки веб-сервера хоча б через те, що веб-програма не може не взаємодіяти з веб-сервером. Ми з Вами недаремно перейшли на Tomcat, т.к. Tomcat має добре описану архітектуру (див. " Apache Tomcat 8 Architecture "). З опису цієї архітектури видно, що Tomcat як веб-сервер представляє веб-додаток як деякий контекст, який і називають Tomcat Context". Цей контекст дозволяє кожному веб-додатку мати свої налаштування, ізольовані від інших веб-додатків. Крім того, веб-додаток може впливати на налаштування цього контексту. Гнучко і зручно. Для більш глибокого розуміння рекомендується до читання стаття " " і розділ документації Tomcat " The Context Container ". Як вище було сказано, наш веб-додаток може впливати на Tomcat Context нашої програми за допомогою файлу/META-INF/context.xml
. І однією з дуже важливих налаштувань, на яку ми можемо вплинути, є Security Realms. Security Realms- Це деяка "область безпеки". Область, для якої вказано певні параметри безпеки. Відповідно, використовуючи Security Realm, ми застосовуємо налаштування безпеки, визначені для цього Realm. Security Realms управляються контейнером, тобто. веб-сервером, а не нашим веб-програмою. Ми можемо тільки розповісти серверу, яку з областей безпеки потрібно поширити на наш додаток. Документація Tomcat у розділі " The Realm Component " описує Realm як набір даних про користувачів та їх ролі для виконання аутентифікації. Tomcat надає набір різних реалізацій Security Realm'ів, одним з яких є " Jaas Realm ". Розібравшись трохи з термінологією, давайте опишемо Tomcat Context у файлі /META-INF/context.xml
:
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<Realm className="org.apache.catalina.realm.JAASRealm"
appName="JaasLogin"
userClassNames="jaas.login.UserPrincipal"
roleClassNames="jaas.login.RolePrincipal"
configFile="jaas.config" />
</Context>
appName
- Ім'я додаток (application name). Tomcat спробує зіставити це ім'я з іменами, вказаними у configFile
. configFile
- Це "login configuration file". Його приклад можна побачити в документації JAAS: " Appendix B: Example Login Configurations ". Крім того, важливо, що файл буде шукатися спочатку в ресурсах. Тому наш веб-додаток може сам надати цей файл. Атрибути userClassNames
і roleClassNames
містять вказівку на класи, які є принципал користувача. JAAS поділяє поняття "користувач" і "роль" як два різні java.security.Principal
. Давайте опишемо вказані вище класи. Створимо найпростішу реалізацію для принципала користувача:
public class UserPrincipal implements Principal {
private String name;
public UserPrincipal(String name) {
this.name = name;
}
@Override
public String getName() {
return name;
}
}
Таку ж реалізацію повторимо і для RolePrincipal
. Як Ви могли побачити по інтерфейсу, головне для Principal – зберігати та повертати деяке ім'я (або ID), що представляють Principal. Тепер у нас є Security Realm, є класи принципалів. Залишилося заповнити файл з атрибуту " configFile
", він же login configuration file
. Його опис можна знайти в документації до Tomcat: " The Realm Component ".
\src\main\resources\jaas.config
Задамо вміст файлу:
JaasLogin {
jaas.login.JaasLoginModule required debug=true;
};
Варто звернути увагу, що тут і використано context.xml
однакове ім'я. Таким чином Security Realm зіставляється з LoginModule. Отже, Tomcat Context повідомив, які класи представляють принципали, а також який LoginModule використовувати. Нам залишилося лише реалізувати цей LoginModule. LoginModule - це, мабуть, одна з найцікавіших речей у JAAS. У розробці LoginModule нам допоможе офіційна документація: " Java Authentication and Authorization Service (JAAS): LoginModule Developer's Guide ". Давайте реалізуємо логін модуль. Створимо клас, який реалізує інтерфейс LoginModule
:
public class JaasLoginModule implements LoginModule {
}
Спочатку опишемо метод ініціалізації LoginModule
:
private CallbackHandler handler;
private Subject subject;
@Override
public void initialize(Subject subject, CallbackHandler callbackHandler, <String, ?> sharedState, Map<String, ?> options) {
handler = callbackHandler;
this.subject = subject;
}
Даний метод збереже Subject
, який ми далі аутентифікуємо та заповнимо інформацією про принципали. А так само збережемо для подальшого використання CallbackHandler
, який нам передають. За допомогою CallbackHandler
ми зможемо запитати різну інформацію про суб'єкта автентифікації трохи пізніше. Докладніше CallbackHandler
можна прочитати у відповідному розділі документації: " JAAS Reference Guide: CallbackHandler ". Далі виконується метод login
для аутентифікації Subject
. Це перша фаза аутентифікації:
@Override
public boolean login() throws LoginException {
// Добавляем колбэки
Callback[] callbacks = new Callback[2];
callbacks[0] = new NameCallback("login");
callbacks[1] = new PasswordCallback("password", true);
// При помощи колбэков получаем через CallbackHandler логин и пароль
try {
handler.handle(callbacks);
String name = ((NameCallback) callbacks[0]).getName();
String password = String.valueOf(((PasswordCallback) callbacks[1]).getPassword());
// Далее выполняем валидацию.
// Тут просто для примера проверяем определённые значения
if (name != null && name.equals("user123") && password != null && password.equals("pass123")) {
// Сохраняем информацию, которая будет использована в методе commit
// Не "пачкаем" Subject, т.к. не факт, что commit выполнится
// Для примера проставим группы вручную, "хардкодно".
login = name;
userGroups = new ArrayList<String>();
userGroups.add("admin");
return true;
} else {
throw new LoginException("Authentication failed");
}
} catch (IOException | UnsupportedCallbackException e) {
throw new LoginException(e.getMessage());
}
}
Важливо, що login
ми не повинні змінювати об'єкт Subject
. Такі зміни мають відбуватися лише у методі підтвердження commit
. Далі ми маємо описати метод підтвердження успішної аутентифікації:
@Override
public boolean commit() throws LoginException {
userPrincipal = new UserPrincipal(login);
subject.getPrincipals().add(userPrincipal);
if (userGroups != null && userGroups.size() > 0) {
for (String groupName : userGroups) {
rolePrincipal = new RolePrincipal(groupName);
subject.getPrincipals().add(rolePrincipal);
}
}
return true;
}
Може здатися дивним поділ методу login
та commit
. Але річ у тому, що login модулі можуть бути об'єднані. І для успішної аутентифікації може бути необхідно, щоб успішно відпрацювало кілька логін модулів. І лише якщо відпрацюють усі потрібні модулі – тоді зберігати зміни. Це другий фазою аутентифікації. Завершимо методами abort
і logout
:
@Override
public boolean abort() throws LoginException {
return false;
}
@Override
public boolean logout() throws LoginException {
subject.getPrincipals().remove(userPrincipal);
subject.getPrincipals().remove(rolePrincipal);
return true;
}
Метод abort
викликається тоді, коли завершилася невдачею перша фаза автентифікації. Метод logout
викликається при виході із системи. Реалізувавши свій Login Module
і настроївши Security Realm
, Тепер нам треба вказати в web.xml
той факт, що ми хочемо використовувати певний Login Config
:
<login-config>
<auth-method>BASIC</auth-method>
<realm-name>JaasLogin</realm-name>
</login-config>
Ми вказали ім'я нашого Security Realm і вказали Authentication Method - BASIC. Це один із видів аутентифікації, описаних у Servlet API у розділі " 13.6 Authentication ". Залишився п
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ