1. Introducción

En este post veremos como arrancar un microservicio de SpringBoot haciendo uso del protocolo https. Os podéis descargar el código de ejemplo de mi GitHub aquí.

Tecnologías empleadas:

  • Java 8
  • Gradle 3.1
  • SpringBoot 1.5.2.RELEASE
  • Spring 4.3.7.RELEASE

2. Estructura del proyecto

3. Configuración para crear el conector https

El fichero build.gradle

group 'com.jorgehernandezramirez.spring.springboot'
version '1.0-SNAPSHOT'

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:1.5.2.RELEASE")
    }
}

apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'maven'

sourceCompatibility = 1.8

springBoot {
    mainClass = "com.jorgehernandezramirez.spring.springboot.https.Application"
}

repositories {
    mavenCentral()
}

dependencies {
    compile("org.springframework.boot:spring-boot-starter-web")
}

El fichero main que arranca SpringBoot

package com.jorgehernandezramirez.spring.springboot.https;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {

    public Application(){
        //For Spring
    }

    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

Nos creamos un controlador de pruebas.

package com.jorgehernandezramirez.spring.springboot.https.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/ping")
public class TestController {

    @RequestMapping
    public String doAlive() {
        return "Alive!";
    }

    @RequestMapping("/rest")
    public String doRestAlive() {
        return new RestTemplate()
                .getForObject("https://localhost:8443/ping", String.class);

    }
}

A continuación viene lo verdaderamente relevante. Vamos a crear un almacén de claves y situarlo en la raíz de nuestro classpath. Para ello vamos a utilizar la herramienta keytool que viene integrada en la jdk.

keytool -genkey -alias springboot -storetype PKCS12 -keyalg RSA -keysize 2048 -keystore keystore.p12 -validity 3650

PD: La password de nuestro almacén de claves, que se nos solicitó en la creación del mismo, fue 12345678

Para habilitar la configuración para que nuestro servidor escuche peticiones por el protocolo https, deberemos hacer.

server:
   port: 8443
   ssl:
      key-store: classpath:keystore.p12
      key-store-password: 12345678
      keyStoreType: PKCS12
      keyAlias: springboot

4. Configuración para habilitar el conector http y https

La configuración vista anteriormente sólo permite atacar al servidor utilizando el protocolo https. Sin embargo si estamos interesados en habilitar, además, el protocolo http deberemos hacer.

Configuración del conector http

server:
   port: 8080

Configuración del conector https
Lo deberemos hacer a través de la creación del objeto EmbeddedServletContainerFactory

package com.jorgehernandezramirez.spring.springboot.https.configuration;

import org.apache.catalina.connector.Connector;
import org.apache.coyote.http11.Http11NioProtocol;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.embedded.EmbeddedServletContainerFactory;
import org.springframework.boot.context.embedded.tomcat.TomcatEmbeddedServletContainerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.net.ssl.HttpsURLConnection;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;

@Configuration
public class SSLTomcatConfiguration {

    private static final String CONNECTOR_CLASS = "org.apache.coyote.http11.Http11NioProtocol";

    private static final String HTTPS = "https";

    private static final Integer PORT = 8443;

    private static final String KEYSTORE_FILE = "classpath:keystore.p12";

    private static final String KEYSTORE_PASSWORD = "12345678";

    @Bean
    public EmbeddedServletContainerFactory servletContainer() throws KeyManagementException, NoSuchAlgorithmException, InterruptedException, IOException {
        final TomcatEmbeddedServletContainerFactory tomcat = new TomcatEmbeddedServletContainerFactory();
        tomcat.addAdditionalTomcatConnectors(createSslConnector());
        return tomcat;
    }

    private Connector createSslConnector() throws InterruptedException, IOException {
        final Connector connector = new Connector(CONNECTOR_CLASS);
        final Http11NioProtocol protocol = (Http11NioProtocol)connector.getProtocolHandler();
        connector.setScheme(HTTPS);
        connector.setSecure(true);
        connector.setPort(PORT);
        protocol.setSSLEnabled(true);
        protocol.setKeystoreFile(KEYSTORE_FILE);
        protocol.setKeystorePass(KEYSTORE_PASSWORD);
        return connector;
    }
}

5. Probando la aplicación

curl -k https://localhost:8443/ping

La respuesta obtenida fue

Alive!

curl -k https://localhost:8443/ping/rest

La respuesta obtenida fue

There was an unexpected error (type=Internal Server Error, status=500).
I/O error on GET request for “https://localhost:8443/ping”: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException

La petición anterior hace una llamada rest a un controlador que hace a su vez una llamada a /ping haciendo uso de la clase RestTemplate de Spring.

return new RestTemplate().getForObject("https://localhost:8443/ping", String.class);

La excepción se produce ya que el certificado que ha sido enviado por el servidor no es confiable, produciendose la excepción SunCertPathBuilderException.

Solución

Exportar el certificado de nuestro almacén de claves

keytool -export -keystore keystore.p12 -alias springboot -file mycertificate.cer

Importarlo al almacén de confianza

El trustore se puede pasar a la JVM como variable de entorno en el arranque de la aplicación, haciendo

-Djavax.net.ssl.trustStrore=[PATH_TO_TRUSTSTORE] -javax.net.ssl.keyStorePassword=[PASSWORD]

Vamos a importar nuestro certificado mycertificate.cer al almacén de confianza por defecto que utiliza la JDK y que se encuentra en $JAVA_HOME/jre/lib/security/cacerts

keytool -import -keystore $JAVA_HOME/jre/lib/security/cacerts -alias springboot -file mycertificate.cer

PD: Por defecto, la contraseña del almacén de confianza cacerts de la JDK es changeit

Si volvemos a atacar a la url ya funcionaría

curl -k https://localhost:8443/ping/rest

La respuesta obtenida fue

Alive!