Skip to main content

Créer une API sécurisée avec gRPC

Écrit par
Headshot of Vitalis Ogbonna

Vitalis Ogbonna

hero build api grpc

25 août 2022

0 minutes de lecture

Un appel de procédure distante de Google (gRPC) est la version open source de Google du framework d’appel de procédure distante (RPC). Il s’agit d’un protocole de communication qui s’appuie sur HTTP/2 et les technologies de tampon de protocole (protobuf). gRPC permet à un client ou à un serveur distant de communiquer avec un autre serveur en appelant simplement la fonction de ce dernier comme si elle était locale. Dans les systèmes distribués, la communication et le transfert de grands volumes de données entre client et serveur s’en trouvent simplifiés.

Comme les autres systèmes RPC, gRPC définit un service. Il en spécifie les méthodes et les types de retour à l’aide de protobuf — un protocole de sérialisation et de désérialisation de Google — afin de faciliter la définition des services et la génération automatique des bibliothèques clientes. gRPC utilise ce protocole, actuellement en version 3, comme langage de définition d’interface et ensemble d’outils de sérialisation.

Pour la plupart des applications modernes, gRPC est un excellent choix grâce à sa prise en charge remarquable de tous les types de données. Il convient particulièrement aux volumes de données importants, comme les données diffusées en continu, et peut être excessif pour les applications simples où le transfert de grandes quantités de données est peu préoccupant.

Cet article vous montre comment utiliser gRPC pour faire communiquer un client et un serveur, à la manière d’applications, entre deux applications Node.js. Nous présenterons également quelques mesures de sécurité à prendre lors de l’utilisation de gRPC comme mécanisme de communication dans vos services.

Prérequis du tutoriel

Pour suivre ce tutoriel, vous devez avoir installé OpenSSL et Node.js (version 4.0 ou ultérieure) sur votre ordinateur. Une bonne compréhension de Node.js et de JavaScript est indispensable. Vous devez également vous assurer que votre environnement de travail dispose des privilèges d’administrateur.

Configurer le projet Node.js

Pour commencer, créez un dossier appelé event-app-node-grpc afin de définir la structure des dossiers de l’application, puis initialisez un projet Node.js avec npm en saisissant les commandes suivantes :

#bash
$ mkdir event-app-node-grpc
$ cd event-app-node-grpc
$ npm init -y

Une fois votre application initialisée, créez la structure de dossiers suivante. Vous pouvez consulter sur GitHub le code complet utilisé dans ce tutoriel :

Event-app-node-grpc
client
app.js
index.js
server
index.js
scripts
generate-certs.sh
events.proto
README.md

Installer les packages

Dans le terminal, accédez au répertoire racine de votre application. Installez les packages suivants à l’aide de la commande npm install, comme indiqué dans l’extrait de code ci-dessous :

#bash
$ npm install express @grpc/grpc-js @grpc/proto-loader

Voyons à quoi servent les packages que vous venez d’installer dans l’extrait de code ci-dessus :

  • Express est le serveur HTTP de votre application.

  • @grpc/grpc-js est une bibliothèque gRPC pour Node.js. Elle nous permet de créer un service gRPC dans l’environnement d’exécution Node.js.

  • @grpc/proto-loader est un package nécessaire au chargement des fichiers protobuf utilisés avec gRPC. Il utilise le package version 3 de protobuf.js.

Après avoir installé les packages ci-dessus, ouvrez le fichier package.json et ajoutez les configurations supplémentaires suivantes aux balises scripts, comme indiqué dans l’extrait de code ci-dessous :

#package.json

"scripts": {
   "start": "node server/index.js",
   "generate:certs": "./scripts/generate-certs.sh"
 },

Les configurations supplémentaires de l’extrait de code ci-dessus concernent la configuration de l’environnement d’exécution de l’application et la génération du certificat SSL. Une fois ces configurations ajoutées, votre fichier package.json mis à jour devrait ressembler à l’extrait de code ci-dessous :

#package.json(updated)

{
 "name": "event-app-node-grpc",
 "version": "1.0.0",
 "description": "A CRUD application to demonstrate the use of gRPC with NodeJS",
 "main": "server/index.js",
 "scripts": {
   "start": "node server/index.js",
   "generate:certs": "./scripts/generate-certs.sh"
 },
 "author": "(author name here)",
 "license": "ISC",
 "dependencies": {
   "@grpc/proto-loader": "^0.6.12",
   "express": "^4.18.1",
   "@grpc/grpc-js": "^1.6.12
 }
}

L’extrait de code ci-dessus présente le fichier package.json mis à jour après l’ajout, dans la balise scripts, des commandes de configuration de l’environnement d’exécution de l’application et de génération du certificat SSL.

Définir le tampon de protocole

Ce tutoriel explique comment utiliser gRPC dans une application simple de suivi d’événements. Cette application de démonstration récupère les détails d’un événement et les enregistre dans une base de données en mémoire, tout en permettant de mettre à jour, récupérer et supprimer les données de l’événement.

Dans les applications gRPC, l’interface de service et les charges utiles requises sont définies dans un fichier protobuf afin de permettre la communication entre différentes applications. Les fichiers protobuf ont l’extension .proto, comme illustré dans le schéma de configuration de notre projet.

Dans le répertoire racine de votre application, créez maintenant un fichier events.proto et ajoutez-y le code suivant. Vous pouvez vous reporter au schéma de structure du projet défini précédemment.

#events.proto

syntax = "proto3";

service EventService {
   rpc GetAllEvents (Empty) returns (EventList) {}
   rpc GetEvent (EventId) returns (Event) {}
   rpc CreateEvent (Event) returns (Event) {}
   rpc UpdateEvent (Event) returns (Event) {}
   rpc DeleteEvent (EventId) returns (Empty) {}
}

message Empty {}

message Event {
   string id = 1;
   string name = 2;
   string description = 3;
   string location = 4;
   string duration = 5;
   int32 lucky_number = 6;
   string status = 7;
}

message EventList {
   repeated Event events = 1;
}

message EventId {
   string id = 1;
}

Dans les extraits de code de définition proto ci-dessus, nous avons d’abord spécifié la version du tampon de protocole à l’aide de la définition syntax = "proto3", puis défini le service de protocole.

Ensuite, dans la description du service d’événements du protocole, nous avons créé un service appelé EventService. Nous avons ensuite créé des fonctions rpc au sein de ce service, avec leurs paramètres requis et leurs valeurs de retour attendues. Vous pouvez définir autant de services que nécessaire pour votre application, mais, pour simplifier, nous n’en définissons qu’un seul.

Nous avons également défini les types de données de la fonction rpc dans la définition EventService, ainsi que les valeurs de retour à l’aide du système de numérotation des champs propre à gRPC. Celui-ci indique le nombre d’octets utilisés lors de l’encodage. Pour en savoir plus, consultez la documentation officielle de protobuf.

Créer le serveur gRPC

En suivant la structure de dossiers ci-dessus, créez un dossier server dans le répertoire racine de votre application, puis créez un fichier index.js dans ce dossier. Collez l’extrait de code suivant dans le fichier server/index.js que vous venez de créer :

#server/index.js

const PROTO_PATH = "./events.proto";

let grpc = require("@grpc/grpc-js");
let protoLoader = require("@grpc/proto-loader");

let packageDefinition = protoLoader.loadSync(PROTO_PATH, {
   keepCase: true,
   longs: String,
   enums: String,
   arrays: true
});

let eventsProto = grpc.loadPackageDefinition(packageDefinition);

Dans l’extrait de code ci-dessus, nous avons importé le fichier events.proto défini précédemment dans la variable PROTO_PATH, puis l’avons chargé à l’aide de la méthode loadSync de la bibliothèque protoLoader. Nous avons ensuite enregistré les définitions proto dans la variable eventsProto, qui les contient toutes.

Ajoutez ensuite l’extrait de code suivant juste après la variable eventsProto dans le fichier server/index.js défini précédemment.

const { randomUUID } = require("node:crypto");

const events = [
   {
       id: "34415c7c-f82d-4e44-88ca-ae2a1aaa92b7",
       name: "Birthday Party",
       description: "27th Birthday in Paris",
       location: "Paris France",
       duration: "All Day",
       lucky_number: 27,
       status: "Pending"
   },
];

const server = new grpc.Server();

Dans l’extrait de code ci-dessus, nous avons importé le package node:crypto et sa fonction randomUUID, qui sert à générer des chaînes aléatoires uniques pour les identifiants de nos événements. Comme nous utilisons une base de données en mémoire pour ce tutoriel, nous la définirons sous forme de tableau pour stocker notre liste d’événements. Nous initialiserons ensuite notre instance de serveur en appelant une nouvelle méthode grpc.Server.

Nous allons ensuite enregistrer les services de l’application. Pour ce faire, ajoutez l’extrait de code suivant juste après la variable server dans l’extrait ci-dessus :

server.addService(eventsProto.EventService.service, {

   getAllEvents: (_, callback) => {
       callback(null, { events });
   },

   getEvent: (call, callback) => {
       let event = events.find(n => n.id == call.request.id);

       if (event) {
           callback(null, event);
       } else {
           callback({
               code: grpc.status.NOT_FOUND,
               details: "Event Not found"
           });
       }
   },

   createEvent: (call, callback) => {
       let event = call.request;

       event.id = randomUUID();
       events.push(event);
       callback(null, event);
   },

   updateEvent: (call, callback) => {
       let existingEvent = events.find(n => n.id == call.request.id);

       if (existingEvent) {
           existingEvent.name = call.request.name;
           existingEvent.description = call.request.description;
           existingEvent.location = call.request.location;
           existingEvent.duration = call.request.duration;
           existingEvent.lucky_number = call.request.lucky_number;
           existingEvent.status = call.request.status;
           callback(null, existingEvent);
       } else {
           callback({
               code: grpc.status.NOT_FOUND,
               details: "Event Not found"
           });
       }
   },

   deleteEvent: (call, callback) => {
       let existingEventIndex = events.findIndex(
           n => n.id == call.request.id
       );

       if (existingEventIndex != -1) {
           events.splice(existingEventIndex, 1);
           callback(null, {});
       } else {
           callback({
               code: grpc.status.NOT_FOUND,
               details: "Event Not found"
           });
       }
   }
});

Dans l’extrait de code ci-dessus, nous avons appelé la méthode addService sur l’instance du serveur gRPC pour enregistrer les services de l’application, qui effectuent essentiellement des opérations de création, de lecture et de mise à jour des événements.

Pour permettre au serveur de l’application de démarrer, collez l’extrait de code suivant juste après la méthode addService de l’extrait ci-dessus.

server.bindAsync("127.0.0.1:50051", grpc.ServerCredentials.createInsecure(), (error, port) => {
console.log(`Server listening at http://127.0.0.1:${port}`);
server.start();
});

Créer le client gRPC

En suivant la structure de dossiers ci-dessus, créez un dossier client dans le répertoire racine de votre application, puis créez-y deux fichiers : index.js et app.js. Collez l’extrait de code suivant dans le fichier client/app.js.

#client/app.js

const PROTO_PATH = "../events.proto";
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");

let packageDefinition = protoLoader.loadSync(PROTO_PATH, {
   keepCase: true,
   longs: String,
   enums: String,
   arrays: true
});

const EventService = grpc.loadPackageDefinition(packageDefinition).EventService;
const client = new EventService("127.0.0.1:50051", grpc.credentials.createInsecure());
module.exports = client;

Dans l’extrait de code ci-dessus, nous avons importé les définitions proto créées précédemment, les avons chargées avec protoLoader, connecté le client grpc à l’adresse IP de l’application serveur, puis exporté le service d’événements sous le nom de variable client. Nous avons également associé un certificat SSL au client afin d’authentifier et de chiffrer les communications entre le client et le serveur.

Collez ensuite l’extrait de code suivant dans le fichier client/index.js :

#client/index.js

const client = require("./app");

const express = require("express");
const app = express();
app.disable('x-powered-by');
app.use(express.json());
app.use(express.urlencoded());

app.get("/", (req, res) => {
   client.getAllEvents(null, (err, data) => {
       if (!err) {
           res.status(200).send({
               data
           });
       }
   });
});

app.post("/createEvent", (req, res) => {

   let newEvent = {
       name: req.body.name,
       description: req.body.age,
       location: req.body.address,
       duration: req.body.address,
       lucky_number: req.body.address,
       status: req.body.status
   };

   client.insert(newEvent, (err, data) => {
       if (err) throw err;
       res.status(200).send({
           data,
           message: 'Event created successfully'
       });
   });
});

app.post("/updateEvent", (req, res) => {
   let updateEvent = {
       name: req.body.name,
       description: req.body.age,
       location: req.body.address,
       duration: req.body.address,
       lucky_number: req.body.address,
       status: req.body.status
   };

   client.update(updateEvent, (err, data) => {
       if (err) throw err;

       res.status(200).send({
           data,
           message: 'Event updated successfully'
       });
   });
});

app.delete("/deleteEvent", (req, res) => {
   client.remove({ id: req.body.eventId }, (err, _) => {
       if (err) throw err;

       res.status(200).send({
           message: 'Event deleted successfully'
       });
   });
});

const PORT = process.env.PORT || 50050;
app.listen(PORT, () => {
   console.log("Client Server listening to port %d", PORT);
});

Dans l’extrait de code ci-dessus, nous avons importé event-service depuis le fichier client/app.js. Nous avons ensuite configuré un serveur Express avec des points de terminaison simples pour gérer la creation, la update, la fetch et la delete des événements, en appelant à distance l’application serveur à l’aide de gRPC.

Tester les applications serveur et client

Nous pouvons maintenant tester notre travail pour vérifier que tout fonctionne comme prévu.

Le serveur

Dans le terminal, accédez au répertoire racine du projet, puis exécutez les commandes suivantes :

$bash
$ npm run start

L’application serveur devrait être accessible à l’adresse http://localhost:50051 :

% npm run start
> event-app-node-grpc@1.0.0 start
> node server/index.js

Server listening at https://127.0.0.1:50051

Le client

Ouvrez une nouvelle fenêtre de terminal, accédez au dossier client depuis le répertoire racine de votre application, puis exécutez les commandes suivantes :

$bash
$ node index

L’application devrait être accessible à l’adresse http://localhost:50050 :

$ event-app-node-grpc  cd client
$ event-app-node-grpc/client node index.js

Client Server listening to port 50050

Pour tester, accédez à localhost:50050 dans votre navigateur ou utilisez un outil de test d’API comme Postman. Vous devriez voir l’événement par défaut que nous avons ajouté initialement à notre tableau d’événements. La réponse devrait être identique à la capture d’écran ci-dessous :

Interface Postman affichant une requête GET vers localhost et une réponse JSON contenant des informations sur une fête d’anniversaire.

Authentifier et sécuriser l’API gRPC

Le protocole gRPC prend en charge différents mécanismes d’authentification, ce qui facilite son adaptation aux systèmes nouveaux ou existants. Pour mettre en œuvre l’authentification dans les communications client-serveur gRPC, nous pouvons utiliser des mécanismes recommandés comme SSL et TLS, avec ou sans authentification par jeton Google. Nous pouvons également créer une authentification personnalisée en étendant simplement la fonction d’authentification intégrée à gRPC.

Par défaut, gRPC intègre les mécanismes d’authentification suivants :

  • SSL et TLS pour authentifier le serveur et chiffrer les données échangées entre le client et le serveur

  • ALTS (un protocole de transport et d’authentification mutuelle conçu par Google) pour sécuriser les communications RPC des applications exécutées sur Google Cloud Platform (GCP)

  • Un mécanisme générique d’authentification par jeton qui associe des identifiants basés sur des métadonnées aux requêtes et aux réponses

Comme indiqué dans l’introduction du tutoriel, nous allons mettre en œuvre l’authentification avec SSL, puis modifier nos fichiers client/app.js et server/index.js pour prendre en charge cette nouvelle configuration.

Générer un certificat SSL avec OpenSSL

Commençons par générer un certificat SSL avec OpenSSL. Vous devez avoir OpenSSL installé et disposer des autorisations nécessaires pour exécuter des scripts bash. Ces conditions sont indispensables pour éviter les erreurs d’autorisation.

Dans la structure de dossiers de notre projet, créez un dossier scripts, puis un fichier appelé generate-certs.sh à l’intérieur. Collez-y l’extrait de code suivant :

#scripts/generate-certs.sh

echo "Creating certs folder ..."
mkdir certs && cd certs

echo "Generating certificates ..."

openssl genrsa -passout pass:1111 -des3 -out ca.key 4096

openssl req -passin pass:1111 -new -x509 -days 365 -key ca.key -out ca.crt -subj  "/C=CL/ST=RM/L=Santiago/O=Test/OU=Test/CN=localhost"

openssl genrsa -passout pass:1111 -des3 -out server.key 4096

openssl req -passin pass:1111 -new -key server.key -out server.csr -subj  "/C=CL/ST=RM/L=Santiago/O=Test/OU=Server/CN=localhost"

openssl x509 -req -passin pass:1111 -days 365 -in server.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out server.crt

openssl rsa -passin pass:1111 -in server.key -out server.key

openssl genrsa -passout pass:1111 -des3 -out client.key 4096

openssl req -passin pass:1111 -new -key client.key -out client.csr -subj  "/C=CL/ST=RM/L=Santiago/O=Test/OU=Client/CN=localhost"

openssl x509 -passin pass:1111 -req -days 365 -in client.csr -CA ca.crt -CAkey ca.key -set_serial 01 -out client.crt

openssl rsa -passin pass:1111 -in client.key -out client.key

Le code ci-dessus génère les certificats SSL nécessaires pour établir une connexion sécurisée et chiffrée entre les applications serveur et client. Lors de son exécution, il crée un dossier certs, génère avec OpenSSL les certificats SSL du serveur et du client, puis les enregistre dans le dossier certs. Pour en savoir plus sur ces configurations et leur rôle, consultez le site Web d’OpenSSL.

Générer un certificat SSL pour l’application avec npm

Utilisez maintenant le script pour générer un certificat SSL pour votre application. Dans le terminal, exécutez les commandes suivantes depuis le répertoire racine de l’application.

$bash
$ npm run generate:certs

Cette opération crée un dossier certs contenant les certificats SSL générés.

Notez que des privilèges d’administrateur sont nécessaires. Si vous obtenez une erreur d’autorisation en exécutant le script, utilisez les commandes suivantes pour lui accorder le privilège d’exécution, puis réessayez.

$bash
$ cd scripts
$ chmod u+r+x generate-certs.sh
$ ./generate-certs.sh

Le résultat suivant devrait s’afficher dans votre terminal :

> event-app-node-grpc@1.0.0 generate:certs
> ./scripts/generate-certs.sh

Creating certs folder…
Generating certificates…
Generating RSA private key, 4096 bit long modulus
………………………………………………..++
……………..++

e is 65537 (0x10001)
Generating RSA private key, 4096 bit long modulus
…………………++
………………………………………….….….….….….………..…………..++
e is 65537 (0x10001)
Signature ok
subject=/C=CL/ST=RM/L=Santiago/0=Test/OU=Server/CN=localhost
Getting CA Private Key
writing RSA key
Generating RSA private key, 4096 bit long modulus
………………………………………………..….….….…………..++
.…………..++
e is 65537 (0x10001)
Signature ok
subject=/C=CL/ST=RM/L=Santiago/0=Test/OU=Client/ON=localhost
Getting CA Private Key
writing RSA key

Mettre à jour les fichiers client/app.js et server/index.js

Nous avons généré les certificats SSL nécessaires à l’authentification de nos API gRPC. Nous allons maintenant modifier les fichiers client/index.js et server/index.js pour utiliser ces certificats.

Dans le fichier client/app.js mis à jour ci-dessous, nous avons ajouté le module fs pour lire les certificats générés. Nous avons ensuite utilisé ces certificats pour créer des identifiants SSL gRPC, puis les avons appliqués au service gRPC.

#client/app.js(updated)

const PROTO_PATH = "../events.proto";
const fs = require('fs');
const grpc = require("@grpc/grpc-js");
const protoLoader = require("@grpc/proto-loader");

let packageDefinition = protoLoader.loadSync(PROTO_PATH, {
   keepCase: true,
   longs: String,
   enums: String,
   arrays: true
});

const credentials = grpc.credentials.createSsl(
   fs.readFileSync('../certs/ca.crt'),
   fs.readFileSync('../certs/client.key'),
   fs.readFileSync('../certs/client.crt')
);

const EventService = grpc.loadPackageDefinition(packageDefinition).EventService;
const client = new EventService("localhost:50051",credentials);
module.exports = client;

Dans le fichier server/index.js mis à jour ci-dessous, nous avons également ajouté le module fs pour lire les certificats générés. Nous les avons ensuite utilisés pour créer des identifiants SSL gRPC et les avons appliqués au serveur.

#server/index.js(updated)

const PROTO_PATH = "./events.proto";
const fs = require('fs');

let grpc = require("@grpc/grpc-js");
let protoLoader = require("@grpc/proto-loader");

let packageDefinition = protoLoader.loadSync(PROTO_PATH, {
   keepCase: true,
   longs: String,
   enums: String,
   arrays: true
});

let eventsProto = grpc.loadPackageDefinition(packageDefinition);

const server = new grpc.Server();

let credentials = grpc.ServerCredentials.createSsl(
   fs.readFileSync('./certs/ca.crt'), [{
   cert_chain: fs.readFileSync('./certs/server.crt'),
   private_key: fs.readFileSync('./certs/server.key')
}], true);

----------

----------

server.bindAsync("0.0.0.0:50051", credentials, (error, port) => {
console.log(`Server listening at http://0.0.0.0:${port}`);
server.start();
});

Exécuter les applications serveur et client

Nous avons mis en place avec succès une solution de gestion des événements conforme aux spécifications gRPC. Pour tester les points de terminaison, démarrez l’application depuis le terminal en suivant les étapes ci-dessous.

Le serveur

Dans le terminal, accédez au répertoire racine du projet et exécutez les commandes suivantes :

$bash
$ npm run start

Une fois ces commandes exécutées, l’application serveur devrait être accessible à l’adresse http://0.0.0.0:50051 :

event-app-node-grpc % npm run start

> event-app-node-grpc@1.0.0 start
> node server/index.js

Server listening at http://0.0.0.0:50051

Le client

Ouvrez une nouvelle fenêtre de terminal, accédez au dossier client depuis le répertoire racine de votre application, puis exécutez les commandes suivantes :

$bash
$ node index

Une fois ces commandes exécutées, l’application cliente devrait être accessible à l’adresse http://localhost:50050 :

> event-app-node-grpc % cd client
> client % node index
Client Server listening to port 50050

Pour tester l’application, accédez à localhost:50050 dans votre navigateur ou utilisez un outil de test d’API comme Postman. Vous devriez voir l’événement par défaut que nous avons ajouté initialement à notre tableau d’événements. Le résultat devrait être identique à la capture d’écran ci-dessous :

Postman affiche une requête GET vers localhost avec une réponse JSON listant un événement d’anniversaire à venir à Paris.

Vous pouvez ensuite tester les autres points de terminaison ajoutés à l’application pour vérifier que tout fonctionne comme prévu.

Vous avez créé une API sécurisée avec gRPC !

Dans ce tutoriel, nous avons créé une API simple avec gRPC et Node.js, en présentant son fonctionnement et ses nombreux avantages, comme HTTP/2 et SSL/TLS, qui assurent l’authentification et le chiffrement de bout en bout afin de renforcer la sécurité des API.

Malgré ces avantages, gRPC présente aussi des inconvénients : prise en charge limitée par les navigateurs, format de données non lisible par l’humain, courbe d’apprentissage abrupte et prise en charge limitée de la mise en cache en périphérie. Malgré ces limites, gRPC reste le meilleur choix pour la communication entre microservices internes, grâce à ses performances inégalées et à sa prise en charge de plusieurs langages de programmation. Le protocole gRPC est impressionnant et s’est largement imposé dans le secteur depuis sa première publication en août 2016. Son adoption devrait continuer à progresser.

gRPC offre de nombreuses autres possibilités. L’exemple de ce tutoriel ne représente qu’une infime partie de ce qu’il permet de faire. Consultez la documentation pour approfondir vos connaissances de gRPC et améliorer les processus de communication de votre application ainsi que vos stratégies de sécurisation de gRPC.

Autres ressources sur les API :