a dev's blog

Some thoughts about thoughts.

Reverse Proxy capabilities within your Spring-Boot application

2016-02-10 Development Java Spring

Kein Apache httpd, kein nginx, nur deine Spring-Boot-Anwendung

Auf zum nächsten Artikel im Spring-Boot Umfeld. Heute schauen wir uns an, wie wir andere Dienste, die eigentlich unter einer anderen Adresse erreichbar sind, nativ über unsere Applikation erreichbar machen.

Als Beispiel soll die Integration einer Elasticsearch-Instanz dienen. Elasticsearch - um kurz einen Ausflug zu unternehmen - ist eine äußerst skalierbare Volltext-Suche mit der großartigen Eigenschaft, dass sie über ein REST-Interface angesprochen werden kann. Damit man mit Elasticsearch loslegen kann, muss man nichts anderes machen als das Binary auf elastic.co herunterladen, entpacken und die entpackte Datei bin/elasticsearch starten. Geht man darafhin auf die Adresse http://localhost:9200 wird man von einem JSON begrüßt, das einem ein paar Informationen zu dem gestarteten Elasticsearch-Cluster gibt.

Da Elasticsearch nur über Plugins abgesichert werden kann, sollte man besser keine plain Elasticsearch-Instanz auf irgendeinem Server starten ohne sich dessen sehr bewusst zu sein. Das erste was man in jedem Fall tun sollte ist - neben einer entsprechenden Firewall-Regel - ein Binding auf localhost. So… Genug von dem Ausflug. ;)

Wir wollen in jedem Fall dafür sorgen, dass wir die Elasticsearch-Instanz über unsere Webapplikation aufrufen können - sie also zum Beispiel via http://localhost:8080/elasticsearch erreichen. Dies bietet uns bspw. die Möglichkeit die REST-Endpunkte der Elasticsearch mittels Spring-Security abzusichern. Wir könnten definieren, welche User mit welchen Rollen Zugriff auf welche Ressourcen besitzen. Wie man das macht werde ich in einem anderen Artikel erklären, der noch in Planung ist ;)

David Smiley’s Reverse-Proxy-Servlet

Es gibt da einen Menschen namens David Smiley und dieser hat vor recht langer Zeit bereits ein ziemlich cooles Stück Code geschrieben, nämlich sein Reverse-Proxy-Servlet welches wir benutzen werden um die gewünschte Funktionalität herzustellen.

Setup

Wieder einmal findet ihr ein entsprechendes Projekt auf meinem Github Account.

Im Kern ist gar nicht allzu viel zu tun. Wir haben in unserer pom.xml folgende Dependencies:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
  <groupId>org.mitre.dsmiley.httpproxy</groupId>
  <artifactId>smiley-http-proxy-servlet</artifactId>
  <version>1.7</version>
</dependency>

…sowie eine SpringBootApplication:

@SpringBootApplication
public class ReverseProxyApplication {

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

Damit läuft zumindest schonmal unsere Applikation - natürlich ohne irgendeinen Zweck zu erfüllen. ;)

Remote-Servlet Konfiguration

Was wir jetzt brauchen ist ein Servlet, dass Requests unter einem bestimmten Pfad entgegennimmt und an die Elasticsearch weiterreicht. Da wir ein wunderschönes Spring-Boot-Projekt haben, welches ohne web.xml auskommt, müssen wir uns über den Spring-Way eine Komponente bauen, die uns eine ServletRegistrationBean liefert.

@Configuration
public class ElasticsearchServletConfiguration {

  @Bean(name = "elasticsarchServlet")
  public ServletRegistrationBean elasticsearchServletRegistrationBean() {
    ProxyServlet proxyServlet = new ProxyServlet();

    Map<String, String> initParams = new HashMap<>();
    initParams.put("targetUri", "http://localhost:9200");

    ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(proxyServlet);
    servletRegistrationBean.setName("elasticsearch");
    servletRegistrationBean.setUrlMappings(new ArrayList<>(Arrays.asList("elasticsearch/*")));
    servletRegistrationBean.setInitParameters(initParams);

    return servletRegistrationBean;
  }
}

In Zeile 6 defininieren wir uns zunächst mal ein ProxyServlet, welches wir in Zeile 11 an die ServletRegistrationBean übergeben. Der Bean geben wir dann noch die init-Parameter für das ProxyServlet sowie ein URL-Mapping mit…. und fertig.

Da ich ein Fan von ausgelagerten Konfigurationen bin, packen wir noch ein paar Properties in unsere application.properties und passen unsere Servlet-Konfiguration entsprechend an.

proxy.elasticsearch.targetUri=http://localhost:9200
proxy.elasticsearch.servletUrl=/elasticsearch/*
@Configuration
public class ElasticsearchServletConfiguration {

  @Value("${proxy.elasticsearch.targetUri}")
  private String targetUri;

  @Value("${proxy.elasticsearch.servletUrl}")
  private String servletUrl;


  @Bean(name = "elasticsarchServlet")
  public ServletRegistrationBean elasticsearchServletRegistrationBean() {
    ProxyServlet proxyServlet = new ProxyServlet();

    Map<String, String> initParams = new HashMap<>();
    initParams.put("targetUri", targetUri);

    ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(proxyServlet);
    servletRegistrationBean.setName("elasticsearch");
    servletRegistrationBean.setUrlMappings(new ArrayList<>(Arrays.asList(servletUrl)));
    servletRegistrationBean.setInitParameters(initParams);

    return servletRegistrationBean;
  }
}

Ein Aufruf von http://localhost:8080/elasticsearch liefert uns jetzt ein wunderhübsches JSON mit unseren Cluster-Informationen. YAY!!

{
  "name" : "Eric Slaughter",
  "cluster_name" : "elasticsearch",
  "version" : {
    "number" : "2.2.0",
    "build_hash" : "8ff36d139e16f8720f2947ef62c8167a888992fe",
    "build_timestamp" : "2016-01-27T13:32:39Z",
    "build_snapshot" : false,
    "lucene_version" : "5.4.1"
  },
  "tagline" : "You Know, for Search"
}

Fazit

Wir haben gesehen, dass es mit wenigen Zeilen Code möglich ist andere Dienste hinter einer URL seiner Spring-Boot-Applikation zu platzieren. Damit können wir jetzt beispielsweise unser Applikations-Berechtigungssystem oder unser Logging für die integrierten Endpunkte nutzen! :)