Обеспечение безопасности доступа реализована в Java довольно давно и архитектура обеспечения этой безопасности называется JAAS — Java Authentication and Authorization Service. Данный обзор попытается раскрыть тайну, что такое аутентификация, авторизация и при чём тут JAAS. Как JAAS дружит с Servlet API, а где у них во взаимоотношениях проблемы.

Введение

Хотелось бы в данном обзоре обсудить такую тему, как безопасность веб-приложений. В 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

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 используется в веб-приложениях.

Веб-приложение

Итак, нам понадобится веб-приложение. В его создании нам поможет система автоматической сборки проектов 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. 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 Поведения плагина описано так:
То есть плагин будет учитывать файлы из этого расположения при сборке WAR архива нашего веб-приложения. Кроме того, в документации Gradle War Plugin'а сказано, что данный каталог будет "root of the archive". И уже в нём мы можем создать каталог WEB-INF и добавить туда файл web.xml. Что это за файл такой? web.xml — это "Deployment Descriptor" или "описатель развёртывания". Это такой файл, который описывает, как нужно настроить наше веб-приложение для работы. В этом файле указывается, какие запросы будет обрабатывать наше приложение, настройки безопасности и многое другое. По своей сути он чем-то похож на manifest файл из JAR файла (см. "Working with Manifest Files: The Basics"). Manifest файл рассказывает, как работать с Java Application (т.е. с JAR архивом), а web.xml рассказывает, как работать с Java Web Application (т.е. с WAR архивом). Само понятие "Deployment Descriptor" возникло не само по себе, а описано в документе "Servlet API Specification". Любое Java веб-приложение зависит от этого "Servlet API". Важно понимать, что это API — то есть это описание некоторого контракта взаимодействия. Веб-приложения — это не самостоятельные приложения. Они запускаются на веб-сервере, который обеспечивает сетевое взаимодействие с пользователями. То есть веб-сервер это некоторый "контейнер" для веб-приложений. Это логично, т.к. мы хотим писать логику веб-приложения, т.е. какие странички увидит пользователь и как они должны реагировать на действия пользователя. И мы не хотим писать код того, как будет отправляться сообщение пользователю, как будут передаваться байты информации и другие низкоуровневые и очень требовательные к качеству реализации вещи. Кроме того, получается, что веб-приложения все разные, а передача данных одинакова. То есть миллиону программистов пришлось бы писать для одной и той же цели код снова и снова. Поэтому за часть взаимодействия с пользователем и за обмен данными отвечает веб-сервер, а за формирование этих данных отвечает веб-приложение и разработчик. А чтобы связать эти две части, т.е. веб-сервер и веб-приложение, нужен контракт их взаимодействия, т.е. по каким правилам они это будут делать. Чтобы как-то описать контракт, как должно выглядеть взаимодействие между веб-приложением и веб-сервером и придуман 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 документ составлен. Теперь воспользуемся примером из главы "14.5 Examples", но схему нужно указать для версии 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 will extend HttpServlet to 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 (см. #1) и проверить, по каком адресу доступно наше веб-приложение (см. #2).

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

Настройки аутентификации зачастую состоят из двух частей: настроек на стороне сервера и настроек на стороне веб-приложения, которое на этом сервере работает. Настройки безопасности веб-приложения не могут не взаимодействовать с настройками безопасности веб-сервера хотя бы по той причине, что веб-приложение не может не взаимодействовать с веб-сервером. Мы с Вами не зря переключились на Tomcat, т.к. Tomcat имеет хорошо описанную архитектуру (см. "Apache Tomcat 8 Architecture"). Из описания этой архитектуры видно, что Tomcat как веб-сервер представляет веб-приложение как некоторый контекст, который и называют "Tomcat Context". Этот контекст позволяет каждому веб-приложению иметь свои настройки, изолированные от других веб-приложений. Кроме того, веб-приложение может влиять на настройки этого контекста. Гибко и удобно. Для более глубокого понимания рекомендуется к прочтению статья "Understanding Tomcat Context Containers" и раздел документации 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 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". Остался последний штрих — нужно наложить ограничение безопасности на ресурс. Например, на нашу страницу /secret. Ограничение на английском будет constraint. Соответственно, ограничение безопасности — Security Constraint. Поэтому, в web.xml добавим:
<security-constraint>
	<web-resource-collection>
		<web-resource-name>Secret Page</web-resource-name>
		<url-pattern>/secret</url-pattern>
	</web-resource-collection>
	<auth-constraint>
		<role-name>agent</role-name>
	</auth-constraint>
</security-constraint>
Указанная в auth-constraint роль должна быть перечислена в тэгах security-role в файле web.xml:
<security-role>
	<role