В предыдущей части этой серии мы обсудили, как создать проект Dropwizard с использованием архетипа Maven, а также как создать простой RESTful API и получить к нему доступ. Безопасность API — важная тема, и сегодня мы обсудим, как использовать аутентификацию и HTTPS с Dropwizard. Также будет затронута проблема настройки приложений Dropwizard. Весь код для примеров ниже можно найти здесь .
Базовая аутентификация — это самый простой способ защитить доступ к ресурсу. Он сводится к передаче пары код-идентификатора пользователя и пароля в кодировке base64, используя HTTP-заголовок Authorize. Если неаутентифицированный клиент пытается получить доступ к защищенному ресурсу, сервер предлагает клиенту предоставить учетные данные, то есть вышеупомянутую пару. В противном случае клиент может предоставить учетные данные без запроса от сервера.
Эта схема аутентификации небезопасна, так как использует незашифрованные учетные данные. Кодировка используется для замены несовместимых с HTTP символов совместимыми и может быть легко изменена при помощи snooper. Хорошей практикой является использование этой схемы в сочетании с HTTPS, когда учетные данные передаются по зашифрованному каналу.
Для начала работы с аутентификацией необходимо добавить зависимость в pom-файл.
<dependency>
<groupId>io.dropwizard</groupId>
<artifactId>dropwizard-auth</artifactId>
<version>${dropwizard.version}</version>
</dependency>
Давайте поговорим о том, как добавить базовую аутентификацию в REST API Dropwizard, а затем обсудим, как получить доступ к защищенным ресурсам как из командной строки, так и из браузера, и, наконец, в конце этой статьи мы увидим, как протестировать защищенный паролем подресурс. методы. Для защиты ресурса в Dropwizard используется трехэтапный подход. Во-первых, следует реализовать интерфейс Authenticator, где определяется метод проверки учетных данных пользователя. Разработчик API должен хранить и проверять учетные данные; их можно хранить в базе данных или на сервере LDAP, оба варианта поддерживаются Dropwizard. Метод может возвращать некоторый объект с пользовательскими данными, которые могут отображаться пользователю после успешной аутентификации. Давайте посмотрим фрагмент.
public class GreetingAuthenticator
implements Authenticator<BasicCredentials, User> {
@Override
public Optional<User> authenticate(BasicCredentials credentials)
throws AuthenticationException {
if ("crimson".equals(credentials.getPassword())) {
return Optional.of(new User());
} else {
return Optional.absent();
}
}
}
Для размещения кода необходимо создать пакет com.javaeeeee.dwstart.auth. Прежде всего давайте рассмотрим метод аутентификации. Он использует жестко запрограммированный пароль и вообще не использует идентификатор пользователя. Хотя это делает код простым для понимания, здесь должен быть код, который обращается к базе данных или серверу LDAP, который проверяет учетные данные. Возврат этого метода имеет отношение к классу User, который определен в пакете com.javaeeeee.dwstart.core. Как разработчик, решать, какие поля добавить в этот класс. Это может быть полное имя пользователя, адрес электронной почты и т. Д. Даже пустой класс сработает.
Если вы посмотрите на определение нашего класса GreetingAuthenticator, то увидите, что вместо аргумента пользовательского типа можно использовать какой-то другой класс, а тип указывается в угловых скобках в качестве параметра второго типа. Первым аргументом типа является BasicCredentials, который предоставляет методу аутентификации учетные данные пользователя.
Как было упомянуто ранее, метод authenticate — это место для некоторого поиска, и результатом может быть то, что пользователь присутствует или отсутствует в хранилище нашего API. Обычный способ обработки ситуации отсутствия пользователя — использование нулевого значения в качестве возвращаемого результата. Однако есть лучший способ выполнить эту работу, а именно иметь специальный класс в качестве возвращаемого типа, объекты которого могут либо хранить ссылку на другой объект, либо быть пустыми. Иногда неясно, может ли метод вернуть значение null, поэтому лучше явно показать, что ситуация, когда результат отсутствует, возможна с использованием необязательного возвращаемого типа. Это приводит к уменьшению возможных ошибок при обработке результата такого метода, так как тип возвращаемого метода информируется о том, что результат может отсутствовать, и запрашивается обработка такого пути. Класс является частью Набор библиотек Google Guava , который входит в семейство Dropwizard.
Для создания необязательного объекта можно использовать либо метод of (…), передав ему ненулевую ссылку, либо метод absent () для создания пустого объекта. Существует третий вариант использования метода fromNullable (…), посредством которого может быть передана ссылка на возможное нулевое значение, но она не удовлетворяет нашим потребностям, поскольку нам нужно либо вернуть пользователя, если он присутствует в нашем магазине, либо показать ее отсутствие. Помимо использования в качестве возвращаемого типа метода, Optional может использоваться как тип параметра метода, а Dropwizard позволяет передавать значения такого типа в методы ресурсов.
Вместо использования аннотации @Default,
public String getTailoredGreetingWithQueryParam(@DefaultValue("world")
@QueryParam("name") String name)
можно использовать аргумент необязательного типа.
public String getTailoredGreetingWithQueryParam(
@QueryParam("name") Optional<String> name) {
if (name.isPresent()) {
return "Hello " + name.get();
} else {
return "Hello world";
}
//The same can be accomplished using or(...) method to provide the default value
//return "Hello " + name.or("world");
}
Код в приведенном выше фрагменте проверяет, присутствует ли значение в объекте имени необязательного типа, а затем извлекает значение. Попытка извлечь значение из пустого Необязательного приводит к исключению. Если значение отсутствует, используется значение по умолчанию. Та же логика может быть реализована с использованием метода удобства или (…), который возвращает значение из необязательного объекта, если он присутствует, и значение, переданное ему в качестве аргумента в противном случае. Возвращаясь к аутентификации, аналогичный код для проверки присутствия пользователя в хранилище используется где-то вверх по течению, где вызывается метод authenticate (…). Обратите внимание, что Java 8 имеет свой собственный необязательный класс.
Вторым шагом является регистрация нашего Authenticator в методе run класса DWGettingStartedApplication.
@Override
public void run(final DWGettingStartedConfiguration configuration,
final Environment environment) {
environment.jersey().register(AuthFactory.binder(
new BasicAuthFactory<>(
new GreetingAuthenticator(),
"SECURITY REALM",
User.class)));
// Resources are registered here
}
Следует отметить, что наше обсуждение относится к версии Dropwizard 0.8.x. Код для предыдущей версии 0.7.1 можно найти в репозитории здесь, в отдельной ветке. Суть приведенного выше кода — это класс BasicAuthFactory, конструктор которого имеет три параметра: класс, реализующий интерфейс Authenticator, скрытую строку, которая должна отображаться неаутентифицированному клиенту внутри заголовка HTTP WWW-Authenticate, и вышеупомянутый класс, ссылка на который возвращается в объект Необязательного типа методом authenticate.
Важность класса BasicAuthFactory заключается в том, что его метод provide (), определенный в интерфейсе org.glassfish.hk2.api.Factory, который используется за кулисами, выполняет всю тяжелую работу, связанную с аутентификацией. Метод извлекает содержимое заголовка Authentication и проверяет, были ли предоставлены учетные данные. Если нет, DefaultUnauthorizedHandler используется для визуализации несанкционированного ответа 401 с сообщением «Учетные данные необходимы для доступа к этому ресурсу». В противном случае учетные данные декодируются и извлекаются и используются для создания экземпляра класса BasicCredentials. После этого вызывается наш метод authenticate для проверки достоверности учетных данных. Если они недействительны, создается описанный выше ответ, в противном случае возвращается экземпляр нашего класса User.
Последний шаг заключается в защите нашего метода, то есть в указании Dropwizard, что только аутентифицированным пользователям разрешен доступ к конкретному ресурсу. Это делается с помощью аннотации @Auth, как показано ниже.
@Path("secured_hello")
public class SecuredHelloResource {
@GET
@Produces(MediaType.TEXT_PLAIN)
public String getGreeting(@Auth User user) {
return "Hello world!";
}
}
Аннотация дает указание Dropwizard запрашивать у пользователей ввод учетных данных каждый раз, когда к ресурсу обращаются, или пользователь может предоставить учетные данные без запроса, и если они действительны, ему предоставляется доступ. В процессе обеспечения метода ресурса объектом класса User вызывается метод provide (). Следует отметить, что ресурс должен быть зарегистрирован в классе DWGettingStartedApplication, чтобы стать доступным. Процесс взаимодействия с защищенным ресурсом показан ниже.
Можно использовать опцию i cURL, чтобы показать заголовки
curl -w "\n" -i localhost:8080/secured_hello
или F12 инструменты браузера. Когда к защищенному ресурсу обращаются через браузер, отображается диалоговое окно с предложением ввести учетные данные.
Если нажать «Отмена», можно проверить заголовки.
Как в терминале, так и в браузере видно, что вышеупомянутая скрытая строка используется в качестве имени области в ответе. Целью безопасности является то, что пакет для классов в Java, способ группировать пользователей и назначать им тип хранения. Если кто-то вводит учетные данные в браузере, они кэшируются, чтобы пользователь не мог вводить их каждый раз при обращении к ресурсу, а затем отображается страница.
Возвращаясь к cURL, хотя ввод учетных данных в качестве содержимого заголовка является жизнеспособной опцией, существует опция u, которая позволяет вводить разделенную столбцами пару user-ID: password и выполняет для нас кодирование.
curl -w "\n" -u javaeeeee:crimson localhost:8080/secured_hello
Результат «Привет, мир! Без кавычек должен отображаться в терминале.
Чтобы использовать Postman для доступа к защищенному ресурсу, следует использовать вкладку Basic Auth, где установлены учетные данные.
После этого нужно нажать кнопку «Обновить заголовки», после чего можно ввести URL и нажать кнопку «Отправить».
Ответ появляется в окне.
конфигурация
Теперь поговорим о конфигурации. Более сложному API требуется соединение с базой данных, поэтому необходимо, чтобы параметры соединения были где-то сохранены. Dropwizard использует читаемый человеком формат данных YAML (YAML Ain’t Markup Language) для хранения конфигурации. Конфигурация десериализуется в экземпляр класса, который расширяет io.dropwizard.Configuration, в случае нашего проекта это DWGettingStartedConfiguration.
Следует отметить, что есть некоторые встроенные параметры, например, порт, который мы используем для подключения к нашему приложению, и такие параметры имеют некоторые разумные значения по умолчанию, например, порт настроен на 8080. Разработчик может использовать свои собственные параметры и для ради изучения того, как использовать пользовательские параметры, мы добавим ID пользователя и пароль в наш файл конфигурации и используем их для аутентификации. Позже мы увидим, как использовать конфигурацию для включения шифрования.
Давайте создадим файл конфигурации. Его имя не важно, так как оно передается в качестве аргумента командной строки, и мы можем назвать его config.xml. Файл должен быть помещен в папку проекта. Фрагмент приведен ниже.
## Configuration file for DWGettingStarted application.
---
# User login.
login: javaeeeee
# User password.
password: crimson
Мы добавили два поля логин и пароль; ключи и значения разделяются двоеточием и пробелом. Знак хеша используется для комментариев. Эти два параметра конфигурации могут быть считаны в поля класса конфигурации с помощью Джерси.
public class DWGettingStartedConfiguration extends Configuration {
@NotNull
private String login;
@NotNull
private String password;
@JsonProperty
public String getLogin() {
return login;
}
@JsonProperty
public String getPassword() {
return password;
}
}
Аннотации @JsonProperty для геттеров используются для десериализации файла конфигурации. Если их также поместить в сеттеры, значения конфигурации объекта можно записать в файл или сериализовать. Аннотации @NotNull в полях являются частью Hibernate Validator и используются для проверки того, что поля не являются нулевыми, то есть если в файле конфигурации нет соответствующих значений, приложение не запустится.
Чтобы использовать настройки, мы должны изменить наш Аутентификатор, как показано ниже.
public class GreetingAuthenticator
implements Authenticator<BasicCredentials, User> {
private String login;
private String password;
public GreetingAuthenticator(String login, String password) {
this.login = login;
this.password = password;
}
@Override
public Optional<User> authenticate(BasicCredentials credentials)
throws AuthenticationException {
if (password.equals(credentials.getPassword())
&& login.equals(credentials.getUsername())) {
return Optional.of(new User());
} else {
return Optional.absent();
}
}
}
Также нам необходимо передать аргументы Аутентификатору в процессе регистрации. Метод run (…) класса DWGettingStartedApplication имеет наш класс конфигурации в качестве параметра, поэтому мы можем легко извлечь наши учетные данные.
public void run(final DWGettingStartedConfiguration configuration,
final Environment environment) {
environment.jersey().register(AuthFactory.binder(
new BasicAuthFactory<>(
new GreetingAuthenticator(configuration.getLogin(),
configuration.getPassword()),
"SECURITY REALM",
User.class)));
// ...
}
Для запуска приложения, использующего файл конфигурации, необходимо набрать в терминале следующую команду
java -jar target/DWGettingStarted-1.0-SNAPSHOT.jar server config.yml
или дайте указание IDE передать имя файла конфигурации в качестве аргумента.
Помимо возможности добавления собственных параметров конфигурации, можно изменить настройки по умолчанию. Например, для установки порта на 8085 можно добавить в config.yml строки, показанные ниже.
#Server configuration.
server:
applicationConnectors:
- type: http
port: 8085
HTTPS
HTTPS — это протокол, используемый для безопасной связи по сети, так как он опирается на безопасность транспортного уровня (TLS) для обеспечения криптографической защиты передаваемых данных. Предшественник TLS назывался SSL. На самом деле HTTPS использует сертификаты открытых ключей для проверки личности сервера и шифрования всех данных, передаваемых между клиентом и сервером, чтобы предотвратить перехват.
Существует специальная третья сторона, которая называется Certificate Authority (CA), которая выдает сертификаты и ваучеры на их действительность, и браузеры знают, как проверить действительность сертификата, выданного сервером. Фактически, сертификат связывает отличительное имя сервера с его открытым ключом и создается, когда клиент подключается к серверу, и помогает клиенту доказать, что сервер действительно тот, кто он есть, а не самозванец.
Для целей разработки и тестирования можно создать сертификат с помощью какого-либо специального инструмента, хотя такой сертификат не защищен каким-либо ЦС и называется самоподписанным сертификатом. Этот сертификат может использоваться для шифрования данных, но не проходит проверку достоверности, и в результате браузер отображает предупреждение, которое может быть проигнорировано пользователем. Нет необходимости знать все гайки и болты HTTPS и TLS, чтобы начать, можно полагаться на инструмент, который сгенерирует все необходимые объекты для его работы.
Чтобы включить HTTPS, необходимо создать хранилище ключей, содержащее пару открытых / закрытых ключей, что можно сделать с помощью программы keytool, являющейся частью JDK. Следующая команда должна быть введена из папки проекта; Java 8 используется.
keytool -genkeypair
-keyalg RSA
-dname "CN=localhost"
-keystore dwstart.keystore
-keypass crimson
-storepass crimson
Команда была отформатирована для ясности, но должна быть введена как одна строка в окне терминала. Опция genkeypair инструктирует keytool генерировать пару, значение keyalg — это алгоритм, используемый для генерации пары, значение опции dname — это отличительное имя объекта, для которого создается сертификат, и является частью сертификата. Параметр хранилища ключей задает имя сгенерированного файла, а последние две опции включают пароли для закрытого ключа и хранилище ключей, пароль и хранилище соответственно. По умолчанию сгенерированный сертификат действителен в течение 90 дней, для изменения этого можно использовать параметр срока действия, чтобы указать срок действия в днях. Следует отметить, что установка паролей в командной строке является плохой практикой, так как их может легко увидеть третья сторона,но это было сделано, чтобы предотвратить дальнейшие вопросы из keytool; если опущено, один будет предложено ввести пароли.
После создания хранилища ключей пришло время сообщить Dropwizardу о его имени и пароле. Это делается с помощью вышеупомянутого файла конфигурации.
#Server configuration.
server:
applicationConnectors:
- type: http
port: 8080
- type: https
port: 8443
keyStorePath: dwstart.keystore
keyStorePassword: crimson
validateCerts: false
Последний параметр позволяет запускать Dropwizard, если сертификат недействителен, например, если срок его действия истек. Теперь мы готовы получить доступ к ресурсу через зашифрованное соединение. Для этого начнем с cURL.
curl -w "\n" -k -u javaeeeee:crimson https://localhost:8443/secured_hello
Опция k отключает проверку сертификата, то есть позволяет нам работать с самозаверяющим сертификатом. При попытке подключиться к серверу, который использует самозаверяющий сертификат, браузеры предупреждают вас о небезопасном соединении. Расширение Postman Chrome будет работать без суеты.
У нас есть приложение, которое использует различные части взаимодействия Dropwizard, а именно аутентификацию и настройку, и мы должны использовать интеграционные тесты, чтобы убедиться, что наше приложение работает правильно. Для этого мы можем использовать аннотацию jUnit @ClassRule, чтобы запустить приложение и указать путь к его файлу конфигурации, как показано ниже.
public class IntegrationTest {
@ClassRule
public static final DropwizardAppRule<DWGettingStartedConfiguration> RULE
= new DropwizardAppRule<>(DWGettingStartedApplication.class,
"config.yml");
@Test
public void testGetGreeting() {
String expected = "Hello world!";
//Obtain client
Client client = ClientBuilder.newClient();
//Build a feature in basic authentication mode
HttpAuthenticationFeature feature
= HttpAuthenticationFeature.basic("javaeeeee", "crimson");
//Register the feature
client.register(feature);
//Get actual resul string
String actual = client
.target("http://localhost:8080/secured_hello")
.request(MediaType.TEXT_PLAIN)
.get(String.class);
//Do an assertion
assertEquals(expected, actual);
}
}
Сначала мы создали экземпляр клиента, используя метод API JAX-RS. Затем, чтобы использовать базовую аутентификацию, мы создали функцию, передающую учетные данные, и зарегистрировали ее. Наконец, мы проверили, что верное значение было возвращено. Чтобы проверить печальный путь, можно указать неверные учетные данные или вообще пропустить регистрацию функции и проверить, что код ответа 401 (неавторизован).
Для тестирования ресурса с использованием HTTPS необходимо выполнить некоторые дополнительные шаги, как показано в следующем фрагменте.
//Create SSL Configurator
SslConfigurator sslConfigurator = SslConfigurator.newInstance();
//Register a keystore
sslConfigurator.trustStoreFile("dwstart.keystore")
.trustStorePassword("crimson");
//Create SSL Context
SSLContext sSLContext = sslConfigurator.createSSLContext();
//Obtain client
Client client = ClientBuilder
.newBuilder()
.sslContext(sSLContext)
.build();
Во-первых, необходимо создать экземпляр класса утилит SslConfigurator и установить наше хранилище ключей как хранилище доверенных сертификатов для клиента и установить пароль для хранилища ключей. Это заставит клиента доверять сертификату, иначе вы получите исключение. Во-вторых, SSLContext создается и устанавливается в ClientBuilder. Наконец, у вас есть экземпляр клиента Jersey 2.x, который может подключаться к вашему приложению через HTTPS. Остальные этапы теста такие же.
Ссылки