«1. Обзор

В этом руководстве мы рассмотрим простой способ отправки заголовков в клиентских запросах Server-Sent Event (SSE) с использованием API клиента Джерси.

Мы также рассмотрим правильный способ отправки основных заголовков типа \»ключ-значение\», заголовков проверки подлинности и ограниченных заголовков с использованием стандартного транспортного соединителя Джерси.

2. Сразу к делу

Вероятно, мы все сталкивались с этой ситуацией при попытке отправить заголовки с помощью SSE:

Мы используем SseEventSource для получения SSE, но для построения SseEventSource нам нужен WebTarget экземпляр, который не дает нам возможности добавлять заголовки. Экземпляр Client тоже не поможет. Звучит знакомо?

Помните, однако, что заголовки связаны не с SSE, а с самим запросом клиента, так что нам действительно следует искать там.

Давайте посмотрим, что мы можем сделать с ClientRequestFilter.

3. Зависимости

Чтобы начать наше путешествие, нам нужна зависимость jersey-client, а также зависимость Jersey от SSE в нашем файле Maven pom.xml:

<dependency>
    <groupId>org.glassfish.jersey.core</groupId>
    <artifactId>jersey-client</artifactId>
    <version>2.29</version>
</dependency>
<dependency>
    <groupId>org.glassfish.jersey.media</groupId>
    <artifactId>jersey-media-sse</artifactId>
    <version>2.29</version>
</dependency>

Обратите внимание, что Jersey поддерживает JAX- RS 2.1 по состоянию на 2.29, так что похоже, что мы сможем использовать его функции.

4. ClientRequestFilter

public class AddHeaderOnRequestFilter implements ClientRequestFilter {

    public static final String FILTER_HEADER_VALUE = "filter-header-value";
    public static final String FILTER_HEADER_KEY = "x-filter-header";

    @Override
    public void filter(ClientRequestContext requestContext) throws IOException {
        requestContext.getHeaders().add(FILTER_HEADER_KEY, FILTER_HEADER_VALUE);
    }
}

Сначала мы реализуем фильтр, который будет добавлять заголовок к каждому клиентскому запросу:

После этого мы зарегистрируем его и используем.

Client client = ClientBuilder.newBuilder()
  .register(AddHeaderOnRequestFilter.class)
  .build();

WebTarget webTarget = client.target("https://sse.example.org/");

SseEventSource sseEventSource = SseEventSource.target(webTarget).build();
sseEventSource.register((event) -> { /* Consume event here */ });
sseEventSource.open();
// do something here until ready to close
sseEventSource.close();

Для наших примеров мы будем использовать https://sse.example.org в качестве воображаемой конечной точки, из которой мы хотим, чтобы наш клиент потреблял события. На самом деле мы бы изменили это на реальную конечную точку сервера событий SSE, которую мы хотим, чтобы наш клиент использовал.

Что, если нам нужно отправить более сложные заголовки, такие как заголовки аутентификации, на нашу конечную точку SSE?

Давайте вместе перейдем к следующим разделам, чтобы узнать больше о заголовках в клиентском API Джерси.

5. Заголовки в клиентском API Джерси

System.setProperty("sun.net.http.allowRestrictedHeaders", "true");

Важно знать, что реализация транспортного соединителя Джерси по умолчанию использует класс HttpURLConnection из JDK. Этот класс ограничивает использование некоторых заголовков. Чтобы обойти это ограничение, мы можем установить системное свойство:

Список запрещенных заголовков можно найти в документации Джерси.

5.1. Простые общие заголовки

public Response simpleHeader(String headerKey, String headerValue) {
    Client client = ClientBuilder.newClient();
    WebTarget webTarget = client.target("https://sse.example.org/");
    Invocation.Builder invocationBuilder = webTarget.request();
    invocationBuilder.header(headerKey, headerValue);
    return invocationBuilder.get();
}

Самый простой способ определить заголовок — это вызвать WebTarget#request для получения Invocation.Builder, который предоставляет метод заголовка.

public Response simpleHeaderFluently(String headerKey, String headerValue) {
    Client client = ClientBuilder.newClient();

    return client.target("https://sse.example.org/")
      .request()
      .header(headerKey, headerValue)
      .get();
}

И, на самом деле, мы можем довольно хорошо сжать это для большей читабельности:

Отсюда мы будем использовать только беглую форму для примеров, так как ее легче понять.

5.2. Базовая аутентификация

public Response basicAuthenticationAtClientLevel(String username, String password) {
    HttpAuthenticationFeature feature = HttpAuthenticationFeature.basic(username, password);
    Client client = ClientBuilder.newBuilder().register(feature).build();

    return client.target("https://sse.example.org/")
      .request()
      .get();
}

На самом деле API-интерфейс клиента Джерси предоставляет класс HttpAuthenticationFeature, который позволяет нам легко отправлять заголовки аутентификации:

Поскольку мы зарегистрировали функцию при создании клиента, она будет применяться к каждому запросу. API обрабатывает кодировку имени пользователя и пароля, требуемую базовой спецификацией.

Обратите внимание, кстати, что мы подразумеваем HTTPS в качестве режима для нашего соединения. Хотя это всегда ценная мера безопасности, она имеет основополагающее значение при использовании обычной проверки подлинности, поскольку в противном случае пароль отображается в виде открытого текста. Джерси также поддерживает более сложные конфигурации безопасности.

public Response basicAuthenticationAtRequestLevel(String username, String password) {
    HttpAuthenticationFeature feature = HttpAuthenticationFeature.basicBuilder().build();
    Client client = ClientBuilder.newBuilder().register(feature).build();

    return client.target("https://sse.example.org/")
      .request()
      .property(HTTP_AUTHENTICATION_BASIC_USERNAME, username)
      .property(HTTP_AUTHENTICATION_BASIC_PASSWORD, password)
      .get();
}

Теперь мы также можем указывать кредиты во время запроса:

5.3. Дайджест-аутентификация

public Response digestAuthenticationAtClientLevel(String username, String password) {
    HttpAuthenticationFeature feature = HttpAuthenticationFeature.digest(username, password);
    Client client = ClientBuilder.newBuilder().register(feature).build();

    return client.target("https://sse.example.org/")
      .request()
      .get();
}

HttpAuthenticationFeature в Jersey также поддерживает дайджест-аутентификацию:

public Response digestAuthenticationAtRequestLevel(String username, String password) {
    HttpAuthenticationFeature feature = HttpAuthenticationFeature.digest();
    Client client = ClientBuilder.newBuilder().register(feature).build();

    return client.target("http://sse.example.org/")
      .request()
      .property(HTTP_AUTHENTICATION_DIGEST_USERNAME, username)
      .property(HTTP_AUTHENTICATION_DIGEST_PASSWORD, password)
      .get();
}

И, аналогичным образом, мы можем переопределить во время запроса:

5.4. Аутентификация токена носителя с помощью OAuth 2.0

OAuth 2.0 поддерживает понятие токенов носителя в качестве еще одного механизма аутентификации.

<dependency>
    <groupId>org.glassfish.jersey.security</groupId>
    <artifactId>oauth2-client</artifactId>
    <version>2.29</version>
</dependency>

Нам понадобится зависимость Jersey oauth2-client, чтобы дать нам OAuth2ClientSupportFeature, которая похожа на HttpAuthenticationFeature:

public Response bearerAuthenticationWithOAuth2AtClientLevel(String token) {
    Feature feature = OAuth2ClientSupport.feature(token);
    Client client = ClientBuilder.newBuilder().register(feature).build();

    return client.target("https://sse.examples.org/")
      .request()
      .get();
}

Чтобы добавить токен носителя, мы будем следовать аналогичному шаблону, как и раньше:

public Response bearerAuthenticationWithOAuth2AtRequestLevel(String token, String otherToken) {
    Feature feature = OAuth2ClientSupport.feature(token);
    Client client = ClientBuilder.newBuilder().register(feature).build();

    return client.target("https://sse.example.org/")
      .request()
      .property(OAuth2ClientSupport.OAUTH2_PROPERTY_ACCESS_TOKEN, otherToken)
      .get();
}

Или мы можем переопределить на уровне запроса, что особенно удобно, когда токен меняется из-за ротации:

5.5. Аутентификация токена носителя с помощью OAuth 1.0

<dependency>
    <groupId>org.glassfish.jersey.security</groupId>
    <artifactId>oauth1-client</artifactId>
    <version>2.29</version>
</dependency>

В-четвертых, если нам нужно интегрироваться с устаревшим кодом, использующим OAuth 1.0, нам понадобится зависимость oauth1-client от Jersey:

public Response bearerAuthenticationWithOAuth1AtClientLevel(String token, String consumerKey) {
    ConsumerCredentials consumerCredential = 
      new ConsumerCredentials(consumerKey, "my-consumer-secret");
    AccessToken accessToken = new AccessToken(token, "my-access-token-secret");

    Feature feature = OAuth1ClientSupport
      .builder(consumerCredential)
      .feature()
      .accessToken(accessToken)
      .build();

    Client client = ClientBuilder.newBuilder().register(feature).build();

    return client.target("https://sse.example.org/")
      .request()
      .get();
}

«

«Аналогично OAuth 2.0, у нас есть OAuth1ClientSupport, который мы можем использовать:

Уровень запроса снова включается свойством OAuth1ClientSupport.OAUTH_PROPERTY_ACCESS_TOKEN.

6. Заключение