Продолжение первой части статьи про JAAS. Разберёмся, можно ли использовать для JAAS только аннотации, какие мы встретим проблемы. Какие средства Servlet API позволяют сделать код универсальнее узнаем из этой части. И подведём итог прочитанного.

Продолжение

В первой части рассмотрения технологии JAAS (см. "JAAS — Введение в технологию (часть 1)") мы рассмотрели основной сценарий использования JAAS и Servlet API. Мы увидели, что контейнер сервлетов Tomcat управлял безопасностью нашего веб-приложения с применением архитектуры JAAS. Имея в наличии знание о "auth-method" и "Security Realm" контейнер Tomcat сам предоставил нам нужную реализацию механизма аутентификации и сам предоставил нам CallbackHandler, мы лишь использовали это всё в нашем логин модуле. Единственное, важно помнить, что браузер сохраняет данные логина и пароля, переданные по BASIC аутентификации. Поэтому, для каждой новой проверки при помощи Chrome можно нажимать Ctrl+Shift+N для открытия нового окна для работы в режиме "инкогнито".

Аннотации

Конфигурирование при помощи XML давно уже выходит из моды. Поэтому, важно сказать, что начиная с Servlet API версии 3.0 у нас появилась возможность задать настройки сервлетов при помощи аннотаций, без использования web.xml файла деплоймент дескриптора. Давайте посмотрим, как поменяется управление безопасностью, если мы будет использовать аннотации. И можно ли реализовать выше описанные подходы при помощи аннотаций. Разобраться с аннотациями нам поможет опять спецификация Servlet API и её раздел "Annotations and pluggability". Данный раздел говорит, что объявление сервлета в web.xml можно заменить аннотацией @WebServlet. Соответственно, наш сервлет будет выглядеть следующим образом:
@WebServlet(name="app", urlPatterns = "/secret")
public class App extends HttpServlet {
Далее обратимся в главе "13.3 Programmatic Security". В ней сказано, что мы можем объявить Security Constraint так же через аннотации:
@WebServlet(name="app", urlPatterns = "/secret")
@ServletSecurity(httpMethodConstraints = {
        @HttpMethodConstraint(value = "GET", rolesAllowed = "admin")
})
public class App extends HttpServlet {
Теперь в нашем web.xml остался только один блок — login-config. Проблема в том, что так уж получилось, что нет способа легко и просто его заменить. Из-за тесной связи настроек безопасности веб-приложения и настроек безопасноти веб-сервера не существует простого и универсального способа это сделать даже программно. Это одна из проблем аутентификации при помощи JAAS и Servlet API. Говоря про блок login-config, стоит понимать, что он является декларативным описанием Authentication Mechanisms, т.е. механизмов аутентификации. До сих пор нет простого универсального способа заменить его, т.к. обработка web.xml происходит глубоко внутри сервлет контейнеров. Например, у Tomcat можно посмотреть исходник ContextConfig.java. Поэтому, даже для контейнера сервлетов Tomcat существует несколько вариантов и все они разные. Например, если мы используем контейнер сервлетов Embedded Tomcat (т.е. поднимаем веб-сервер из кода), то про такие варианты можно прочитать тут: "Embedded Tomcat with basic authentication via code". Кроме того, общий пример поднятия Embedde Tomcat можно увидеть в руководстве PaaS-платформы Heroku: "Create a Java Web Application Using Embedded Tomcat". Если же Tomcat используется не в Embedded режиме, то для Tomcat можно использовать часто встречаемый подход — слушатели событий. В Tomcat это "The LifeCycle Listener Component". При этом важно понимать, что у сервлет контейнеров (в нашем случае Tomcat) могут быть свои загрузчики классов и просто так взять и использовать свои классы не выйдет. Для Tomcat необходимо разбираться с "Class Loader HOW-TO". В другом сервлет контейнере, который называется Undertow, такое можно реализовать при помощи "Servlet Extensions". Как видно, кто-то предусмотрел более гибкие механизмы, а кто-то нет. Как видите, единого варианта нет. Все они очень разные. Есть ли возможность как-то сделать что-то универсальное имея только Servlet API и JAAS? В интернете можно найти предложение использовать Servlet Filter для выполнения аутентификации без блока login-config. Давайте напоследок рассмотрим и этот вариант. Это позволит нам ещё повторить, как работает JAAS.

Auth Filter

Итак, наша цель — избавиться полностью от web.xml файла. Если мы от него избавимся, то, к сожалению, мы не сможем больше использовать Security Constraint, т.к. их обработка может происходить гораздо раньше, чем будут применены сервлет фильтры. Эта та плата, которую придётся платить за "универсальность" использования фильтров. Т.е. нам придётся убрать аннотацию @ServletSecurity, а все проверки, которые мы ранее описывали в security constraint, придётся выполнять программным образом. Как видите, этот вариант накладывает на нас тоже много неприятных ограничений. Первым делом, уберём аннотацию @ServletSecurity с ресурса и блок login-config из файла web.xml. Теперь, нам самим придётся реализовать Basic аутентификацию. Теперь добавим наш фильтр:
@WebFilter("/*")
public class JaasFilter implements javax.servlet.Filter {
Пока кажется просто. Напишем метод инициализации:
@Override
public void init(FilterConfig filterConfig) throws ServletException {
	String jaas_conf = filterConfig.getServletContext().getRealPath("/WEB-INF/jaas.config");
	System.getProperties().setProperty("java.security.auth.login.config",jaas_conf);
}
Как видим, мы вынуждены теперь использовать базовые средства JAAS для поиска jaas config файла. У этого есть большой недостаток — если приложений будет несколько, то одно приложение может сломать аутентификацию другому. Про то, каким образом можно вообще задать Jaas Config файл подробно описано в документации JAAS: "Appendix A: JAAS Settings in the java.security Security Properties File". Далее опишем сам метод фильтрации. Начнём с получения HTTP реквеста:
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
	HttpServletRequest req = (HttpServletRequest) request;
	// Если в реквесте уже есть Principal - ничего не делаем
	if (req.getUserPrincipal() != null ) {
		chain.doFilter(request, response);
	}
Здесь всё просто. Если Principal доступен, значит аутентификация пройдена. Далее, нужно описать действия фильтра, когда пользователь не прошёл аутентификацию, т.е. его ещё не распознали. Ранее мы описывали способ базовой аутентификации, BASIC. Т.к. мы теперь сами пишем фильтр, то придётся разобраться, а как же вообще работаем BASIC аутентификация. Можно воспользоваться "MDN web docs : HTTP авторизация". А так же "How Does HTTP Authentication Work?". Из описания работы Basic аутентификации понятно, что если сервер хочет провести BASIC аутентификацию, а пользователь не предоставил данные, то сервер отправляет особый заголовок "WWW-Authenticate" и код ошибки 401. Создадим для этого внутренний метод:
private void requestNewAuthInResponse(ServletResponse response) throws IOException {
	HttpServletResponse resp = (HttpServletResponse) response;
	String value = "Basic realm=\"JaasLogin\"";
	resp.setHeader("WWW-Authenticate", value);
	resp.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
Теперь давайте задействуем этот метод и добавим в метод doFilter следующий блок кода:
// Получаем security Header. А если его нет - запрашиваем
String secHeader = req.getHeader("authorization");
if (secHeader == null) {
	requestNewAuthInResponse(response);
}
Теперь давайте добавим ветку для if, которая будет выполнятся, когда у нас заголовок всёж передан:
// Проверяем аутентификацию
else {
	String authorization = secHeader.replace("Basic ", "");
	Base64.Decoder decoder = java.util.Base64.getDecoder();
	authorization = new String(decoder.decode(authorization));
	String[] loginData = authorization.split(":");
	try {
		if (loginData.length == 2) {
			req.login(loginData[0], loginData[1]);
			chain.doFilter(request, response);
		} else {
			requestNewAuthInResponse(response);
		}
	} catch (ServletException e) {
		requestNewAuthInResponse(response);
	}
}
В этот код можно добавить авторизацию, например: req.isUserInRole("admin") Вот мы и сделаелали с вами аутентификацию при помощи фильтра. С одной стороны — много кода и ручная обработка. С другой стороны — универсальность и независимость от сервера.

Заключение

Вот мы и дошли до конца этого обзора. Надеюсь, теперь станет немного понятнее, что такое JAAS, что такое Subject и что такое Principals. Слова Security Realm и Login Config не будут больше вызывать вопросов. Кроме того, мы теперь знаем, как применять JAAS и Servlet API вместе. Кроме того, мы узнали про узкие места, где нас не спасут аннотации в Servlet API. Конечно же, это далеко не всё. Например, аутентификация в Java может быть не только BASIC. Про другие типы можно посмотреть в спецификации Servlet API в разделе "13.6 Authentication". Сложно найти в интернете какую-то подробную информацию по JAAS. Но я могу посоветовать данный материал: Надеюсь, информации из этого обзора будет полезна и понятна. #Viacheslav