React.js #2 : Comment gérer le routing et la sécurité sur une application React ?

Cet article est le second article d'une série de trois articles dans laquelle nous voyons comment bien débuter son projet React.

Retrouvez tous les articles ici :

Dans cet article, nous aborderons le sujet de la gestion des routes et de la sécurité d'une application React.js. C'est parti ! 😉

Créer des routes privées avec React Router

Comment faire ? Eh bien avec React Router, il est possible de gérer le routage des urls avec des composants mais il n’est pas possible pas de gérer des routes protégées ou privées !

Mais tout n’est pas perdu, nous allons pouvoir nous appuyer sur les fonctionnalités fournies et quelques explications qu’ils nous fournissent pour créer notre propre composant Route qui gérera les routes privées (dans l’exemple de React Router leur composant s’appelle PrivateRoute).

Nous allons créer notre propre composant qu’on appellera donc Route où on utilisera la syntaxe d’un composant d’ordre supérieur. Pour résumer rapidement ce qu’est un HOC (Higher Order Component), c’est tout simplement une fonction qui prend en paramètre un composant et en retourne un nouveau.

Dans notre cas, nous prendrons en paramètre un composant et on affichera le composant Route de React Router ou d’autres composants selon les conditions.

Notre composant Route nous permettra de gérer la sécurité de notre application, notamment les restrictions d’accès à certaines routes (pas authentifié, pas les rôles nécessaires, …).

const Route = ({ component: Component, roles, path }) => {
    roles = roles || [];
    return (
        <Route
            path={path}
            exact={true}
            render={(props) => 
                hasRoles(roles) ? (
                    <Component {...props} />
                ) : (
                    isAuth() ? (
                        <Unauthorized />
                    ) : (
                        <Redirect to="/login" />
                    )
                )
            }
        />
    );
}

En guise d’agents de sécurité, nous avons deux services : isAuth et hasRoles (qui vivent donc dans le dossier services de notre structure (cf. article 1.)).

police_gif

isAuth est un service qui nous dit si l’utilisateur est authentifié ou non. hasRoles regarde pour chacun des rôles indiqués si les rôles connus pour la session en cours sont acquis. Si aucun rôle n'est passé, on considère que la route est accessible à tout le monde y compris les utilisateurs non authentifiés.

Dans notre composant Route, on rend le composant reçu en paramètre de notre fonction selon les rôles que l’on a et si on est authentifié.

  • Si on a les rôles, on peut afficher le composant. Pour information, dans notre application, à partir du moment où l’utilisateur est authentifié, il aura toujours un rôle au minimum.
  • Sinon, si on est authentifié on affiche le composant nous informant que nous ne sommes pas autorisés à voir la page. Mais si on n’est pas authentifié, on redirige l’utilisateur vers l’url /login à l’aide du composant Redirect de la librairie react-router.

Voyons voir maintenant où nous allons utiliser notre composant Route.

Configuration des routes

Voyons tout d’abord ce que fait la méthode getRoutes().

Dans notre fichier routes.js situé dans le dossier src/ nous avons un tableau avec les routes accessibles dans notre application :

const routes = [
   {
       'name': 'login',
       'path': '/login',
       'component': Login,
   },
   {
       'name': 'index',
       'path': '/',
       'component': Index,
       'roles': ['ROLE_USER'],
   },
];

Ici, nous avons 2 routes : /login et /index. Pour /login, nous affichons le composant Login et pour /index nous affichons le composant Index seulement si l’utilisateur a le rôle ROLE_USER.

Pour rappel, nous utilisons flow.js dans notre projet afin de typer nos variables et le vérifier à la transpilation (TypeScript, est une autre solution envisageable, sans que pour autant flow.js ou TypeScript soit indispensable au projet. Pour vous aider à choisir entre Flow.js, TypeScript et Javascript, voici un excellent article.).

Dans notre fichier, nous avons créé un type que nous avons appelé Route, regardons à quoi il ressemble :

type Route = {
   name: string,
   path: string,
   component?: React.AbstractComponent<any>,
   roles?: string[],
   routes?: Route[],
};

Le type Route est un objet qui a 5 attributs :

  • name qui est une chaîne de caractères obligatoire. C’est un alias qui nous servira plus tard pour récupérer une route.
  • path qui est une chaîne de caractères obligatoire. C’est le chemin d’accès au composant.
  • component est un composant React qui est facultatif.
  • roles est un tableau de chaînes de caractères facultatif. Cela représente les rôles nécessaires pour afficher le composant.
  • routes est un tableau de Route et il est facultatif. Cela permet de renseigner des sous-routes à notre route principale.

Voyons maintenant notre méthode getRoutes() :

const compile = (parentRoute: Route, subRoutes: Route[]): Route[] => {
   return subRoutes.flatMap(subRoute => {
       const newRoute: Route = {
           'name': subRoute.name,
           'path': parentRoute.path + subRoute.path,
           'component': subRoute.component,
           'roles': (parentRoute.roles || []).concat((subRoute.roles || [])),
       };
       return (subRoute.routes) ? [...compile(newRoute, subRoute.routes)] : newRoute;
   });
}

export const getRoutes = () => {
   const parentRoute = {
       'name': '',
       'path': '',
   };
   const flatRoutes = compile(parentRoute, routes);
   return flatRoutes;
}

Cette méthode retourne un tableau de routes mis à plat. En effet, il est possible de renseigner des sous-routes à une route, nous avons donc besoin de mettre à plat ce tableau pour pouvoir itérer dessus simplement dans notre composant App. On utilise pour cela une méthode récursive que nous avons appelée compile.

Bref, je passe le côté technique de cette méthode, tout ce qu’elle fait c’est qu’elle nous retourne un beau tableau de routes sur lequel on peut itérer simplement. 😁

Génération des routes

Maintenant que nous avons créé notre composant Route, nous allons rendre ces routes ou bien faire des redirections ou afficher des pages personnalisées en fonction des rôles de notre utilisateur et ceux requis par la page. Cela se passe dans le fichier App.js :

const App = () => {
  return (
      <Router>
        <Switch>
          {
            getRoutes().map((route, index) => {
              return <Route exact {...route} key={index} />
            })
          }
          <Route component={NotFound} />
        </Switch>
      </Router>
  );
}

La méthode getRoutes(), que nous avons vue au point précédent, retourne un tableau de routes avec toutes les informations nécessaires (chemin d’accès, le composant rendu, les rôles nécessaires pour accéder à la route, …).

Le composant Router doit se trouver à la racine de votre hiérarchie d’éléments. Vous pouvez même le mettre un niveau au-dessus dans le fichier index.js, comme ceci :

ReactDOM.render(
        <Router>
            <App />
        </Router>, 
        root
    );

Pour notre exemple, nous garderons cette configuration qui est plus visuelle.

Ensuite, nous avons Switch qui permet de faire le rendu de la première Route rencontrée qui satisfait les conditions d’exactitude du chemin.

La dernière Route est alors affichée quand aucune des précédentes routes n’a rempli les conditions, autrement dit, qu’aucune route ne correspondait à l’URL entrée par l’utilisateur.

Dans notre cas, on affiche un composant NotFound.

Nos services de sécurité

Je vous ai parlé au tout début des services isAuth et hasRoles sans vous présenter ce qu’ils faisaient vraiment.

Eh bien notre service isAuth aura pour rôle de récupérer un token auprès de votre back-end une fois l’utilisateur authentifié. Dans notre cas ce token est un JWT qui nous transmet au même moment les rôles de l’utilisateur.

Si l’utilisateur a renseigné des identifiants corrects on lui donne au moins un rôle ROLE_USER et il pourra ainsi se connecter. Sinon, notre back-end nous renvoie une réponse pour nous dire que l’utilisateur n’a pas pu se connecter.

Ensuite notre service hasRoles, à partir de notre token et donc des rôles de l’utilisateur, regarde si l’utilisateur peut accéder à la page demandée.

// @flow

import getToken from "./token";

export default function isGranted(roles: Array<string>): boolean {
   const token = getToken();

   // If few roles are required but no token
   if (roles.length > 0 && (token === null || Array.isArray(token.roles) === false)) {
       return false;
   }

   return roles.every(role => token.roles.includes(role));
}

Notre méthode isGranted, prend en paramètre un tableau de rôles requis et regarde à partir des rôles dans le token, si l’utilisateur peut accéder à la page .

Améliorer nos routes avec des alias

Renforçons maintenant notre système de routing en utilisant les alias.

sponge_bob_strong_gif

En effet, imaginons que nous fassions des redirections un peu partout comme ceci :

<Redirect to="/login" />

Ou dans notre code :

props.history.push(“/login”);

Si nous souhaitons changer l’url pour /toto par exemple, nous allons devoir le changer partout dans notre application. Pour éviter ceci, nous allons donc utiliser des alias. Nous avons déjà un champ prêt pour ça dans nos objects Route, c’est le champ name.

Nous allons donc écrire une méthode getPath dans notre fichier routes.js qui à partir d’un nom de route, retourne une Route.

Ainsi nous aurons :

<Redirect to=getPath(‘login’) />
props.history.push(getPath(‘login’));

Voici la méthode getPath que nous avons écrite :

export const getPath = (name: string, params: Object = null) => {
   const routeFound = getRoutes().find(route => route.name === name);
   let path = routeFound ? routeFound.path : null;
   if (path && params) {
       Object.entries(params).forEach(([key, value]: [string, any]) => {
           path = path ? path.replace(`:${key}`, value) : '';
       });
   }
   return path;
}

Dans l’exemple précédent (redirection vers login), nous avons un cas simple où la redirection ne prend pas de paramètres. Qu’en est-il si nous souhaitons rediriger vers une page /user/1 ?

Eh bien avec notre méthode getPath, nous pouvons passer un deuxième paramètre params (optionnel donc) qui est un objet avec les différents paramètres de la route.

Par exemple, si nous avons cette route :

{
    'name': 'user',
    'path': '/user/:id',
    'component': User,
    'roles': ['ROLE_USER'],
},

Alors il faudra appeler getPath comme ceci :

<Redirect to={getPath(‘user’, {‘id’: 1})} />
props.history.push(getPath(‘user’, {‘id’: 1}));

Merci d'avoir lu ce second article, le prochain abordera la gestion du store dans une application React.js sans passer par Redux. On se retrouve donc là-bas dans quelques jours 😃 ! N'hésitez à commenter en pied d'article pour lever des sujets !

Tags de
l'article

React.JS Front-end

Catégories de l'article

Javascript

Commentaires

Il n'y a actuellement aucun commentaire. Soyez le premier !

Articles liés