HTTPS авторизация по сертификату


Отправка простых HTTP запросов из Java уже хорошо описана в различных источниках, например в официальном туториале, поэтому пересказывать в очередной раз как работать с URL и выкладывать примеры кода не вижу смысла. В случае если приходится работать с защищенным HTTPS соединением, также особых осложнений у большинства программистов возникнуть не должно (возможно кроме случаев с самозаверенными сертификатами, но на эту тему также очень много учебного материала в интернете). Несколько больший интерес представляют собой случай, когда HTTPS подключение требует дополнительной авторизации по клиентскому сертификату. Вот об этом и пойдет речь в этой статье.

Для многих пользователей такой способ авторизации выглядит немного экзотичным, т.к. встречается он не часто, хотя работает “из коробки” в большинстве браузеров и поддерживается многими веб-серверами.

Более распространен такой способ авторизации не в клиент-серверных решениях, а скорее в межсерверном взаимодействии. Например при решения задач в сфере B2B, когда необходимо отправлять запросы к различным серверам контрагентов и внешним системам. Отчасти это связанно с тем, что одним из популярных способов создания таких коммуникаций является использование веб-служб, что в свою очередь означает программную отправку HTTP запросов. Поскольку внутренняя реализации JAX-WS, базируется на использовании стандартных классов (URL, HttpURLConnection и т.д.), решив задачу в общем виде (отправка https запросов с авторизацией по сертификату) автоматически решается задача для клиента веб-службы (речь идет о JAX-WS, в Axis2 задачу решать можно немного другим способом).

Самое простое что можно сделать для решения этой задачи — указать ключи и пароли в системных свойствах:

-Djavax.net.ssl.keyStore=privateKey.jks
-Djavax.net.ssl.keyStorePassword=myPrivateKeyPassword
-Djavax.net.ssl.trustStore=truststore.jks
-Djavax.net.ssl.trustStorePassword=myTrustStorePassword

В данном случае в файл privateKey.jks вы можете положить свой приватный (закрытый) ключ, с помощью которого будет проходить аутентификация на сервере и (при необходимости) в файл truststore.jks положить сертификат сервера, к которому идет обращение.

Хранилище ключей может иметь тип не только JKS (Java Key Store), но например быть в более распространенном формате PKCS12. В таком случае в настройках следует указать тип хранилища:

-Djavax.net.ssl.keyStoreType=pkcs12
-Djavax.net.ssl.keyStore=privateKey.p12                
-Djavax.net.ssl.keyStorePassword=myPrivateKeyPassword

Очень хорошо описаны отличия в использовании JKS и PKCS12 в статье Java 2-way TLS/SSL (Client Certificates) and PKCS12 vs JKS KeyStores.

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

Теперь рассмотрим более сложный случай в котором необходимо программно регулировать работу с ключами. Например, когда у вас в файле (или на смарт-карте) находится несколько ключей и нужно явно как-то их дополнительно анализировать, указывать какой ключ использовать.

Общая последовательность шагов будет выглядеть следующим образом.
1. Получить хранилище ключей.
2. Создать объект класса менеджера ключей.
3. Создать объект управляющий доверенными сертификатами.
4. Произвести инициализацию ssl-контекста.

Теперь нужно разобраться, кто это такой “менеджер ключей” и “управляющий доверенными сертификатами”, а также с кем еще возможно придется столкнуться. Для этого приведу сильно упрощенную диаграмму классов:
sslcontext

Из диаграммы легко видеть как можно управлять поведением работы программы. Например, чтобы поменять логику выбора алиаса, можно сделать наследника класса менеджера ключей (KeyManager).
Для этого вначале нужно получить хранилище ключей (KeyStore), а дальше сделав своего наследника, который будет работать с ключами, передать его в метод для инициализации SSL контекста. Самый простой способ сделать такого наследника — получить менеджера ключей по-умолчанию, а затем обернуть его методы таким образом, чтобы основные методы “проксировались” и в то же время можно было перекрыть нужный метод со специфической логикой (выбор алиас, отслеживание какой ключ/принципал используется и тд).

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

Далее привожу очень простой пример, естественно это “вырожденный” случай иллюстрирующий общую схему работы.
Код приводится в ознакомительных целях и использовать его “как есть” крайне не рекомендуется.


/**
 *
 * @author Vit vit@programmisty.com
 */
public class Demo {

    public static void main(String[] args) throws Exception {
        SSLContext sc = SSLContext.getInstance("SSL");

        // Каким-то образом получили хранилище ключей. Это за рамками статьи.
        KeyStore keystore = ... ;

        String algorithm = KeyManagerFactory.getDefaultAlgorithm();
        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(algorithm);
        // не забываем инициализировать менеджера ключей
        keyManagerFactory.init(keystore, password);
        // получаем список менеджером (при необходимости делаем наследника и/или оборачиваем keyManager)
        KeyManager[] keyManagers = keyManagerFactory.getKeyManagers();
        // если у нас свой список доверенных серверов
        TrustManager[] trustManagers = getTrustManagers("/trusted.jks", "trustedPassword".toCharArray());
        sc.init(keyManagers, trustManagers, null);
        // проставляем контекст
        SSLContext.setDefault(sc);
        // для проверки отправляем запрос
        String url = "https://my-secure-server.com";
        try (InputStream in = new URL(url).openStream()){
            System.out.println(IOUtils.toString(in));
        }
    }

    /**
     * если у нас какой-то свой файлик с доверенными сертификатами
     */
    private static TrustManager[] getTrustManagers(String path, char[] passwd) throws Exception {
        String algorithm = TrustManagerFactory.getDefaultAlgorithm();
        TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm);
        // Загружаем доверенных
        KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType());
        // Из ресурсов, так проще. 
        InputStream keystoreStream = Demo.class.getResourceAsStream(path);
        keystore.load(keystoreStream, passwd);
        trustManagerFactory.init(keystore);
        // 
        TrustManager[] trustManagers = trustManagerFactory.getTrustManagers();
        return trustManagers;
    }
}

Любое использование либо копирование материалов или подборки материалов сайта, элементов дизайна и оформления допускается лишь с разрешения правообладателя и только со ссылкой на источник: programador.ru

Телеграм канал: @prgrmdr
Почта для связи: vit [at] programmisty.com