Введение
Хотелось бы в данном обзоре обсудить такую тему, как безопасность веб-приложений. В 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.
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 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, т.к. 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".
\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".
Остался п
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ