Lectura http y https desde java

De ChuWiki

Vamos a ver cómo leer desde java una página web ofrecida por un servidor http, por un servidor https que no requiere certificado de cliente y por un servidor https que sí requiere certificado de cliente.

Primero haremos aproximaciones con código java. Al final un ejemplo sin tanto rollo de código, pero usando las System Properties de java.


Servidor http[editar]

El código para leer una página web ofrecido por un servidor http normalito puede ser el siguiente

package com.chuidiang.ejemplos;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;

public class PruebaHTTP {
   public static void main(String[] args) {
      try {
         // Se abre la conexión
         URL url = new URL("http://www.chuidiang.com");
         URLConnection conexion = url.openConnection();
         conexion.connect();
         
         // Lectura
         InputStream is = conexion.getInputStream();
         BufferedReader br = new BufferedReader(new InputStreamReader(is));
         char[] buffer = new char[1000];
         int leido;
         while ((leido = br.read(buffer)) > 0) {
            System.out.println(new String(buffer, 0, leido));
         }
      } catch (MalformedURLException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      } catch (IOException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      }
   }
}

El código es sencillo. Hacemos un new URL() con la URL en cuestión. Llamamos al método openConnection() y luego connect(). Sólo nos queda obtener el InputStream de lectura y leer en él como mejor nos venga para nuestras necesidades. En el ejemplo vamos leyendo en buffer de caracteres y sacándolo por pantalla, por lo que obtendremos en pantalla el texto html de la página cuya URL hemos abierto (suponiendo que esa URL tenga un texto html).


Servidor https que no requiere certificado de cliente[editar]

Cuando se abre una conexión con https contra un servidor, el servidor nos presenta un certificado digital. El cliente debe decidir si acepta o no ese certificado digital. Todos hemos visto esto en alguna ocasión cuando navegamos por internet. Las páginas https que usamos ofrecen un certificado digital. Si nuestro navegador confía en él, todo va de perlas. Si nuestro navegador no confía en ese certificado, nos mostrará un aviso de si estamos seguros que queremos visitar esa página.

El código para leer de un servidor https que no requiere certificado de cliente es en principio exactamente igual que el anterior salvo por dos pequeños detalles:

  • En la URL, en vez de http://.... ponemos https://.....
  • Debemos indicar a nuestro código java en qué certificados de servidor confiamos. Si no obtendremos error.

Para indicar a nuestro código java los certificados en los que debe confiar, necesitamos tener esos certificados en ficheros. Cualquier navegador, visitando la página en cuestión, suele ofrecer en algún sitio la posibilidad de ver los detalles del certificado del servidor y guardarlos. En la barra de la URL del navegador suele aparecer un candadito cuando visitamos una página https. Pinchando el candadito podremos ver los datos del certificado y en la ventana que sale tenemos opción de guardarlo en algún fichero.

Una vez que tenemos el o todos los certificados necesarios, debemos guardarlos en un único fichero de almacén de certificados. Podemos hacerlo, entre otras, con openssl o con keytool. Vamos con esta última puesto que viene con java. Si hemos guardador los distintos certificados en servidor1.cer, servidor2.cer, etc, el comando para crear el almacén de certificados de esta manera

keytool -importcert -alias servidor1 -keystore .keystore -file servidor1.cer
keytool -importcert -alias servidor2 -keystore .keystore -file servidor2.cer
...

A cada certificado debemos darle un "alias" distinto para identrificarlo dentro del almacén. En el ejemplo los hemos llamado servidor1 y servidor2. Con -keystore .keystore estamos indicando que nuestro fichero de almacén será ".keystore", podemos darle el nombre que queramos, pero usaremos el mismo nombre cada vez si queremos que los distintos certificados se vayan añadiendo a ese fichero. Finalmente, con -file vamos indicando cada uno de los certificados que nos hemos bajado con el navegador. Al ejecutar estos comandos nos pedirá una password. Puede ser la que queramos, pero es importante que recordarla, ya que es la que nos permitirá leer más adelante el contenido de ese fichero, añadir más certificados o borrar los existentes. Esa password deberemos usarla también en nuestro código java.

El código java para leer el contenido de una página ofrecida bajo https puede ser como este

package com.chuidiang.ejemplos;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

public class PruebaHTTPSOneWay {
   public static void main(String[] args) {
      try {
         // Carga del fichero que tiene los certificados de los servidores en
         // los que confiamos.
         InputStream fileCertificadosConfianza = new FileInputStream(new File(
               "c:/unpath/.keystore"));
         KeyStore ksCertificadosConfianza = KeyStore.getInstance(KeyStore
               .getDefaultType());
         ksCertificadosConfianza.load(fileCertificadosConfianza,
               "unaPassword".toCharArray());
         fileCertificadosConfianza.close();

         // Ponemos el contenido en nuestro manager de certificados de
         // confianza.
         TrustManagerFactory tmf = TrustManagerFactory
               .getInstance(TrustManagerFactory.getDefaultAlgorithm());
         tmf.init(ksCertificadosConfianza);

         // Creamos un contexto SSL con nuestro manager de certificados en los
         // que confiamos.
         SSLContext context = SSLContext.getInstance("TLS");
         context.init(null, tmf.getTrustManagers(), null);
         SSLSocketFactory sslSocketFactory = context.getSocketFactory();

         // Abrimos la conexión y le pasamos nuestro contexto SSL
         URL url = new URL("https://una.url.com");
         URLConnection conexion = url.openConnection();
         ((HttpsURLConnection) conexion).setSSLSocketFactory(sslSocketFactory);

         // Ya podemos conectar y leer
         conexion.connect();
         InputStream is = conexion.getInputStream();
         BufferedReader br = new BufferedReader(new InputStreamReader(is));
         char[] buffer = new char[1000];
         int leido;
         while ((leido = br.read(buffer)) > 0) {
            System.out.println(new String(buffer, 0, leido));
         }
      } catch (MalformedURLException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      } catch (IOException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      } catch (KeyStoreException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      } catch (NoSuchAlgorithmException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      } catch (CertificateException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      } catch (KeyManagementException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      }
   }
}

En el primer bloque de código leemos el fichero de almacén de certificados que generamos con keytool. Debemos indicar la password que usamos para crear ese fichero almacén. También debemos indicar de qué tipo es el fichero almacén, con la línea que dice KeyStore.getInstance(KeyStore.getDefaultType()). El defaultType, cómo no, es el formato que usa java y la herramienta keytool.

Dejamos el segundo bloque para explicar después.

En el tercer bloque creamos un contexto SSL y a él tenemos que pasarle un TrutManager (en realidad un array de TrustManager). Este TrusManager no es más que una clase a la que el socket SSL (nuestra conexión https) preguntará si un certificado ofrecido por un servidor es o no válido. Este TrusManager se pasa como segundo parámetro en el método context.init() del SSLContext.

¿Y cómo obtenemos ese TrusManager a partir de nuestro almacén .keystore?. A través de una fábrica de TrusManager, es decir, a partir de la clase TrustManagerFactory. Ese es nuestro segundo bloque de código. Obtenemos esa fábrica con el método TrustManagerFactory.getInstante() y la inicializamos con el KeyStore que cargamos en el primer paso.

Ya sólo nos queda preparar la conexión como hicimos antes, pasarle nuestro contexto SSL y leer de la forma habitual.

Servidor https que requiere certificado de cliente[editar]

El certificado de cliente es un fichero que normalmente alguien nos proporcionará para que podamos acceder a un servidor que lo requiera. Sin él no podremos acceder. Este fichero suele tener extensión .p12 y debemos cargarlo también en nuestro código java de una forma similar a como hicimos con el almacen de certificados de servidor que admitimos.

El código java puede ser como este

package com.chuidiang.ejemplos;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.security.KeyManagementException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.CertificateException;

import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;

public class PruebaHTTPSTwoWay {
   public static void main(String[] args) {
      try {
         // Carga del fichero que tiene los certificados de los servidores en
         // los que confiamos.
         InputStream fileCertificadosConfianza = new FileInputStream(new File(
               "c:/unpath/.keystore"));
         KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
         ks.load(fileCertificadosConfianza, "unaPassword".toCharArray());
         fileCertificadosConfianza.close();

         // Ponemos el contenido en nuestro manager de certificados de
         // confianza.
         TrustManagerFactory tmf = TrustManagerFactory
               .getInstance(TrustManagerFactory.getDefaultAlgorithm());
         tmf.init(ks);

         // Cargamos el fichero con el certificado de cliente
         InputStream fileCertificadoCliente = new FileInputStream(
               new File(
                     "c:/unpath/unCertificadoCliente.p12"));
         KeyStore ksCliente = KeyStore.getInstance("PKCS12");
         ksCliente.load(fileCertificadoCliente, "unaPassword".toCharArray());
         fileCertificadoCliente.close();

         // Creamos un manager de certificados con nuestro certificado de
         // cliente
         KeyManagerFactory kmf = KeyManagerFactory
               .getInstance(KeyManagerFactory.getDefaultAlgorithm());
         kmf.init(ksCliente, "unaPassword".toCharArray());

         // Creamos un contexto SSL tanto con el manger de certificados de
         // servidor en los que confiamos como con el manager de certificados de
         // cliente de los que disponemos
         SSLContext context = SSLContext.getInstance("TLS");
         context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
         SSLSocketFactory sslSocketFactory = context.getSocketFactory();

         // Abrimos la conexión y le pasamos el contexto SSL que hemos creado
         URL url = new URL("https://un.servidor.que.requiere.certificado.cliente.com");
         URLConnection conexion = url.openConnection();
         ((HttpsURLConnection) conexion).setSSLSocketFactory(sslSocketFactory);

         // Ya podemos leer.
         conexion.connect();
         InputStream is = conexion.getInputStream();
         BufferedReader br = new BufferedReader(new InputStreamReader(is));
         char[] buffer = new char[1000];
         int leido;
         while ((leido = br.read(buffer)) > 0) {
            System.out.println(new String(buffer, 0, leido));
         }
      } catch (MalformedURLException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      } catch (IOException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      } catch (KeyStoreException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      } catch (NoSuchAlgorithmException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      } catch (CertificateException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      } catch (KeyManagementException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      } catch (UnrecoverableKeyException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      }
   }
}

El código es básicamente igual al del ejemplo anterior, pero hemos añadido la carga del certificado de cliente.

En un primero trozo de código, cargamos en un KeyStore el certificado de cliente unCertificadoCliente.p12

Como al contexto SSL tendremos que pasarle, además del TrustManager anterior, un KeyManager con la clave de cliente, primero obtenemos una fábrica de KeyManager (un KeyManagerFactory) al que inicializamos con el KeyStore que es el certificado de cliente. Este KeyManagerFactory nos permitirá obtener el KeyManager que pasaremos, un poco más abajo, como primer parámetro del contexto SSL.

Ahora sólo queda pasar el contexto SSL a la URLConnection, abrirla y leer de la forma habitual.

Un detalle nada más. Un certificado de cliente p12 está en un formato PKCS12, por eso en el KeyStore usamos un getInstance("PKCS12") en vez de un getDefaultType(), que corresponde a "JKS" que es el formato por defecto que da keytool a los almacenes de certificados. También tenemos que pasar la clave que habitualmente protege estos certificados.


https usando java system properties[editar]

Podemos hacer este último ejemplo sin necesidad de tanto código. Nos bastaría con configurar todo usando las System Properties de java. El código quedaría tan sencillo como esto

package com.chuidiang.ejemplos;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;

public class PruebaHTTPSTwoWayProperties {
   public static void main(String[] args) {
      try {
         System.setProperty("javax.net.ssl.trustStore", "c:/unpat/.keystore");
         System.setProperty("javax.net.ssl.trustStorePassword", "unaPassword");
         System.setProperty("javax.net.ssl.trustStoreType", "JKS");

         System.setProperty("javax.net.ssl.keyStore", "c:/unpath/unCertificadoCliente.p12");
         System.setProperty("javax.net.ssl.keyStorePassword", "unaPassword");
         System.setProperty("javax.net.ssl.keyStoreType", "PKCS12");

         // Abrimos la conexión y le pasamos el contexto SSL que hemos creado
         URL url = new URL("https://un.servidor.que.requiere.certificado.cliente.com");
         URLConnection conexion = url.openConnection();

         // Ya podemos leer.
         conexion.connect();
         InputStream is = conexion.getInputStream();
         BufferedReader br = new BufferedReader(new InputStreamReader(is));
         char[] buffer = new char[1000];
         int leido;
         while ((leido = br.read(buffer)) > 0) {
            System.out.println(new String(buffer, 0, leido));
         }
      } catch (MalformedURLException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      } catch (IOException e) {
         // TODO Auto-generated catch block
         e.printStackTrace();
      }
   }
}

La diferencia es que estas propiedades afectarían a todo nuestro programa en java. Puede ser efectivo si todas nuestras conexiones van a usar y admitir los mismos certificados. Sin embargo si queremos que nuestro programa abra diferentes conexiones usando/admitiendo diferentes conjuntos de certificados, debemos hacerlo con código, ya que ahí podemos pasar un SSLContext específico a cada conexión que abramos.