Comprendre l’asynchrone via les microservices / Part 2

le jeudi 3 septembre 2020

Bearstech | Dernière mise à jour : jeudi 3 septembre 2020

Relier ses différents microservices, écrits dans différents langages, sans couplage fort et en sécurité avec JWT.

Notre prochain webinar

Voici le second volet de l'article publié la semaine dernière et que vous pouvez retrouver ici

Déléguer sa confiance

Pour laisser de l'air à ses µservices, il faut un découplage net entre eux. On doit pouvoir les faire évoluer chacun à leur rythme, sans jamais imposer le déploiement de plusieurs services en même temps, et un incident sur un µservice ne doit pas faire écrouler l'ensemble. Concrètement, les µservices ne partageront pas de base de données (de base, rien n'empêche qu'il partage un serveur si c'est plus pratique à héberger) et limiteront leurs discussions privées au strict minimum.

Le premier "partage" entre µservices dont on aura besoin va être l'identification de l'utilisateur. Un service assure l'authentification de l'utilisateur (mot de passe, 2FA …), les autres profiteront de son identification (identifiant groupes, rôles…).

Au niveau mathématique, le service assurant l'authentification va fournir un jeton signé, qui sera utilisé par les autres services, qui se contenteront de vérifier la signature (et la période de validité). Comme il faut laisser la cryptographie aux cryptographes, pour que les vaches soient bien gardées, on va utiliser LE standard pour ça, et surtout ne rien bricoler.

Le standard, pour les jetons signés, pour le web et au-delà, est JWT, JSON Web Token. Enfin, si on veut pinailler, JWT, c'est le jeton, et JWS la signature utilisant un algorithme normalisé par un JWA. Je vous laisse aller chercher la définition des autres JW*.

Pour sélectionner une bibliothèque, le plus simple est de suivre les recommandations de jwt.io pour donner des bons et des mauvais points sur les implémentations. Jwt.io fournit aussi un outil de debug en ligne fort pratique.

Implémentation

L'API REST de mashup en Node.js

Node possède des tonnes de bibliothèques, plus ou moins maintenues, plus ou moins concurrentes pour faire des tas de choses. On a de la chance, pour tout ce qui est service web, il existe un standard de fait, ExpressJS qui est agréable à utiliser et bien documenté. Comme ils sont gentils, il y a aussi un middleware pour gérer le JWT.

Une API REST est juste un service HTTP qui parle JSON et respecte un certain nombre de conventions, ce n'est pas plus compliqué que ça.

const express = require('express');
const jwt = require('express-jwt');
const mashup = require('./mashup');

const app = express();
app.use(express.json());

On déclare une instance d'Express, on lui ajoute un middleware pour gérer le JSON.

const port = process.env.PORT || 3000;
const host = process.env.HOST || "127.0.0.1";

Le paramétrage du host et du port sur lequel va écouter le service se fait via des variables d'environnement, comme le préconise the 12 factors app.

app.post(
  "/pict",
  jwt({
    secret: process.env.SECRET,
    algorithms: ["HS256"],
    issuer: "cms",
    credentialsRequired: true,
    getToken: (req) => {
      if (
        req.headers.authorization &&
        req.headers.authorization.split(" ")[0] === "Bearer"
      ) {
        return req.headers.authorization.split(" ")[1];
      }
      return null;
    },
  }),
  async (req, res, next) => {
    if (!("txt" in req.body)) {
      res.status(400).end();
      return;
    }
    try {
      res.json(await mashup.illustrate_that_for_me(req.body.txt));
    } catch (error) {
      return next(error);
    }
  }
);

Une seul route, /pict qui acceptera des requêtes en POST, le middleware JWT qui va chercher le token dans un header authorization, puis l'appel de la fonction de mashup qui utilise des arguments issu du body HTTP en JSON, et qui répond en JSON.

app.listen(port, host, () =>
    console.log(`App listening at http://${host}:${port}`));

Il ne reste plus qu'à lancer le service.

Le CMS en PHP

Dans notre exemple, on a un CMS en PHP qui va déléguer à son javascript (dans le navigateur) des appels distants et potentiellement mous.

composer va ajouter la bibliothèque chaudement recommandée par jwt.io.

composer require lcobucci/jwt

On peut maintenant importer le nécessaire, puis créer un token.

require __DIR__ . '/vendor/autoload.php';

use Lcobucci\JWT\Builder;
use Lcobucci\JWT\Signer\Key;
use Lcobucci\JWT\Signer\Hmac\Sha256;

$key = new Key($_SERVER['SECRET']);

$signer = new Sha256();
$time = time();

$token = (new Builder())->issuedBy('cms') // Configures the issuer (iss claim)
    ->permittedFor('http://' . $_SERVER['HTTP_HOST']) // Configures the audience (aud claim)
    ->issuedAt($time) // Configures the time that the token was issue (iat claim)
    ->expiresAt($time + 3600) // Configures the expiration time of the token (exp claim)
    ->withClaim('api', 'mashup') // Configures a new claim
    ->getToken($signer, $key); // Retrieves the generated token

Pour rester dans quelque chose de simple, on va utiliser une clef symétrique (c'est la même clef pour créer et valider le jeton, à la différence d'une paire de clefs asymétrique, où il y en a une pour signer, une autre pour valider). La clef utilise SHA256 comme hachage.

Les parties notables du code sont l'obtention de la clef via la variable d'environnement SECRET et la durée de vie d'une heure.

On va maintenant imaginer le CMS.

$txt = <<<EOT
Linus Torvalds : « Vos limitations matérielles ne devraient pas être un problème pour le reste d'entre nous »
EOT;

On a maintenant tout ce qu'il faut pour afficher la page web.

<p id="blah">
    <?php echo $txt; ?>
</p>
<div id="dropzone">
</div>

<script>
const TOKEN = '<?php echo $token; ?>';

window.onload = (event) => {
    const txt = document.getElementById('blah').innerText;

    fetch('/pict', {
            method: 'POST',
            body: JSON.stringify({
                txt: txt,
            }),
            headers: new Headers({
                authorization: `Bearer ${TOKEN}`,
                'Content-Type': 'application/json',
            })
        })
        .then(response => response.json())
        .then(data => {
            console.log(data);
            const dropzone = document.getElementById('dropzone');
            const v = document.createElement("video");
            v.setAttribute('src', data.gif.mp4);
            v.setAttribute('width', data.gif.width);
            v.setAttribute('height', data.gif.height);
            v.setAttribute('autoplay', true);
            dropzone.append(v);
        });
}
</script>

Pour ne pas tout mélanger le PHP et le javascript, on va commencer par afficher notre contenu, mettre le token dans une variable js et déclencher les actions js quand la page est chargée (l'évènement onload). C'est maintenant le javascript qui a la main.

  • Le texte est récupéré via l'identifiant de sa balise.
  • Appel au service disant, avec le texte et le token, avec un payload en JSON.
  • On va chercher dans le DOM la zone où afficher le gif, et l'on crée ex nihilo une balise <video> avec les arguments issus de l'appel REST.

Ce qui donne cette petite frise chronologique :

La tactique est simple, le PHP se contente de faire des choses avec des temps prévisibles : gestion du contenu, création du token, affichage de la page. Ça permet de libérer rapidement le worker PHP pour qu'il puisse retourner servir d'autres pages.

Le navigateur web affiche le contenu avec un garde place vide, pour les illustrations, et déclenche un appel asynchrone pour réclamer des illustrations. Quand il reçoit les réponses, il affiche le gif dans son emplacement.

Les navigateurs webs sont architecturés autour de la notion d'asynchronicitée, depuis toujours, alors que les langages synchrones (PHP dans notre cas), travail par lot en s'engageant à répondre rapidement, pour pouvoir retourner traiter le lot suivant.

Le code complet est disponible sur Github.

what is asynchronous

Service Audit de Performance web gitlab

Bearstech vous propose ses services Audit de Performance web gitlab

Découvrir ce service

Partager cet article

Flux RSS

flux rss

Partager cet article :