Elasticsearch – Sécurité et accès via JAVA et Spring MVC

De Fabien Gaujous dans Technique

9 déc 2013

Dans les 2 articles précédents, nous avons mis en place Elasticsearch (Mise en place et Astuces)

Une fois les données indexées dans Elasticsearch, la recherche doit être accessible sur le web par des services tiers externes.
Ainsi, cet article traite de ces différents points :

  • La gestion de la sécurité pour ne pas qu’une personne tierce puisse exécuter une commande type (curl -XDELETE…) et ainsi supprimer toutes nos données.
  • L’accès à Elasticsearch via des Web services REST pour effectuer des recherches.

La sécurité

Une des possibilités pour sécuriser l’accès à Elasticsearch est de ne pas rendre accessible le moteur de recherche directement sur internet afin de le protéger des personnes malveillantes ;-) .
Seules des personnes identifiées sur le réseau interne peuvent avoir accès aux différents nodes d’Elasticsearch (Via des règles firewall) et ainsi à toutes les commandes en HTTP notamment à l’interface du plugin Head pour la gestion du cluster si besoin.
Par défaut ElasticSearch n’est fourni avec aucune authentification, mais un plugin jetty qui rajoute cette fonctionnalité existe : voir le blog de David Pilato à ce sujet

Accès aux données

Nous avons fait le choix de créer des Web services qui servent de Façade pour accéder à Elasticsearch. Ce sont eux qui sont accessibles depuis internet pour 2 raisons principales :

  • Gérer la sécurité et donc l’accès aux données depuis notre application Web
  • Gérer l’accès au cluster Elasticsearch
  • Masquer la complexité d’Elasticsearch pour fournir un Web service plus fonctionnel lié au métier. Ainsi, les développeurs utilisant nos Web services n’ont pas besoin de connaitre comment fonctionne Elasticsearch ni même de savoir que c’est ElasticSearch qui est caché derrière.

Elasticsearch, basé sur Netty, fonctionne donc sur le model “Non blocking IO”.
Nous avons décidé de mettre en place des WS REST (En prenant en compte les bonnes pratiques REST et plus particulièrement ce livre très interessant de APIGEE)
sur la base de Spring MVC mais en utilisant les dernières nouveautés de Spring MVC 3.2 :
L’implémentation de la spécification 3.0 des servlets : La gestion asynchone des requêtes HTTP.

L’application est déployée sous forme de WAR dans le serveur d’application Tomcat 7 (La version 7 est la première à implémenter les Servlet 3.0).
Afin de faire fonctionner en mode NIO, voici les 2 modifications à effectuer :

Dans le fichier server.xml, modifier le connecteur HTTP avec le connecteur NIO approprié

1
<Connector URIEncoding="UTF-8" connectionTimeout="20000" port="8080" protocol="org.apache.coyote.http11.Http11NioProtocol" redirectPort="8443"/>

Dans le web.xml de l’application, Mettre la bonne DTD 3.0 ainsi que le paramètre async-supported sur TOUS les servlets et filter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:j2ee="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
version="3.0"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/j2ee/web-app_3_0.xsd">
...
    <filter>
        <filter-name>characterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <async-supported>true</async-supported>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
        
    </filter>
    <servlet>
        <servlet-name>springapp</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
        <async-supported>true</async-supported>
    </servlet>
    <servlet-mapping>
        <servlet-name>springapp</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

Accès à Elasticsearch

L’accès à Elasticsearch se fait via l’API JAVA qui permet de créer un Client pour accéder à toutes les fonctionnalités (indexation,recherche…).
Si vous utilisez Maven pour la gestion de dépendance, il faut ajouter cette dépendance :

1
2
3
4
5
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>VOTRE VERSION ELASTICSEARCH</version>
</dependency>

Une configuration particulière permet de gérer l’accès non pas à un seul node mais à tout le cluster grâce à la fonctionnalité de “sniff” (à partir d’un node, le client retrouve les autres nodes du cluster).
Le côté le plus intéressant est qu’il gère lui même le load balancing (round-robin) sur les différents nodes pour distribuer les requêtes mais détecte aussi les arrêts ou démarrage de nouveaux nodes.

1
2
3
4
5
6
7
Settings settings = ImmutableSettings.settingsBuilder()
       .put("cluster.name", "cluster-prod") //Nom du cluster
       .put("client.transport.sniff", true) //Activer le mode sniff
       .build();
Client client = new TransportClient(settings).addTransportAddress(new InetSocketTransportAddress(localhost,
                9300));

Attention : Gérer le Client avec un modèle singleton pour ne pas avoir à instancier un nouveau client à chaque fois….

Pour finir, il ne reste plus qu’à créer les Web services Spring en utilisant le type Callable pour rendre nos appels asynchrones.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    @RequestMapping(value = "/v1/users/{id}", method = RequestMethod.GET, produces = "application/json;charset=UTF-8")
    @ResponseBody
    public Callable<String> getUser(@PathVariable(value = "id") final Long id) {
        return new Callable<String>() {
            @Override
            public String call() throws WebserviceException {
                LOG.info("GET /v1/users/" + id);
                GetResponse response = null;
                response = client.prepareGet("index", "type", id.toString()).execute()
                        .actionGet();
                return response.getSourceAsString();
            }
        };
    }

Si vous avez besoin d’analyser le contenu des informations renvoyées par Elastisearch et par exemple, renvoyer seulement le contenu des “hits”, il faut pour cela créer un XContentBuilder :

1
2
3
4
5
6
7
8
9
10
XContentBuilder builder;
builder = XContentFactory.jsonBuilder();
builder.startObject();
SearchRequestBuilder searchRequestBuilder = client.prepareSearch("index");
SearchResponse searchResp = searchRequestBuilder.setSize(RESULTMAX).setTypes("type")
.setQuery(queryBuilder).execute().actionGet();
searchResp.getHits().toXContent(builder, ToXContent.EMPTY_PARAMS);
builder.endObject();
String result = builder.string();
return result;

Nous pouvons ainsi rajouter toutes les authentifications et autorisations que nous souhaitons (OAUTH2…).

Server Web frontal :

Si dans la plupart de vos projets web, vous utilisez un serveur Web devant Tomcat (type Apache), afin de palier aux problème connu C10K et la création d’un thread par requête (comme le fait Apache), vous pouvez à la place utiliser nginx.
Vous aurez ainsi toute votre stack asynchone ;-)

Ressources

Utilisation du JAVA Client API Elasticsearch sur Stackoverflow

Commentaire

5 − = deux

iMDEO recrute !

REJOIGNEZ-NOUS

A la recherche de nouveaux talents (développeurs web et mobile, chefs de projet,...)

Voir les annonces