JavaRush /Java блог /Random UA /JAAS - Введення в технологію (частина 1)
Viacheslav
3 рівень

JAAS - Введення в технологію (частина 1)

Стаття з групи Random UA
Безпека доступу реалізована в Java досить давно і архітектура забезпечення цієї безпеки називається JAAS - Java Authentication and Authorization Service. Цей огляд спробує розкрити таємницю, що таке автентифікація, авторизація і до чого тут JAAS. Як JAAS товаришує із Servlet API, а де у них у взаєминах проблеми.
JAAS - Введення в технологію (частина 1) - 1

Вступ

Хотілося б у цьому огляді обговорити таку тему, як безпека веб-додатків. У 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 ".

Але наша сьогоднішня розмова буде про іншу технологію, яка отримала назву Java Authentication and Authorization Service (JAAS). Саме вона описує такі важливі речі, як автентифікація та авторизація. Давайте розберемося з цим детальніше.
JAAS - Введення в технологію (частина 1) - 2

JAAS

JAAS - це розширення Java SE і воно описано в документі " Java Authentication and Authorization Service (JAAS) Reference Guide ". Як випливає з назви технології, JAAS описує те, як потрібно виконувати аутентифікацію та авторизацію:
  • " Аутентифікація ": у перекладі з грецької "authentikos" означає "реальний, справжній". Тобто автентифікація – це автентифікація. Що той, хто проходить автентифікацію, справді той, за кого себе видає.

  • " Авторизація " : у перекладі з англійської означає " дозвіл " . Тобто авторизація — це контроль доступу після успішного проходження аутентифікації.

Тобто JAAS — це визначення тих, хто запитує доступ до ресурсу, і про винесення рішення, а чи може він цей доступ отримати. Невелика аналогія із життя:ви їдете дорогою і Вас зупиняє інспектор. Прохання надати документи – автентифікація. Чи можете Ви керувати автомобілем за документами — авторизація. Або, наприклад, у магазині Ви хочете купити алкоголь. Спочатку, у Вас просять паспорт – автентифікація. Далі на основі вашого віку вирішується, чи маєте Ви право купувати алкоголь. Це авторизація. У веб-додатках, вхід під користувачем (введення логіну та пароля) є автентифікацією. А визначення того, які вам можна відкривати сторінки - авторизацією. У цьому нам і допомагає "The Java Authentication and Authorization Service (JAAS)". Розглядаючи JAAS, важливо розуміти кілька ключових понять, які описує JAAS: Subject, Principals, Credentials. Subject- Це суб'єкт аутентифікації. Тобто це носій чи володар прав. У документації Subject визначено як джерело (source) запиту (request) виконання певної дії. Суб'єкт чи джерело необхідно якось описати і для цього використовується Principal, який російською теж іноді називають принципалом. Тобто кожен Principal є уявленням Subject із певної точки зору. Щоб стало зрозуміліше, наведемо приклад: Певна людина є Subject. А як Principal'ів можуть виступати:
  • його посвідчення водія, як подання людини як учасника дорожнього руху
  • його паспорт, як подання людини як громадянина своєї країни
  • його закордонний паспорт як подання людини як учасника міжнародних відносин
  • його читацький квиток у бібліотеці, як подання людини як прикріпленого до бібліотеки читача
Крім того, Subject має набір Credential, що в перекладі з англійської означає посвідчення. Це те, чим Subject підтверджує, що це він. Наприклад, як Credential може виступати пароль користувача. Або будь-який об'єкт, яким користувач може підтвердити, що він це справді він. Давайте подивимося, як JAAS використовується у веб-додатках.
JAAS - Введення в технологію (частина 1) - 3

Веб-додаток

Отже, нам знадобиться веб-додаток. У його створенні нам допоможе система автоматичного збирання проектів 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 Поведінка плагіна описано так:
JAAS - Введення в технологію (частина 1) - 4
Тобто плагін буде враховувати файли з цього розташування при складанні WAR архіву нашого веб-додатку. Крім того, у документації Gradle War Plugin'а сказано, що цей каталог буде "root of the archive". І вже ми можемо створити каталог WEB-INF і додати туди файл web.xml. Що це за такий файл? 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
JAAS - Введення в технологію (частина 1) - 5
Важливо перевірити, чи контейнер сервлетів вибраний Tomcat (див. #1) і перевірити, за якою адресаою доступна наша веб-додаток (див. #2).
JAAS - Введення в технологію (частина 1) - 6

Аутентифікація

Установки автентифікації часто складаються з двох частин: настройок на стороні сервера та настройок на стороні веб-програми, яка працює на цьому сервері. Установки безпеки веб-програми не можуть не взаємодіяти з налаштуваннями безпеки веб-сервера хоча б через те, що веб-програма не може не взаємодіяти з веб-сервером. Ми з Вами недаремно перейшли на 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 ".
JAAS - Введення в технологію (частина 1) - 7
Тобто ми можемо помістити налаштування JAAS Login Config у ресурси своєї веб-додатки і завдяки Tomcat Context'у ми зможемо його використовувати. Даний файл повинен бути доступний як ресурс для ClassLoader'а, тому його шлях повинен бути таким: \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 ". Залишився п
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ