Transcription
La transition d'un développeur de niveau intermédiaire à un ingénieur senior nécessite de déplacer votre concentration de la simple écriture de code à la maîtrise de la conception architecturale de haut niveau. Cette vidéo détaille la feuille de route essentielle pour construire des systèmes évolutifs prêts pour la production à partir de zéro, couvrant tout, des protocoles d'API à la sélection de bases de données. Vous apprendrez les différences fondamentales entre REST, GraphQL et gRPC, ainsi que les stratégies de mise à l'échelle grâce à l'équilibrage de charge et aux pratiques de sécurité robustes. Hayek a développé ce cours.
>> La plupart des développeurs ne peuvent pas concevoir de systèmes ou de fonctionnalités à partir de zéro. Ils peuvent ajouter à l'architecture de quelqu'un d'autre avec des tâches aux exigences claires et déjà sur des systèmes matures. Mais si vous leur demandez de concevoir quelque chose à partir de zéro, la plupart d'entre eux se figent généralement. Et en fait, c'est précisément la compétence qui sépare les développeurs de niveau intermédiaire des seniors, car les seniors sont également capables de prendre des décisions, de concevoir des compromis, de concevoir l'architecture à partir de zéro et de prendre des décisions avec des exigences approximatives. Les entreprises ne paient donc pas six chiffres pour des personnes qui peuvent simplement coder ou suivre des instructions, mais elles paient pour des décisions architecturales, pour rendre le système performant, pour optimiser le stockage des données et pour prendre des décisions qui affectent également les clients et le logiciel qu'ils construisent.
Dans cette vidéo, je vais donc vous enseigner les concepts exacts que j'ai maîtrisés pour pouvoir concevoir de tels systèmes à partir de zéro et également accéder à des postes seniors. C'est ainsi que j'ai passé les entretiens de conception de systèmes sans aucun problème. Et ce sont les compétences que j'ai acquises pour atteindre le niveau senior au cours de ma deuxième année de carrière. Je ne vous enseigne donc pas de théorie tirée de livres ou de newsletters. C'est ce qui fonctionne réellement dans les emplois réels et dans les entretiens réels.
Alors, allons sur mon ordinateur pour voir ce que nous allons couvrir dans ce cours. Tout d'abord, nous commencerons par les fondations, les concepts clés que vous devez comprendre avant toute chose en conception de systèmes. Ensuite, nous aborderons la conception d'API, qui est une partie importante de la conception de systèmes, comment concevoir réellement des API qui évoluent et qui ont du sens pour les autres développeurs qui les utiliseront. Ensuite, nous aborderons les bases de données, comment choisir la bonne base de données pour différents scénarios et concevoir correctement votre couche de données. Ensuite, nous aborderons la mise en cache. Comment utiliser la mise en cache ? Comment utiliser les CDN, l'équilibrage de charge pour rendre vos systèmes rapides et fiables. Ensuite, nous aborderons le traitement des big data, car c'est un sujet important en soi. Comment gérer les données à grande échelle de la bonne manière. Ensuite, nous passerons à la conception pour la production, comment construire des systèmes qui fonctionnent réellement dans le monde réel, pas seulement sur votre ordinateur portable ou sur une seule machine. Et enfin, vous verrez comment je conçois les systèmes pour les entretiens et comment gérer cette étape afin que vous puissiez réussir ces entretiens et obtenir les offres dont vous avez besoin pour accéder à des postes seniors.
Concevoir un système pour supporter des millions d'utilisateurs est un défi, mais chaque système complexe commence par quelque chose de simple. C'est pourquoi, dans cette leçon, nous allons construire une configuration de base qui ne supporte qu'un seul utilisateur, puis nous l'étendrons progressivement au fur et à mesure. Car commencer petit nous permet de comprendre chaque composant clé avant d'ajouter plus de complexité.
Commençons donc par la première étape et construisons une configuration à serveur unique. Imaginez que nous mettons en place un système pour une petite base d'utilisateurs. Cela signifie que tout fonctionne sur un seul serveur, l'application web, la base de données, le cache et également les autres composants. Et cette configuration nous permet de visualiser le fonctionnement principal sans complexité ajoutée.
Décomposons maintenant comment cette configuration à serveur unique gère les requêtes des utilisateurs. Nous avons des utilisateurs qui essaient d'accéder à notre site Web ou à notre API sur le serveur. Ils peuvent utiliser soit le navigateur Web, soit une application mobile pour accéder à notre serveur. Et d'autre part, nous avons notre serveur qui possède les fichiers nécessaires à servir aux navigateurs Web, ainsi que les points d'API nécessaires à servir à l'application mobile, et il est hébergé à cette adresse IP d'exemple. Initialement, nos utilisateurs n'ont pas cette adresse IP. Ils ont le domaine auquel ils essaient d'accéder. Disons que c'est app.demo.com. Donc, s'ils tapent simplement ce nom de domaine et appuient sur Entrée, leur navigateur Web, par exemple, contactera le DNS, qui signifie Domain Name System. C'est un fournisseur qui mappe les domaines aux adresses IP. Et dans notre cas, disons que notre nom de domaine est mappé à l'adresse IP qui est l'adresse IP du serveur que nous avons. Ce fournisseur DNS renverra donc l'adresse IP au navigateur Web ou à l'application mobile, à nos clients. Et cette adresse IP est l'adresse IP de notre serveur. Ils ont donc maintenant l'emplacement où ils essaient d'envoyer des requêtes.
Avec cette adresse IP en main, l'appareil de l'utilisateur envoie une requête HTTP à notre serveur pour demander des données spécifiques. Ensuite, notre serveur traite cette requête et renvoie les données demandées. Il peut s'agir d'une page HTML pour un navigateur ou d'une réponse JSON pour l'application, en fonction du type de requête.
Dans cette configuration, le trafic provient généralement de deux sources principales. La première est les applications Web et la seconde est les applications mobiles qui essaient d'accéder à notre serveur. Pour nos utilisateurs Web, le serveur gère la logique métier, le stockage des données et également la présentation à l'aide de HTML, CSS et JavaScript. Et pour les utilisateurs mobiles, la communication se fait généralement via HTTP. Ces applications mobiles demandent des données au serveur à l'aide d'appels d'API. Et le JSON est souvent utilisé pour les réponses car il est léger et facile à interpréter pour les appareils mobiles.
Voici un exemple de requête d'API que nous pouvons recevoir pour notre serveur. Il peut s'agir d'une requête GET vers notre domaine/produit/l'ID de ce produit. Et pour ce point de terminaison, nous devons récupérer les détails d'un produit. Et voici un exemple de réponse que nous pourrions renvoyer au client. Il s'agit d'une réponse JSON qui contient l'ID du produit. Elle contient le nom de ce produit, une description, le prix du produit et d'autres métadonnées utiles pour le client. Et cela sera ensuite utilisé par l'application mobile ou par le navigateur Web pour afficher ce produit à l'écran.
Et au fur et à mesure que nous avançons, notre objectif sera d'identifier les domaines où un seul serveur pourrait ne pas suffire à la demande des utilisateurs. Pour l'instant, cette configuration est idéale pour les petites bases d'utilisateurs, mais elle peut avoir du mal sous un trafic intense. Nous allons donc ensuite explorer les moyens de mettre à l'échelle chaque partie du système pour prendre en charge plus d'utilisateurs efficacement.
Quelques points clés à retenir de ceci sont que nous devons commencer petit. Nous devons commencer par une configuration simple à serveur unique pour comprendre les composants essentiels de l'architecture système. Nous comprenons également maintenant comment ces requêtes circulent dans votre système, ce qui est fondamental pour construire des systèmes plus évolutifs, et nous reconnaissons également les exigences uniques des applications Web et mobiles et la manière dont elles interagissent avec votre serveur. Et dans les leçons suivantes, nous commencerons à examiner les stratégies d'optimisation et de mise à l'échelle de cette configuration.
À mesure que notre base d'utilisateurs augmente, un seul serveur ne suffit plus à gérer la demande accrue. Et pour accueillir plus d'utilisateurs, nous pouvons séparer notre couche Web, qui gère le trafic Web et mobile, et notre couche de données, qui gère la base de données. Cette configuration nous permet de mettre à l'échelle chaque serveur en fonction de sa charge spécifique. Mais quand il s'agit de choisir la bonne base de données, comment savoir quelle base de données spécifique est la meilleure pour notre application spécifique ?
En ce qui concerne la sélection de bases de données, il existe deux options principales. La première option est les bases de données relationnelles ou SGBDR, qui sont structurées en tables et en lignes. Quelques exemples populaires sont PostgreSQL, MySQL, Oracle Database ou SQLite. D'autre part, nous avons les bases de données non relationnelles ou NoSQL. Celles-ci conviennent aux applications qui nécessitent de la flexibilité et un accès rapide à de grandes quantités de données non structurées. Quelques exemples sont Cassandra, MongoDB, Redis ou Neo4j.
Commençons par explorer les bases de données relationnelles. Ces bases de données utilisent le langage de requête structuré ou SQL pour trouver et manipuler des données. Les données sont ici structurées en tables, qui sont les éléments fondamentaux des bases de données SQL. Et elles sont similaires à des feuilles de calcul. Chaque table se compose de colonnes, qui peuvent être considérées comme les champs ou les attributs de la table. Et elle se compose également de lignes, qui sont des enregistrements uniques au sein de cette table. Par exemple, si vous imaginez une table de clients, au sein de cette table, nous pouvons avoir des colonnes comme ID, nom, âge et email. Et pour chaque ligne, nous pouvons avoir des clients spécifiques comme l'ID 123, le nom sera John, l'âge sera 30 et ainsi de suite.
Mais quels sont les avantages d'utiliser une base de données SQL ? Tout d'abord, elles prennent en charge des opérations de jointure complexes entre plusieurs tables. Par exemple, si vous imaginez que nous avons une table de clients et une table de produits, et que nous voulons maintenant créer une table séparée qui connectera les clients et les produits qu'ils ont commandés. Avec SQL, vous pouvez joindre ces deux tables dans une table de commandes, et celle-ci contiendra les informations sur les ID clients qui ont cette commande et également les ID produits que ce client a commandés. Et ce processus de combinaison de deux tables ou plus en une seule table s'appelle des opérations de jointure en SQL.
Et l'autre grand avantage est qu'elles fournissent une cohérence et une intégrité des données robustes, en particulier pour les transactions. Les transactions en SQL sont une séquence d'une ou plusieurs opérations SQL qui sont effectuées comme une seule unité atomique, et chaque transaction en SQL suit l'acronyme ACID. Vous pouvez penser à un exemple de transaction comme un transfert bancaire. Donc, tout d'abord, toutes les transactions sont atomiques, ce qui signifie que la transaction entière est traitée comme une seule unité qui réussit complètement ou échoue complètement. Chaque transaction est également cohérente, ce qui signifie qu'elle transforme la base de données d'un état valide à un autre état valide, et elles sont également dotées d'une isolation, ce qui signifie que les modifications apportées par des transactions concurrentes sont isolées les unes des autres et n'interfèrent pas entre elles. Et enfin, elles sont dotées de durabilité, ce qui signifie que même si le système tombe en panne ou si le serveur de base de données tombe en panne, les données resteront toujours là.
Et maintenant, jetons un coup d'œil aux bases de données non relationnelles. Les bases de données non relationnelles peuvent prendre différentes formes. Par exemple, nous avons des magasins de documents comme MongoDB, ou vous pouvez utiliser des magasins de colonnes larges comme Cassandra, des magasins clé-valeur comme Redis et des magasins de graphes comme Neo4j. Examinons chacun de ces types séparément et commençons par les magasins de documents. MongoDB est l'exemple le plus populaire de magasin de documents, et les données y sont stockées dans des documents de type JSON, ce qui nous permet d'avoir des structures de données complexes au sein d'un seul enregistrement.
Ensuite, nous avons les magasins de colonnes larges où les données sont stockées dans des tables, des lignes et des colonnes dynamiques. Quelques exemples ici sont Cassandra ou Cosmos DB. Le principal avantage de ces bases de données est qu'elles peuvent gérer des échelles massives et sont très bonnes pour de nombreuses opérations d'écriture. L'autre option est les bases de données de graphes qui se concentrent sur le stockage des entités et de leurs relations sous forme de graphes. Un exemple de base de données de graphes est Neo4j. Par exemple, chez Amazon, ils utilisent la base de données de graphes Neptune, qui les aide à vous faire des recommandations de produits basées sur vos commandes précédentes. Et l'autre type populaire est les magasins clé-valeur. Ici, les données sont stockées sous forme de paires clé-valeur. Le plus grand avantage des magasins clé-valeur est leur simplicité et leur rapidité. Comme ils sont principalement stockés en RAM, la lecture et l'écriture dans ces bases de données sont extrêmement rapides par rapport aux autres bases de données. Quelques exemples de magasins clé-valeur sont Memcached ou Redis.
Ce sont donc les quatre principaux types de bases de données NoSQL. Examinons maintenant les avantages de ces bases de données NoSQL. Si vous regardez le même exemple que nous avions pour les bases de données SQL où nous avons des clients et des produits et que nous voulons les joindre dans des commandes. Par exemple, dans MongoDB, vous pourriez avoir cela comme un seul document. Vous pourriez donc stocker toutes les données utilisateur, ainsi que les commandes et les produits dans un seul document. Et en raison de cette structure, les bases de données NoSQL peuvent gérer des ensembles de données hautement dynamiques et volumineux sans la structure imposée par les bases de données relationnelles. Et elles sont également optimisées pour une faible latence et une évolutivité.
Alors, quand devriez-vous utiliser des bases de données relationnelles par rapport à des bases de données non relationnelles ? Voici une comparaison rapide des deux. Si les données de votre application sont bien structurées avec des relations claires, alors vous devriez utiliser des bases de données SQL. Par exemple, si vous avez une application de commerce électronique qui suit les clients et les commandes, c'est un bon cas d'utilisation pour utiliser une base de données SQL. Ensuite, si vous avez besoin d'une forte cohérence et d'une intégrité transactionnelle, par exemple, si vous avez une application financière ou un système bancaire, alors vous devriez utiliser des bases de données SQL. Cependant, si votre application exige une latence ultra faible pour des réponses rapides, alors vous devriez opter pour des bases de données non relationnelles, ou si les données sont non structurées ou semi-structurées comme des objets JSON et que les relations ne sont pas si cruciales, alors vous devriez également opter pour des bases de données NoSQL. Et enfin, si votre application nécessite un stockage flexible et évolutif pour des volumes de données massifs, par exemple, un moteur de recommandation stockant des données d'activité utilisateur au format clé-valeur, alors vous devriez également opter pour des bases de données NoSQL.
Explorons les deux approches principales de mise à l'échelle, qui sont les méthodes verticales et horizontales de mise à l'échelle. Et nous verrons également pourquoi la mise à l'échelle horizontale est généralement plus adaptée aux applications à fort trafic.
Tout d'abord, nous avons la mise à l'échelle verticale ou parfois appelée "scale up". Cela signifie simplement que nous ajoutons plus de ressources à notre serveur existant, c'est-à-dire de la RAM, du CPU ou toute autre ressource qui pourrait nous aider à gérer plus de trafic. Et cette approche est simple et fonctionne bien pour les applications à trafic faible ou modéré. Cependant, elle présente des limites : premièrement, les limites de ressources. Il y a un plafond strict sur la quantité que vous pouvez ajouter à un seul serveur, et vous atteindrez éventuellement une limite sur la façon dont vous pouvez mettre à niveau votre nouveau serveur. Et la deuxième raison est le manque de redondance. Cela signifie que si ce serveur tombe en panne, vous n'avez aucun autre serveur pour servir vos utilisateurs, ce qui signifie que toute votre application tombe en panne avec votre serveur unique.
D'autre part, nous avons la mise à l'échelle horizontale, également appelée "scale out". Dans le cas de la mise à l'échelle horizontale, nous ajoutons simplement plus de serveurs pour partager la charge. Donc, au lieu d'avoir un seul serveur, nous pouvons le répliquer et avoir trois de ces mêmes serveurs. Et maintenant, nous pouvons partager cette charge entre ces serveurs au lieu de tout gérer sur un seul serveur. Généralement, cela est plus adapté aux applications à grande échelle car il offre une plus grande tolérance aux pannes. Et une plus grande tolérance aux pannes signifie que si l'un de nos serveurs tombe en panne, nous avons toujours deux serveurs disponibles. Ces deux serveurs peuvent donc continuer à servir nos utilisateurs pendant que le deuxième serveur se remet de la panne. Et cela offre également une meilleure évolutivité, car vous pouvez simplement ajouter plus de serveurs au besoin. Au lieu d'en avoir trois, vous pourriez en introduire un quatrième qui gérera le nouveau trafic entrant.
Mais comment mettons-nous en œuvre la mise à l'échelle horizontale ? Dans le cas d'un serveur unique, nous savons que toutes nos requêtes clients allaient vers ce serveur unique, qu'elles proviennent d'une application mobile ou d'un ordinateur de bureau. Mais qu'en est-il si nous avons maintenant trois serveurs pour gérer toute la charge ? Comment distribuons-nous les requêtes clients ? Disons que notre application mobile fait une requête. Comment savons-nous où cette requête doit aller ? Qu'elle doit aller au serveur un, au serveur deux ou au serveur trois. Et il semble que nous ayons besoin de quelque chose au milieu qui dirigera le trafic vers les serveurs appropriés. Et cette partie au milieu s'appelle un équilibreur de charge.
Nous utilisons des équilibreurs de charge pour distribuer le trafic sur plusieurs serveurs. Par exemple, ici, nous avons trois serveurs. Serveur un, deux et trois. Chaque fois que nous avons une nouvelle requête des clients, l'équilibreur de charge décide où la charge est la plus faible. Et ensuite, il redirige le trafic vers ce serveur. Et il contrôle également la tolérance aux pannes, ce qui signifie que si l'un de nos serveurs tombe en panne, comme le serveur trois, il cessera d'envoyer du trafic au premier serveur car il n'est plus disponible. Et il enverra tout le trafic aux serveurs deux et un jusqu'à ce que le serveur trois soit à nouveau disponible. Et cela peut également rendre notre application plus évolutive, car nous pouvons introduire un quatrième serveur et tout autre serveur que nous souhaitons. Et cet équilibreur de charge garantira que tout le trafic est distribué uniformément.
Ce sont donc les deux principales approches de mise à l'échelle, qui sont les méthodes verticales et horizontales de mise à l'échelle. Dans le cas de la mise à l'échelle verticale, nous ajoutons simplement plus de ressources à notre même serveur. Mais dans le cas de la mise à l'échelle horizontale, nous ajoutons plus d'utilisateurs à notre base de serveurs. Et ensuite, nous utilisons un équilibreur de charge qui distribue le trafic sur plusieurs serveurs. Mais pour l'instant, cet équilibreur de charge est un peu une boîte noire pour nous, car nous ne comprenons pas comment il fonctionne, comment il prend les requêtes et comment il distribue le trafic. Explorons cela dans la prochaine leçon et voyons comment cela fonctionne exactement et quelles sont les stratégies que nous utilisons dans l'équilibrage de charge.
Les équilibreurs de charge distribuent le trafic entrant sur plusieurs serveurs tout en garantissant qu'aucun serveur unique ne supporte une charge trop importante. Mais comment cela se passe-t-il réellement et comment fonctionne la logique de distribution du trafic entrant ? Pour mieux comprendre les équilibreurs de charge, explorons sept stratégies et algorithmes couramment utilisés dans l'équilibrage de charge.
Commençons par le Round Robin, qui est l'un des algorithmes les plus populaires. C'est principalement parce que c'est la forme la plus simple d'équilibrage de charge où chaque serveur du pool reçoit une requête dans un ordre séquentiel et rotatif, ce qui signifie essentiellement que la première requête qu'il reçoit, il la dirige vers le premier serveur, et la requête suivante va au deuxième serveur, et la troisième va au troisième serveur. Et une fois que le dernier serveur est atteint, dans ce cas, c'est le serveur trois, il le redirige vers le premier serveur, puis à nouveau vers le deuxième serveur, et ainsi de suite. Cela fonctionne bien pour les serveurs ayant des spécifications similaires. Cela signifie que si nos trois serveurs ont la même capacité, alors le Round Robin sera un bon choix ici.
L'option suivante est l'algorithme des moins de connexions. Il dirige le trafic vers le serveur ayant le moins de connexions actives. Par exemple, si nous avons 10 connexions actives sur le serveur un, nous avons neuf connexions actives sur le serveur deux et nous avons 30 connexions actives sur le serveur trois. S'il reçoit une nouvelle requête du client, il la dirigera vers le serveur deux car il a le moins de connexions actives à ce moment-là. Il aura donc une connexion de plus. Et ceci est particulièrement utile pour les applications où vous avez des sessions de durée variable. Cela signifie qu'une de vos sessions peut durer 10 minutes, une autre 1 minute, et ainsi de suite. Et dans ce cas, l'équilibreur de charge en tiendra compte et enverra le trafic au serveur ayant le moins de connexions.
La troisième option est le temps de réponse le plus court. Cet algorithme est davantage axé sur la réactivité des serveurs. Supposons que votre premier serveur soit très réactif. Le deuxième est peu réactif et le troisième est moyennement réactif. Dans ce cas, l'équilibreur de charge choisit le temps de réponse le plus court et avec le moins de connexions actives. Cela signifie qu'il essaiera d'abord d'envoyer autant de connexions que possible au serveur le plus réactif. Mais il tient également compte des connexions actives. Disons que ce serveur atteint 30 connexions actives. Ensuite, il passera au troisième serveur car c'est le serveur à réactivité moyenne. Et il enverra du trafic, disons 20 autres requêtes au serveur à réactivité moyenne, et après cela, il passera au deuxième serveur, et il pourrait envoyer encore 10 requêtes à ce premier serveur jusqu'à ce qu'il les redirige vers le premier serveur. Ceci est efficace lorsque l'objectif est de fournir le temps de réponse le plus rapide aux requêtes et que vous avez également des serveurs différents avec des capacités différentes.
La quatrième option est l'algorithme de hachage IP, qui détermine quel serveur reçoit la requête en fonction du hachage de l'adresse IP du client. Ceci est utile lorsque vous souhaitez que vos clients se connectent de manière cohérente au même serveur. Disons que le client un fait une requête à votre équilibreur de charge. L'équilibreur de charge utilisera l'adresse IP du client et, sur cette base, la hachera et l'enverra au serveur approprié. Disons le serveur deux. Et toutes les futures requêtes du client un passeront par l'équilibreur de charge, et il utilisera le même algorithme de hachage IP, et sur la base de cette adresse IP, il redirigera à nouveau les requêtes de l'utilisateur un vers le serveur deux. Ceci est utile s'il est important qu'un client se connecte de manière cohérente à la même application. Si chacun de vos serveurs possède des informations sur les clients qui y sont connectés, alors le hachage IP est un bon choix.
Ensuite, il existe également des algorithmes pondérés. Ce sont des variantes des méthodes ci-dessus qui peuvent également être pondérées. Par exemple, vous pouvez avoir un Round Robin pondéré ou des connexions pondérées les moins nombreuses. Dans ce cas, les serveurs se voient attribuer des poids, généralement basés sur leur capacité et leurs métriques de performance. Par exemple, si le premier serveur a 16 Go de RAM, le deuxième 32 et le troisième 64. Sur la base de la RAM du serveur et d'autres métriques, ils se voient attribuer des poids et l'équilibreur de charge en tient compte lors de la redirection du trafic. Premièrement, il essaiera d'envoyer autant de connexions que possible au premier serveur car il est plus pondéré, ce qui signifie qu'il a plus de performances, puis il essaiera d'envoyer le reste du trafic au serveur deux, puis la dernière et petite partie ira au serveur un.
Il existe également des algorithmes géographiques, qui sont des algorithmes basés sur la localisation qui dirigent les requêtes vers le serveur géographiquement le plus proche de l'utilisateur. Disons que cette application est destinée aux utilisateurs américains. Donc, la plupart des utilisateurs se connectent à cette application depuis les États-Unis. Mais nous avons également une partie des utilisateurs qui se connectent depuis l'Europe. Et dans notre pool de serveurs, nous pouvons avoir un serveur situé dans l'Est des États-Unis, un autre serveur situé dans l'Ouest des États-Unis. Et le dernier serveur peut être situé quelque part en Europe pour la petite base d'utilisateurs située en Europe. Donc, si un utilisateur vient d'Europe et fait une requête à cet équilibreur de charge, il redirigera cet utilisateur vers le serveur en Europe. Ou si un utilisateur vient de vos États-Unis et fait une requête à cet équilibreur de charge, il vérifiera la localisation de cet utilisateur américain en fonction de son adresse IP, puis il redirigera soit vers l'Est des États-Unis, soit vers l'Ouest des États-Unis. Ce type d'équilibrage de charge est utile pour les services mondiaux où la réduction de la latence est importante.
Et le dernier type le plus populaire est le hachage cohérent. Dans ce cas, nous utilisons une fonction de hachage pour distribuer les données sur divers nœuds. Nous avons une fonction de hachage à l'intérieur d'un équilibreur de charge et nous imaginons généralement un espace de hachage avec cela qui forme un anneau de hachage comme un cercle. Cette fonction de hachage forme un cercle où nous avons les serveurs, par exemple le serveur un, deux et trois, qui sont situés devant cet équilibreur de charge. Donc, chaque fois qu'une nouvelle requête arrive d'un utilisateur, cette fonction de hachage prend l'adresse IP de cet utilisateur et, sur cette base, localise cet utilisateur sur cet anneau de hachage. Disons qu'elle le localise quelque part ici, puis en fonction du serveur auquel ce point est le plus proche, par exemple dans ce cas, il est plus proche du serveur deux, elle redirige le trafic vers ce serveur. C'est une façon un peu plus compliquée d'équilibrer la charge, mais elle garantit également que le même client se connecte de manière cohérente au même serveur, comme dans le cas du hachage IP.
Nous avons également parlé du fait que chaque fois qu'un serveur tombe en panne, cet équilibreur de charge garantit que le trafic n'est pas redirigé vers ce serveur. Mais comment sait-il en premier lieu que ce serveur n'est pas disponible ? Pour cela, la plupart des équilibreurs de charge sont dotés de fonctionnalités de vérification de l'état ("health check"), ce qui signifie qu'ils surveillent constamment les serveurs en envoyant des requêtes de vérification de l'état à tous ces serveurs et ils ont des informations sur les serveurs qui sont en ligne. Disons que les trois premiers serveurs sont disponibles et lesquels sont hors ligne, ce qui signifie que le quatrième serveur est hors ligne. Donc, chaque fois qu'il détecte une panne lors de la vérification de l'état, il sait que ce quatrième serveur n'est plus disponible. Et sur la base de ces informations, si la prochaine requête arrive du client, il ne la redirigera pas vers le quatrième serveur jusqu'à ce que la vérification de l'état réussisse à nouveau et qu'il sache que le quatrième serveur est de retour en ligne.
Et maintenant, voyons quelques exemples d'équilibreur de charge. Et qu'est-ce que c'est en fait ? Comment les mettons-nous en œuvre ? Premièrement, nous avons des équilibreurs de charge logiciels. Par exemple, Nginx est probablement le type le plus courant d'équilibreur de charge logiciel. Il possède d'autres fonctionnalités et est également utilisé comme serveur Web, mais il offre également la fonctionnalité d'équilibreur de charge. Généralement, vous installez ce Nginx sur votre serveur, puis vous configurez les serveurs qui doivent être équilibrés en charge, ainsi que l'algorithme. Et comme vous pouvez le voir, il est également doté de vérifications de l'état ("health checks") que j'ai mentionnées. Vous pouvez donc configurer des vérifications de l'état parmi vos serveurs, et cela surveillera constamment vos serveurs, et chaque fois que l'un de vos serveurs tombe en panne, il ne redirigera pas le trafic vers ce serveur. Un autre exemple d'équilibreur de charge logiciel est HAProxy, qui est un logiciel open-source que vous pouvez à nouveau installer sur votre serveur et configurer comme vous le souhaitez. Mais en dehors des équilibreurs de charge logiciels, nous avons également des équilibreurs de charge matériels. Par exemple, nous avons l'équilibreur de charge F5, qui est un équilibreur de charge matériel largement utilisé, connu pour ses hautes performances et son ensemble de fonctionnalités. Ensuite, nous avons Citrix, qui offre également des fonctionnalités d'équilibrage de charge. Et encore une fois, il s'agit d'un type d'équilibreur de charge matériel. Mais si vous ne souhaitez pas configurer tout cela vous-même sur votre serveur ou en tant que matériel, alors les solutions plus faciles sont les équilibreurs de charge basés sur le cloud. Par exemple, AWS propose l'équilibrage de charge élastique ("Elastic Load Balancing"). Et si vous avez également configuré vos serveurs dans AWS, alors il est très facile de les configurer avec vos serveurs. Et vous pouvez également voir dans les avantages qu'il est automatiquement doté de sécurité, de mise à l'échelle automatique, ce qui signifie qu'il ajoutera automatiquement de nouveaux serveurs au pool si la demande de votre application augmente. Et il est également doté de surveillance, ce qui est la même chose que les vérifications de l'état. Vous n'avez donc pas à le configurer vous-même. Et d'autres exemples similaires à AWS sont l'équilibreur de charge d'Azure et l'équilibrage de charge de Google Cloud.
Parlons maintenant du concept appelé "point de défaillance unique" ("single point of failure") en conception de systèmes. C'est une partie de votre système entier qui, lorsqu'elle tombe en panne, entraîne la chute de l'ensemble du système avec elle. Pour le dire simplement, c'est tout composant qui pourrait causer la défaillance de l'ensemble du système lorsqu'il cesse de fonctionner. Par exemple, si vous imaginez cette configuration où les clients se connectent à notre équilibreur de charge, puis que l'équilibreur de charge les distribue aux API, puis que nous avons une base de données unique qui est utilisée par tous les serveurs d'API. La base de données est ici un exemple de point de défaillance unique. Chaque fois que cette base de données tombe en panne, toutes ces API ne pourront pas se connecter à la base de données et, par conséquent, toutes celles-ci ne fonctionneront pas correctement et nos clients ne pourront recevoir aucune réponse des serveurs.
Avoir des points de défaillance uniques dans votre système est problématique car ils peuvent créer des vulnérabilités. Le premier inconvénient évident est la fiabilité, car une seule défaillance, comme la défaillance de cette base de données, peut faire tomber tout le système, ce qui peut entraîner des pertes commerciales, car les utilisateurs ne peuvent pas accéder à notre plateforme. Peut-être qu'ils ne peuvent pas non plus accéder à la page de paiement ou à d'autres parties du système, ce qui peut entraîner des pertes pour l'entreprise. C'est aussi un problème pour l'évolutivité, car les systèmes qui ont des points de défaillance uniques comme celui-ci ont souvent du mal à évoluer, car chaque composant ajoute un risque de faire tomber cette partie unique. Et la dernière partie, cela pose également un problème de sécurité, car si vous avez un point de défaillance unique dans votre système, comme l'équilibreur de charge, les attaquants peuvent compromettre ce point en lui envoyant un trafic énorme, et s'il tombe en panne, tout le système s'effondrera. Nous parlerons de la manière d'éviter les points de défaillance uniques de la base de données dans la section des bases de données. Mais dans cette section, nous pouvons examiner comment éviter que les équilibreurs de charge ne deviennent un point de défaillance unique. Parce que pour l'instant, nous n'avons qu'une seule configuration d'équilibreur de charge. Et si cet équilibreur de charge tombe en panne, alors tous nos utilisateurs ne pourront pas accéder à ce point et ils ne pourront pas non plus accéder à nos API.
La première stratégie consiste à ajouter de la redondance à notre système. Cela signifie que nous pouvons utiliser plus d'un équilibreur de charge, et par exemple, si le deuxième équilibreur de charge tombe en panne, les utilisateurs ne pourront pas se connecter à cet équilibreur de charge, mais dans ce cas, nous pouvons rediriger tout le trafic vers le premier. Et ensuite, ce premier équilibreur de charge répartira la charge entre ces serveurs, et nous surveillerons l'état de ce deuxième équilibreur de charge, et chaque fois qu'il sera de nouveau en ligne et disponible, nous redirigerons également 50 % du trafic vers le deuxième équilibreur de charge.
Une autre stratégie consiste à utiliser des vérifications de l'état et une surveillance pour les équilibreurs de charge eux-mêmes. Comme nous l'avons vu, les équilibreurs de charge peuvent effectuer des vérifications de l'état pour les serveurs et vérifier si nos serveurs sont en ligne ou hors ligne. Nous pouvons utiliser la même stratégie pour les équilibreurs de charge et nous pouvons vérifier leur état en continu, et chaque fois que l'un de nos équilibreurs de charge tombe en panne, nous saurons que nous ne devrions rediriger aucun trafic vers cet équilibreur de charge jusqu'à ce qu'il soit de nouveau en ligne.
Et le troisième type courant est les systèmes auto-réparateurs ("self-healing systems"), ce qui signifie que nous surveillons à nouveau l'état de notre équilibreur de charge. Et si à un moment donné nous détectons qu'il tombe en panne, nous le remplacerons par un nouvel équilibreur de charge, qui est essentiellement une instance de ce même équilibreur de charge. Et de cette façon, nous ne causerons aucune interruption et nos clients pourront se connecter à ce nouvel équilibreur de charge.
Si vous trouvez cela utile jusqu'à présent, j'ai beaucoup plus de contenu sur la conception de systèmes sur ma chaîne YouTube, y compris des analyses approfondies de composants spécifiques et également des études de cas de conception de systèmes à partir de zéro. Recherchez simplement Hikimon sur YouTube ou consultez le lien dans la description.
Bienvenue dans cette section où vous apprendrez les principes fondamentaux de la conception d'API qui vous permettront de créer des interfaces efficaces, évolutives et également maintenables entre les systèmes logiciels. Voici ce que nous allons couvrir dans cette leçon. Nous commencerons par ce que sont les API et quel est leur rôle dans l'architecture système. Ensuite, nous couvrirons les trois styles d'API les plus couramment utilisés, qui sont REST, GraphQL et gRPC. Nous discuterons des quatre principes de conception essentiels qui font de grandes API et également de la manière dont les protocoles d'application influencent les décisions de conception d'API. Nous couvrirons également le processus de conception d'API. Donc, en commençant par la phase de conception, la phase de développement, le déploiement. Nous verrons donc à quoi ressemble ce processus.
Commençons donc par comprendre ce qu'est une API. API signifie Application Programming Interface, qui définit comment les composants logiciels doivent interagir les uns avec les autres. Disons que d'un côté, vous avez le client, qui est soit le téléphone mobile, soit le navigateur de cet utilisateur, et de l'autre côté, vous avez le serveur, qui répondra aux requêtes. Donc, une API est ici juste un contrat qui définit ces termes, qui sont les requêtes qui peuvent être faites. Elle nous fournit donc une interface sur la façon de faire ces requêtes, c'est-à-dire quels points de terminaison nous avons, quelles méthodes nous pouvons utiliser, et ainsi de suite. Aussi, quelles réponses nous pouvons attendre de ce serveur pour un point de terminaison spécifique.
Tout d'abord, c'est un mécanisme d'abstraction, car il masque les détails d'implémentation tout en exposant la fonctionnalité. Par exemple, nous pouvons faire une requête pour enregistrer des données utilisateur sur ce serveur. Mais nous ne nous soucions pas du tout de la façon dont la logique s'applique en coulisses à l'intérieur de ce serveur. Nous ne nous soucions donc que de l'interface fournie par cette API, et nous utilisons uniquement ce point de terminaison et nous enregistrons l'utilisateur sans même connaître les détails d'implémentation, et cela définit également les limites du service, car il définit des interfaces claires entre les systèmes et les composants. Cela nous permet donc d'avoir plusieurs serveurs. Nous pouvons avoir un serveur responsable de la gestion des utilisateurs. Nous pouvons en avoir un autre responsable d'autres enregistrements, disons pour la gestion des publications, et ainsi de suite. Cela permet donc à différents systèmes de communiquer indépendamment de leur implémentation sous-jacente, comme les navigateurs clients avec les serveurs, ou les serveurs avec d'autres serveurs, et ainsi de suite.
Concentrons-nous maintenant sur les styles d'API les plus importants que vous rencontrerez pendant la phase de conception. Il s'agit de RESTful, GraphQL et gRPC. Le plus courant parmi ceux-ci est REST, qui signifie Representational State Transfer. Ces types d'API utilisent une approche basée sur les ressources en utilisant les méthodes HTTP comme protocole. L'un des avantages des API REST est qu'elles sont sans état, ce qui signifie que chaque requête contient toutes les informations nécessaires pour la traiter, et nous n'avons pas besoin de requêtes antérieures pour pouvoir traiter la requête actuelle. Et elle utilise les méthodes standard du protocole HTTP qui sont GET pour récupérer des données, POST pour stocker des données, PUT ou PATCH pour mettre à jour des données, et DELETE pour supprimer des données. Donc, en fonction de ses caractéristiques, REST est le plus couramment utilisé dans les applications Web et mobiles.
Ensuite, nous avons GraphQL, qui est le deuxième style d'API le plus courant après les API REST. GraphQL est un langage de requête qui permet aux clients de demander exactement ce dont ils ont besoin. Cela signifie qu'il est doté d'un seul point de terminaison pour toutes les opérations, et nous pouvons choisir ce que nous attendons de cette API en fournissant la charge utile dans la requête, et les opérations sont appelées "query" lorsque nous récupérons des données ou "mutation" lorsque nous mettons à jour des données. C'est donc l'équivalent de PUT, PATCH ou POST dans les API RESTful, et il existe également une opération "subscription" pour la communication en temps réel. L'avantage des API GraphQL est qu'elles permettent un nombre minimal d'allers-retours. Disons que nous avons besoin de certaines données pour lesquelles, dans les API RESTful, nous devrons faire trois requêtes pour obtenir toutes ces données. Dans le cas de GraphQL, nous pouvons faire une seule requête et obtenir toutes ces données, évitant ainsi les deux requêtes inutiles que nous devrions autrement faire en RESTful. Et à cause de cela, c'est l'option recommandée pour les interfaces utilisateur complexes. Donc, partout où vous avez des interfaces utilisateur complexes où sur une page vous pourriez avoir besoin de données différentes, sur une autre page vous pourriez avoir besoin d'autres données imbriquées complexes. Dans ces cas, GraphQL est un meilleur choix que les API RESTful.
Et la dernière option est gRPC. Je dirais que c'est le moins courant parmi ces trois. gRPC est un framework RPC haute performance qui utilise des protocoles tampons ("protocol buffers") pour la communication. Les méthodes en gRPC sont définies comme des RPC dans les fichiers proto et il prend en charge le streaming et la communication bidirectionnelle. C'est une excellente approche pour les microservices en particulier et la communication interne des systèmes, car elle est plus efficace lorsque vous travaillez entre serveurs par rapport à GraphQL ou par rapport aux API RESTful.
La différence entre les API REST, GraphQL et gRPC est assez claire, mais clarifions également la différence réelle entre les API REST et GraphQL avec des exemples. Donc, comme vous l'avez vu, REST est doté de points de terminaison basés sur les ressources. Par exemple, ici, si nous examinons ces requêtes, vous pouvez voir que la ressource ici est "users". Vous vous attendez donc toujours à voir un point de terminaison "users" ou un point de terminaison "followers" ou disons un point de terminaison "posts". C'est donc basé sur les ressources. Et parfois, nous pourrions avoir besoin de faire plusieurs requêtes pour obtenir les données associées. Comme vous pouvez le voir ici, nous avons besoin, disons, des détails de l'utilisateur, mais nous avons également besoin des publications et des abonnés de l'utilisateur. Donc, dans ce cas, nous devons faire trois requêtes pour obtenir toutes ces données, et elle utilise les méthodes HTTP pour définir les opérations. Comme vous pouvez le voir, ce sont des points de terminaison HTTP et nous utilisons spécifiquement la méthode GET, et les structures de réponse sont fixes, ce qui signifie que si vous obtenez une réponse pour cet utilisateur spécifique, la prochaine fois vous pouvez vous attendre à avoir exactement la même structure de réponse. Peut-être que certaines données seront modifiées, mais la structure reste toujours la même. Et elle fournit également un versionnement explicite. Comme vous pouvez le voir, elle est dotée de v1 pour l'API v1. Ensuite, si elle a subi une mise à niveau majeure, elle deviendra v2, et ainsi de suite. Et vous pouvez utiliser les en-têtes des requêtes pour exploiter la mise en cache HTTP sur les API RESTful.
Maintenant, si nous comparons cela aux API GraphQL, elle est dotée d'un seul point de terminaison pour toutes les opérations. Donc, le plus souvent, c'est /graphql ou /quelque point de terminaison d'API qui est couramment utilisé pour toutes les opérations, et dans ce cas, nous utiliserons une seule requête pour obtenir les données précises dont nous avons besoin, et nous utiliserons le langage de requête de GraphQL. Voici à quoi ressemble le langage de requête. Comme vous pouvez le voir, nous commençons par une requête, puis nous définissons ce dont nous avons besoin. Par exemple, nous avons besoin de l'utilisateur avec l'ID 123. Ensuite, nous avons besoin du nom de l'utilisateur, des publications, puis nous définissons ce dont nous avons besoin des publications. Peut-être que nous avons seulement besoin du titre et du contenu, et rien de plus. Et aussi les abonnés, et ce dont nous avons besoin des abonnés, peut-être seulement les noms. Cela nous permet donc d'être plus efficaces dans nos requêtes par rapport aux API RESTful où nous aurions besoin de faire trois requêtes pour ces mêmes données. Cela signifie que le client doit spécifier la structure de réponse, et dans ce cas, l'évolution du schéma se fait sans versionnement. Donc, ici, comme vous l'avez vu, c'est avec v1, v2, et ainsi de suite. Dans ce cas, le schéma évolue généralement sans versionnement. Mais il existe également un modèle courant pour commencer à versionner les champs. Par exemple, vous pouvez avoir des abonnés v2, et ce sera le deuxième type de schéma d'abonnés. Mais vous pouvez aussi vous passer de versionnement. Vous pouvez donc simplement commencer à modifier les abonnés ou les publications si vous êtes sûr qu'aucun autre client n'utilise votre ancienne API, et dans ce cas, vous pouvez exploiter la mise en cache au niveau de l'application au lieu de la mise en cache HTTP.
Discutons maintenant des principaux principes de conception qui nous permettront de créer des API cohérentes, simples, sécurisées et également performantes. En fin de compte, la meilleure API est celle que nous pouvons utiliser sans même lire la documentation. Par exemple, si vous avez vu les points de terminaison précédents dans les utilisateurs, vous voyez que nous avons /users/123 et évidemment nous nous attendons à obtenir les détails de l'utilisateur de cet utilisateur spécifique. Et si vous faites une requête, par exemple, à ce point de terminaison pour récupérer les détails de l'utilisateur, mais que vous découvrez qu'il met également à jour certains abonnés ou quelque chose pendant cette requête, alors évidemment c'est un très mauvais type d'API car nous ne nous attendions pas à ce qu'elle effectue de telles opérations.
Tout d'abord, une bonne API doit être cohérente, ce qui signifie qu'elle doit utiliser des noms, des casse et des modèles cohérents. Par exemple, si vous utilisez le camel case dans l'un des points de terminaison, disons que vous avez "userDetails" et que vous le faites en camel case, mais dans un autre cas, vous le faites avec un snake case comme "user/details", alors ce n'est pas courant et ce n'est pas cohérent. Le deuxième principe clé est de le garder très simple et de se concentrer sur les cas d'utilisation principaux et une conception intuitive. Vous devez donc minimiser la complexité et viser des conceptions que les développeurs peuvent comprendre rapidement sans même avoir à lire la documentation. Et la simplicité revient encore à ceci : la meilleure API est celle que les développeurs peuvent utiliser sans même lire la documentation.
Ensuite, évidemment, elle doit être sécurisée. Vous devez donc avoir une sorte d'authentification et d'autorisation entre les utilisateurs. De plus, si vous avez des entrées, vous devez vous assurer qu'elles sont validées et vous devez également appliquer une limitation de débit ("rate limiting"). Ce sont donc les choses les plus basiques que vous devez faire pour garder vos API sécurisées. Et le dernier pilier est la performance. Vous devez donc concevoir pour l'efficacité avec des stratégies de mise en cache appropriées, avec la pagination. Si vous avez une grande quantité de données, disons des milliers de publications, vous ne voulez pas récupérer tout cela chaque fois qu'ils font une requête pour obtenir les publications. Vous devriez donc toujours avoir une pagination avec une limite et un décalage. De plus, les charges utiles, c'est-à-dire les données que vous renverrez, doivent être minimisées, et également, chaque fois que possible, vous devez réduire les allers-retours. Donc, si vous avez la possibilité d'envoyer de petites données avec la requête d'un des points de terminaison, il est préférable de le faire si vous savez que vous allez l'utiliser, au lieu de faire une autre requête pour obtenir les mêmes données.
Chacune de ces API utilise différents protocoles, et nous en apprendrons davantage à ce sujet dans la prochaine leçon. Mais fondamentalement, votre choix de protocole façonnera fondamentalement vos options de conception d'API. Par exemple, les fonctionnalités du protocole HTTP permettent directement les capacités RESTful. Il est donc plus logique d'utiliser HTTP avec les API RESTful, car il vous fournit également des codes d'état, et ceux-ci sont parfaits pour être utilisés avec les opérations CRUD que vous aurez dans les API RESTful. D'autre part, WebSockets, qui est un autre type de protocole, permet la communication de données en temps réel et également des API bidirectionnelles. Cela peut donc être utilisé avec des API en temps réel partout où vous avez besoin d'une application de chat ou d'un streaming vidéo. C'est un bon cas d'utilisation des API WebSockets. Dans le cas des API GraphQL, vous utiliserez à nouveau le protocole HTTP au lieu de WebSockets ou gRPC. gRPC, quant à lui, peut être utilisé avec des microservices dans votre architecture pour la rendre plus rapide par rapport à HTTP. Votre choix de protocole affectera donc la structure de l'API, ainsi que les performances et les capacités. Par conséquent, vous devriez le choisir en fonction de ses limitations et de ses forces, et celui qui a le plus de sens pour le type d'API que vous développerez.
Discutons maintenant du processus de conception d'API. Tout commence par la compréhension des exigences, qui consiste à identifier les cas d'utilisation principaux et les histoires d'utilisateurs que vous devrez développer. De plus, définir la portée et les limites, car s'il s'agit d'une API énorme, vous ne développerez probablement pas toutes les fonctionnalités en une seule fois. Vous devriez donc la limiter à certaines fonctionnalités spécifiques que vous développerez et également à ce qui est hors de portée pour l'instant. Ensuite, vous devez déterminer les exigences de performance et, spécifiquement dans le cas de votre API, quels seront les goulots d'étranglement et où vous devez vous assurer qu'elle est performante, et vous ne devez pas non plus négliger les contraintes de sécurité. Vous devez donc implémenter toutes les fonctionnalités de base comme l'authentification, l'autorisation, la limitation de débit, mais peut-être quelques autres choses en fonction de l'API que vous développerez.
En ce qui concerne les approches de conception, il existe plusieurs façons de procéder. La première est l'approche descendante ("top-down"), où vous commencez par les exigences et les flux de travail de haut niveau. C'est plus courant dans les entretiens où l'on vous donne les exigences concernant ce que sera l'API, puis vous commencez à définir quels seront les points de terminaison, quelles seront les opérations, et ainsi de suite. Mais il existe également l'approche ascendante ("bottom-up"), où si vous avez des modèles de données et des capacités existants, vous devez concevoir l'API en fonction de cela. C'est plus courant lorsque vous travaillez dans une entreprise et qu'elle a déjà ses modèles de données et ses capacités d'API. Vous devez donc en tenir compte lors de la conception de l'API. Et nous avons également l'approche "contrat d'abord" ("contract first"), où vous définissez le contrat d'API avant l'implémentation, c'est-à-dire à quoi devraient ressembler les requêtes et à quoi devraient ressembler les réponses, et c'est plus similaire à l'approche descendante, et c'est aussi couramment utilisé dans les entretiens.
En ce qui concerne la gestion du cycle de vie des API, elle commence par la phase de conception où vous concevez l'API, discutez des exigences et des résultats attendus de l'API, et ce n'est qu'après cela que vous pouvez commencer le développement et peut-être les tests locaux de votre API. Après cela, vous la déployez et la surveillez généralement. Vous faites donc d'autres tests, mais maintenant sur staging ou en production. Mais ensuite, il y a aussi la phase de maintenance. Et c'est pourquoi il est important de la développer en gardant la simplicité à l'esprit. Il sera donc plus facile pour vous de la maintenir ou pour d'autres développeurs de la maintenir à l'avenir. Et enfin, les API passent également par une phase d'obsolescence et de retrait. Certaines API finissent par être obsolètes car il pourrait y avoir une nouvelle version de l'API que vous devriez utiliser, ou disons que vous passez de l'API V1 à l'API V2. C'est donc aussi la phase d'obsolescence de l'API v1. Développer des API n'est donc pas seulement dans la phase de développement, comme vous pourriez le penser, ce n'est pas juste du codage. Une grande partie consiste à la concevoir, ainsi qu'à la maintenir, et enfin, vous pourriez avoir besoin de la retirer à la fin.
Récapitulons et voyons quelles sont nos prochaines étapes. Nous avons appris ce que sont les API et les trois types d'API les plus dominants qui sont RESTful, GraphQL et gRPC. Nous avons couvert les quatre principes clés qui nous guideront lors de la création de conceptions d'API efficaces. Et vous comprenez maintenant également comment le choix de conception de votre protocole influencera la conception de votre API et également l'ensemble du processus de conception d'API du début à la fin. Mais nous n'avons pas discuté des limitations et des forces de ces protocoles d'API. C'est pourquoi, dans la prochaine leçon, nous apprendrons tout sur les protocoles d'API que nous pouvons utiliser avec la conception d'API et lequel nous devrions choisir en fonction des exigences de notre API.
Choisir le mauvais protocole pour notre API peut entraîner des goulots d'étranglement de performance et également des limitations de fonctionnalité. C'est pourquoi nous devons d'abord comprendre ces protocoles, ce qui nous permettra de construire des API qui répondent à nos exigences utilisateur spécifiques en matière de latence, de débit et également de modèles d'interaction. C'est pourquoi, dans cette leçon, nous couvrirons le rôle des protocoles d'API dans la pile réseau. Les deux protocoles fondamentaux que sont HTTP et HTTPS, ainsi que leur relation avec les API. Également un autre type de protocole courant, qui est WebSockets pour la communication en temps réel. Nous couvrirons également le protocole AMQP (Advanced Message Queuing Protocol), qui est couramment
utilisé pour la communication asynchrone et enfin nous aborderons le gRPC, qui est l'appel de procédure à distance de Google, et c'est aussi un autre type de protocole courant utilisé couramment au sein des serveurs. Commençons par comprendre les protocoles d'application dans la pile réseau. Les protocoles de la couche application se situent au sommet de la pile réseau, s'appuyant sur des protocoles comme TCP et UDP, qui se trouvent à la couche transport. Ces protocoles de la couche application définissent les formats et structures des messages, ainsi que les modèles de requête-réponse, la gestion des connexions et la gestion des erreurs. Maintenant, en dessous, nous avons de nombreuses autres couches comme la couche réseau, la couche de liaison de données, voire les couches physiques. Mais lors de la création d'API, nous nous intéressons principalement aux protocoles de la couche API qui sont HTTP, HTTPS, websockets, etc. Le type de protocole le plus courant et aussi le fondement des API web est HTTP, qui signifie Hypertext Transfer Protocol. C'est l'interaction typique entre le client et le serveur lorsqu'ils interagissent sur HTTP. Comme vous pouvez le voir, le client envoie toujours une requête et définit la méthode qui peut être GET, POST ou d'autres méthodes, et il définit l'URL de la ressource qui peut être /API/produits. Disons qu'ils demandent des données pour cet identifiant spécifique du produit, et ils définissent également la version du protocole HTTP qu'ils utilisent. Ils définissent également l'hôte, qui est le domaine de votre serveur où les informations sont accessibles, et généralement, ils s'authentifient également avant d'accéder à des ressources. Il peut donc s'agir d'un jeton porteur ou d'une authentification de base, etc. Ainsi, une fois la requête authentifiée sur le serveur, celui-ci reçoit la réponse qui est dans un format similaire, et c'est une réponse HTTP. Vous obtenez donc la version HTTP, qui est à nouveau la même que celle que vous avez demandée, et le code d'état qui peut être 200 si tout s'est bien passé, ou 400 si le client a fait une erreur, ou 500 si l'erreur s'est produite sur le serveur, etc. Vous recevez le type de contenu qui peut généralement être application/JSON, mais il peut aussi s'agir d'une page web statique ou autre chose. Et il existe de nombreux autres en-têtes que vous pouvez contrôler, comme le contrôle du cache. Vous pouvez utiliser l'en-tête Cache-Control ou d'autres propriétés. Mais ce sont les éléments principaux que vous remarquerez dans les cycles de requête-réponse HTTP. Maintenant, en ce qui concerne les méthodes, vous avez GET pour récupérer des données, POST pour créer des données sur le serveur, PUT ou PATCH pour mettre à jour des données partiellement ou entièrement, et DELETE pour supprimer des données du serveur. Et en ce qui concerne les codes d'état reçus par le serveur. Vous avez la série 200 qui concerne les cas de succès, vous avez la série 300 pour la redirection. 400 signifie que le client a fait une erreur dans la requête. C'est donc un problème côté client, ou 500, ce qui signifie que le serveur a fait une erreur ou qu'une erreur s'est produite sur le serveur, ce qui signifie que c'est un problème sur ce serveur. Et ce sont les en-têtes courants comme Content-Type, qui est généralement défini par le serveur, mais aussi par le client. Authorization pour faire une requête et autoriser auprès du serveur, Accept headers, Cache-Control, User-Agent, et il y a d'autres en-têtes, mais ce sont les plus courants. Ensuite, nous avons aussi HTTPS, qui est fondamentalement le même protocole HTTP mais avec une sorte de chiffrement TLS ou SSL, ce qui signifie que nos données sont maintenant protégées pendant le transit lorsque nous faisons des requêtes. Il ajoute donc une couche de sécurité grâce à ces certificats TLS ou SSL et au chiffrement, et il protège les données pendant le transit. Les avantages de HTTPS sont évidemment que vos données sont chiffrées pendant le transit. Il offre l'intégrité des données et vous authentifiez également les utilisateurs avant de fournir des données, et il ajoute également des avantages SEO. Et vous avez de nombreux risques lorsque vous utilisez uniquement HTTP sans aucun chiffrement. La norme d'or est donc d'utiliser toujours HTTPS sur les serveurs. Le type de protocole suivant est WebSockets. Alors que nous avons HTTP, qui est très bon pour les modèles de requête-réponse, HTTP a parfois des limitations. Par exemple, disons que vous récupérez des données. Disons qu'il s'agit d'un chat utilisateur. Vous avez le client et le serveur. Côté client, vous avez le chat utilisateur, et côté serveur, vous avez les messages entre deux utilisateurs. Lorsque l'un des utilisateurs envoie un message à l'autre, il envoie une requête au serveur pour notifier qu'un message a été envoyé. Et il reçoit une réponse du serveur, peut-être les messages des autres utilisateurs s'il y en a. Et ensuite, si vous avez besoin de savoir si vous avez de nouveaux messages, vous devez refaire une autre requête au serveur, et peut-être que vous n'avez aucun nouveau message. Ce fut donc essentiellement un cycle de requête-réponse non nécessaire, et vous pourriez demander à partir d'un autre moment, disons à partir d'une minute, et recevoir une réponse. Maintenant, vous avez des messages, mais cela peut aussi être vide à nouveau. Cette méthode n'est donc pas idéale pour la communication en temps réel. Comme vous pouvez le constater, vous augmentez la latence, vous gaspillez de la bande passante en faisant des requêtes qui sont vides, et vous utilisez également les ressources du serveur sans avoir besoin de faire des requêtes au serveur. Et pour de tels cas, nous avons les WebSockets qui résolvent ce problème. Dans WebSocket, il y a généralement une poignée de main qui se produit lors de la première requête. Et maintenant, vous avez une communication bidirectionnelle entre le client et le serveur. Ce qui signifie qu'une fois la poignée de main effectuée, le serveur peut décider indépendamment de pousser des données vers le client. Disons que vous avez maintenant deux nouveaux messages sur le serveur. Le serveur peut donc décider d'envoyer ces messages au client sans même que le client ne les demande. Mais le client peut toujours demander des données. Donc, si le client a besoin de données externes ou de plus de données du serveur, il peut toujours faire des requêtes. Mais le serveur est maintenant également capable de pousser indépendamment des données vers le client. C'est donc ce qui permet les données en temps réel avec une latence minimale. Dès que vous avez de nouvelles données sur le serveur, il les pousse vers le client et cela réduit également l'utilisation de la bande passante en permettant une communication bidirectionnelle. Dans le modèle client-serveur avec HTTP, vous feriez, disons, de nouvelles requêtes toutes les 5 secondes ou 10 secondes pour voir s'il y a de nouvelles données sur le serveur. Mais dans ce scénario, vous ne faites plus de requêtes autres que la première. Et maintenant, dès qu'il y a de nouvelles données, le serveur les poussera. Et quand il n'y a pas de données à demander, alors vous n'avez pas besoin de faire de requêtes inutiles au serveur. Le type de protocole très courant suivant est Advanced Message Queuing Protocol, qui est un protocole de messagerie d'entreprise utilisé pour la mise en file d'attente de messages et la garantie de livraison. Dans cette configuration, vous avez généralement le producteur, qui peut être un service web, un système de paiement ou autre chose. Et de l'autre côté, vous avez le consommateur, qui peut être le processeur des paiements, les systèmes de notification, etc. Le producteur publie donc des messages vers le courtier de messages, et c'est là que vous avez le Advanced Message Queuing Protocol. Vous avez des files d'attente au milieu. Disons que l'une de ces files d'attente est pour le traitement des commandes. Donc, chaque fois qu'une nouvelle commande est passée, le producteur publie un message dans cette file d'attente. Et ensuite, lorsque le consommateur est libre, il peut récupérer des messages de cette file d'attente et commencer à mettre à jour l'inventaire et les données dans la base de données. Cela permet au consommateur de ne récupérer des données d'ici que lorsqu'il a de la capacité. Et lorsque ce consommateur est occupé à d'autres tâches, il laisse le message dans la file d'attente, puis plus tard, lorsqu'il a de la capacité libre, il récupérera le message et commencera à mettre à jour les données. Et en ce qui concerne les types d'échange, vous avez des échanges directs un à un, des diffusions ou des communications basées sur des sujets. Et nous explorerons cela plus en détail lorsque nous aborderons la section sur la mise en file d'attente des messages. L'autre type de protocole courant est gRPC, qui fonctionne avec les Protocol Buffers. Il s'agit d'un framework RPC haute performance inventé par Google et il utilise HTTP/2 pour le transport, c'est-à-dire la deuxième version de HTTP. Cela signifie que les clients doivent prendre en charge HTTP/2, sinon cela ne peut pas être utilisé entre le client et le serveur, mais c'est pourquoi il est le plus couramment utilisé entre les serveurs. Donc, généralement, le client est un autre serveur, et nous avons d'autres microservices qui communiquent entre eux avec ce framework gRPC. Il utilise principalement les Protocol Buffers et il est également doté de capacités de streaming intégrées car il utilise HTTP/2. Ce sont donc les types de protocoles d'API les plus courants. Il en existe beaucoup d'autres, mais généralement dans 90 % des cas, vous ne verrez que ces protocoles. Et lors du choix du bon, vous devez principalement tenir compte des modèles d'interaction. Généralement, par défaut, vous optez pour HTTP s'il s'agit simplement d'un cycle de requête-réponse. Mais si vous construisez quelque chose comme un chat en temps réel ou une communication en temps réel, alors vous devrez opter pour les WebSockets. Le choix dépend également des exigences de performance. Donc, si vous avez plusieurs serveurs, des microservices qui communiquent entre eux et qu'il n'y a pas d'opportunité d'utiliser gRPC, par exemple, alors vous pouvez y recourir pour augmenter les performances et la vitesse de la communication. Mais cela dépend aussi de la compatibilité du client. Par exemple, la plupart des navigateurs ne prennent pas en charge la dernière version de HTTP. C'est pourquoi gRPC n'est pas si courant pour la communication navigateur-serveur. Cela dépend également de la taille de la charge utile, c'est-à-dire du volume des données et de l'encodage, des besoins en sécurité basés sur l'authentification, le chiffrement, etc., et également de l'expérience du développeur. Donc, les outils et la documentation, et cela dépend aussi de l'expérience du développeur, car vous allez principalement travailler avec cette API, et elle doit avoir une bonne documentation et de bons outils pour que vous puissiez travailler pleinement avec ce type de protocole d'API. Pour récapituler, nous avons exploré le rôle des protocoles d'application dans la pile réseau. HTTP et HTTPS, qui sont les types de protocoles les plus fondamentaux, WebSockets pour la communication en temps réel, AMQP, qui signifie Advanced Message Queuing Protocol, qui nous permet d'avoir une communication asynchrone et d'ajouter des files d'attente de messages entre le consommateur et le producteur, et également gRPC, qui signifie Google Remote Procedure Call. Et le principal avantage est qu'il s'agit d'un framework RPC haute performance qui utilise HTTP/2 pour le transport. Nous avons donc discuté de la couche application, qui comprend ces protocoles que nous utilisons généralement pour construire des API. Mais nous ne connaissons pas encore la couche transport, qui comprend TCP et UDP. Dans la prochaine leçon, nous discuterons de cette couche et comprendrons lesquels de ces couches de transport, TCP ou UDP, sont le meilleur choix en fonction de l'API que nous construisons. La plupart des développeurs travaillent avec des API mais ne pensent jamais à ce qui livre réellement ces paquets, comment cela se fait-il que la requête soit faite du client au serveur et comment cette requête traverse Internet. C'est là qu'intervient la deuxième couche du modèle OSI, qui est la couche de transport, qui contient TCP et UDP. Ce sont tous deux des protocoles de couche de transport, ce qui signifie qu'ils gèrent la façon dont les données se déplacent d'une machine à une autre sur le réseau, mais tous deux le font de manière très différente. Dans cette leçon, nous allons apprendre ces protocoles de couche de transport. Nous commencerons par TCP, qui est la version fiable mais plus lente. Ensuite, nous apprendrons UDP, qui est en bref, la version plus rapide et non fiable de TCP, et nous comparerons les deux et déciderons lequel nous devons choisir en fonction des exigences de l'API. Commençons par TCP, qui signifie Transmission Control Protocol. Pensez-y comme à l'envoi d'un paquet avec un suivi de reçu et aussi une signature requise. Donc, lorsque vous envoyez des paquets sur Internet, vous n'envoyez généralement pas tout en une seule fois. Parfois, les données sont plus volumineuses. Disons qu'elles sont divisées en trois morceaux. Vous devez donc les envoyer séparément. Le premier morceau, le deuxième morceau, et aussi le troisième morceau. Dans ce cas, TCP garantit la livraison de ces trois morceaux. Si l'un de ces paquets est perdu ou arrive dans le désordre, TCP le renverra ou le réordonnera. C'est aussi basé sur la connexion, ce qui signifie qu'avant d'envoyer des données, il effectue une poignée de main en trois étapes, qui établit la connexion entre le client et le serveur. Il ordonne également ces paquets. Disons que le client reçoit d'abord le premier paquet, puis le troisième, puis le deuxième. Il s'assure qu'il est réordonné en premier, deuxième et troisième. Cela ajoute bien sûr une surcharge, mais cela garantit qu'il est précis et fiable. C'est pourquoi les API impliquant des paiements, l'authentification ou les données utilisateur utilisent toujours TCP. D'autre part, nous avons UDP, qui signifie User Datagram Protocol. C'est rapide et efficace. Mais le revers de la médaille est qu'il ne garantit pas que tous les paquets arriveront. Par exemple, si vous envoyez quatre paquets du serveur au client, l'un de ces paquets pourrait être perdu et ne sera pas envoyé au client, et UDP ne s'assurera pas qu'il sera finalement livré. Il n'y a donc aucune garantie de livraison. Il n'y a pas non plus de poignée de main, de connexion ou de suivi. Mais en raison de ces compromis, la transmission est plus rapide et elle a moins de surcharge car elle n'a pas besoin de s'assurer que tous les paquets sont livrés ou dans le bon ordre. Par exemple, dans les appels vidéo, UDP peut être le meilleur protocole car si certaines informations sont coupées au milieu, ou disons que vous êtes en appel avec quelqu'un et que sa connexion Internet est faible, vous n'avez pas besoin de recevoir cette ancienne connexion ou les anciennes données de ce qu'il a dit car vous êtes en appel maintenant. UDP est donc la solution pour les appels vidéo, les jeux en ligne ou les flux en direct, car si l'un de ces paquets est perdu, ce n'est pas grave et vous n'avez pas besoin de revenir en arrière et de renvoyer ce paquet. Vous pouvez simplement continuer et envoyer les paquets suivants. Voici à quoi ressemble la poignée de main en trois étapes dans TCP. Comme vous pouvez le voir, la première étape est que le client envoie une requête au serveur. Dans la deuxième étape, le serveur synchronise et accuse réception de la requête. Et dans la première étape, le client accuse réception du serveur, et c'est là que la connexion est établie entre le client et le serveur. Et maintenant, ils peuvent commencer à envoyer des données dans les deux sens sur ce protocole TCP. En bref, TCP est la version plus sûre et fiable de UDP, mais elle est plus lente. Et d'autre part, UDP est plus rapide et léger, mais il est risqué. Par exemple, si l'un des paquets entre la source et la destination est perdu, il ne le renvoie pas. Il n'y a donc aucune garantie de livraison. Mais d'un autre côté, si dans TCP un paquet est perdu après un certain délai, il renvoie toujours les premiers paquets. Et de cette façon, il garantit que toutes les données seront livrées par rapport à UDP où certaines données pourraient être perdues, mais il continuera. Et lors du choix entre les deux, voici les principaux éléments à rechercher. Si vous avez besoin que la connexion soit sûre et fiable, alors vous devez opter pour TCP. Ou si vous avez besoin qu'elle soit rapide, légère, mais que certaines pertes de données soient acceptables, alors vous devrez opter pour UDP. Par exemple, il est préférable d'utiliser TCP dans les banques, les e-mails, les paiements, etc. Et d'autre part, UDP est principalement utilisé dans le streaming vidéo, les jeux, etc. Ce sont les principaux éléments que vous devez connaître sur les couches application et transport. Et ce sont les seules couches qui seront nécessaires pour construire des API. Et dans la prochaine leçon, nous parlerons des API RESTful et de la façon dont nous concevons généralement les API au format RESTful. Les API RESTful permettent à différentes parties d'un système de communiquer entre elles en utilisant les méthodes HTTP standard. Elles sont le moyen le plus courant pour les développeurs de construire et de consommer des API aujourd'hui. Et dans cette vidéo, vous apprendrez à concevoir des API REST propres en suivant les meilleures pratiques éprouvées afin d'éviter de créer des modèles désordonnés et incohérents qui rendent les API difficiles à utiliser et à maintenir. Nous commencerons par apprendre les principes architecturaux et les contraintes des API RESTful, la modélisation des ressources et la conception des URL. Également, les codes d'état et la gestion des erreurs, ainsi que le filtrage, le tri, etc. et nous apprendrons les meilleures pratiques lors de l'utilisation et du développement d'API RESTful. Commençons par la modélisation des ressources. Les ressources sont les concepts centraux dans REST. Disons que vous avez le domaine métier qui comprend les produits, les commandes et les avis. Lors de la modélisation de ceux-ci dans une API RESTful, vous les convertissez généralement en noms et non en verbes. Cela signifie que le produit devient "produits", la commande devient "commandes", et de même pour les avis. Ceux-ci peuvent être des collections ou des éléments individuels. Par exemple, cette première requête, qui est vers /API/produits, vous retournera la collection de produits, pas un seul produit. Mais d'un autre côté, vous pourriez avoir /produits/ et /produits/{id spécifique d'un produit} qui vous retournera l'élément individuel. Et remarquez que nous utilisons /produits lors de la récupération de la collection de produits. Et nous n'utilisons pas quelque chose comme get products, ce qui ne serait pas une bonne pratique dans les API RESTful. Comme je l'ai mentionné, nous utilisons des noms ici et non des verbes. Donc, pour récupérer les commandes par exemple, vous ne définissez pas l'URL comme get orders. Vous la définissez simplement comme /orders et en fonction de la méthode que nous utiliserons. Disons que c'est une méthode GET, alors vous récupérerez les commandes. Si c'est une méthode POST, alors vous créerez une commande, etc. Toutes les ressources doivent donc être clairement identifiables via les URL. Par exemple, voici un exemple de récupération d'une collection. Voici un exemple de récupération d'un élément spécifique. Et les ressources imbriquées doivent également être clairement définies. Par exemple, si vous souhaitez récupérer les avis pour un produit spécifique, nous supposerions que si vous faites une requête vers /produits/{id du produit}/avis, vous obtiendrez les avis pour ce produit spécifique. Mais dans les API du monde réel, vous voulez rarement retourner tous les résultats en une seule fois. C'est pourquoi nous incorporons généralement le filtrage, le tri et la pagination dans les API. Commençons donc par le filtrage. Par exemple, si vous faites une requête pour obtenir tous les produits, vous ajoutez généralement un paramètre de requête qui, dans ce cas, vous pouvez voir, est la catégorie. Vous les filtrez donc d'abord par catégorie. Et ensuite, avec le signe "et", vous ajoutez qu'ils doivent être en stock. Donc, "en stock" doit être vrai. Et de cette façon, vous ne retournez que les articles que vous allez afficher dans l'interface utilisateur, et vous ne faites pas de requêtes qui gaspilleront la bande passante de cette API, et ce sera aussi une réponse énorme pour vous côté front-end. Ensuite, vous avez également le tri. Dans ce cas, il est à nouveau contrôlé par les paramètres de requête, et les paramètres de requête sont tout ce qui commence après le point d'interrogation dans l'URL. Donc, dans ce cas, vous passez généralement l'attribut de tri, et cela peut être, par exemple, ascendant par prix ou ascendant par avis, ou cela peut aussi être l'ordre descendant. Donc, en fonction de cela, vous obtiendrez la réponse de l'API dans un ordre trié, car si vous avez, par exemple, mille articles dans le back-end dans la base de données, vous ne voulez pas récupérer tous ces articles dans un ordre non trié vers le front-end, car disons que le front-end doit maintenant les trier par prix ascendant. Cela signifie qu'il doit faire une requête pour obtenir tous les produits, qui sont ces mille articles que vous avez dans la base de données. Ce serait donc très inefficace. C'est pourquoi nous faisons le tri dans le back-end à la place. Votre back-end doit donc prendre en charge la fonctionnalité de tri. De cette façon, le front-end peut simplement faire une requête à votre back-end et passer ce paramètre de requête de tri, et ainsi il obtiendra les produits triés à afficher à l'écran. Et ensuite, nous avons également la pagination. Encore une fois, avec le paramètre de requête, vous passez généralement la page que vous souhaitez récupérer, ainsi que la limite, car si vous ne passez pas la limite, cela vous donnera à nouveau tous les produits à partir de la page deux jusqu'à la fin, ce qui peut être beaucoup d'articles. Vous passez donc également une sorte de limite, et cette limite est ce que vous allez afficher sur le front-end, puis en fonction de cela, vous obtiendrez la réponse, et ici, disons que vous avez récupéré 10 articles. Vous allez donc afficher ces 10 articles dans l'interface utilisateur, puis une fois qu'ils cliquent sur la page suivante, vous ferez une autre requête à la page trois cette fois, et vous obtiendrez les articles suivants du serveur. Maintenant, nous utilisons généralement "page" pour la pagination, mais il existe un autre attribut courant, qui est "offset". Donc, certaines API utilisent "offset" au lieu de "page", et elles l'utilisent en combinaison avec "limit", ce qui signifie essentiellement que si vous avez mille articles. Donc, "offset" indiquera à l'API à partir d'où commencer à compter ces mille articles, puis "limit" est le même que vous l'avez ici. Il limite donc essentiellement le nombre d'articles que vous obtenez de cet "offset" pour les récupérer vers le front-end. Et la dernière option, vous pouvez également avoir une pagination basée sur curseur. Donc, au lieu de "page" et "limit", vous passeriez un curseur qui serait le hachage de la page que vous souhaitez récupérer. Cette approche d'ajout de filtrage, de tri et de pagination présente des avantages. Tout d'abord, elle économise la bande passante de votre serveur. Elle améliore également les performances côté serveur et côté front-end, et elle donne également plus de flexibilité au front-end, car vous pouvez maintenant récupérer uniquement ce dont vous avez besoin, et non des données inutiles de la base de données. Passons maintenant aux méthodes HTTP que les API REST utilisent, car elles s'appuient sur le protocole HTTP et utilisent donc les méthodes HTTP, en particulier pour les opérations CRUD. Ce sont donc les types d'opérations CRUD les plus courants que vous verrez dans les API REST. Tout d'abord, nous avons la méthode GET, qui est utilisée pour lire des données à partir de l'API. C'est donc pour récupérer des ressources, comme vous l'avez vu, récupérer les produits, récupérer les avis, etc. Et l'URL ressemble généralement à ceci : vous faites une requête GET vers /API/version de l'API/nom de la ressource. Et ces types de requêtes sont à la fois sûrs et idempotents. Ce qui signifie essentiellement que si vous faites une requête à /produits deux ou trois fois, vous vous attendez à recevoir exactement le même résultat à chaque fois, à moins que de nouveaux produits n'aient évidemment été ajoutés à la base de données. Ensuite, nous avons la méthode POST. C'est généralement lorsque vous créez une ressource sur votre serveur. L'exemple courant est à nouveau que vous ferez la requête vers le même point d'accès que vous avez pour le GET afin de créer une collection, mais dans ce cas, au lieu de GET, vous utilisez la méthode POST, et cela indique à l'API que vous devez créer une ressource dans les produits et non les récupérer. Ces types de requêtes modifient l'état du serveur. Ils ajoutent un nouvel élément et ils ne sont pas idempotents, ce qui signifie qu'ils créent une ressource. Donc, la première fois que vous créez une ressource, vous obtiendrez l'ID du premier élément que vous avez créé. La deuxième fois que vous le créez, vous obtiendrez l'ID du deuxième, et ainsi de suite. Ensuite, nous avons les méthodes PUT et PATCH, qui sont très similaires, mais elles mettent à jour des ressources dans votre API, mais elles le font un peu différemment. La méthode PUT remplace la ressource entière, tandis que la méthode PATCH met à jour partiellement la ressource dans votre API. Vous pouvez voir que l'URL de la requête est exactement la même dans les deux cas. C'est donc vers /produits/{id d'un produit que vous souhaitez modifier}. Dans le cas de la requête PUT, elle prendra tout ce produit avec l'ID 123 et le remplacera par le nouveau qui provient du front-end. Alors que dans le cas de PATCH, elle prendra à nouveau cet élément de la base de données avec l'ID 123, mais elle le mettra à jour partiellement. Disons que vous avez juste mis à jour le titre depuis le front-end et que vous avez fait la requête avec la méthode PATCH. Cela ne mettra à jour que le titre de ce produit et laissera les autres parties, les autres propriétés inchangées. Et la dernière opération CRUD est DELETE, et nous utilisons la méthode DELETE dans ce cas, et évidemment, comme son nom l'indique, elle supprime la ressource de la base de données. Donc, encore une fois, l'URL est exactement la même que celle que vous avez pour modifier des éléments. C'est vers /produits/{id de la ressource}, et dans ce cas, vous ne passez rien dans le corps de la requête. Vous faites donc simplement une requête DELETE à cet élément et vous le supprimez de la base de données, et chacune de ces opérations vous renvoie des codes d'état différents en fonction de la façon dont la requête s'est déroulée, qu'elle ait réussi ou non. Pour cela, nous avons les codes d'état et la gestion des erreurs dans les API RESTful. Vous devez donc utiliser les codes d'état appropriés lorsque vous travaillez avec des API REST. Par exemple, la série 200 concerne les requêtes réussies. Par exemple, 200 signifie OK. 201 signifie que la ressource a été créée. 204 signifie qu'il n'y a pas de contenu ici. Disons que vous avez fait une requête, la requête précédente dont nous parlions, vers /produits/{id d'un produit}, et vous avez récupéré cet élément avec succès. Cela signifie que vous devez également définir le code d'état sur 200, car la requête a été réussie. Dans l'autre cas, lorsque vous créez un produit et que vous faites une requête POST vers /produits, cette fois vous ne devriez pas répondre avec le même code 200, car 200 signifie généralement que le statut était OK. Mais dans le cas 201, cela signifie que la ressource a été créée. Et dans ce cas, puisque vous créez un nouveau produit, vous devriez évidemment répondre avec le code d'état 201, signifiant que la ressource a été créée. Nous avons également la série 300, qui concerne les redirections. Disons que vous faites une requête vers une URL et que cette URL a été déplacée ailleurs. Elle répondra donc avec une série 300 et vous redirigera vers la nouvelle URL. Dans la série 400, nous avons les erreurs client. C'est donc chaque fois que votre front-end a fait une mauvaise requête ou que l'utilisateur a fait une mauvaise requête. Par exemple, 400 est une mauvaise requête générique. Dans 401, nous avons des requêtes non autorisées, ce qui signifie que l'utilisateur n'est pas authentifié pour faire cette requête. Pour 404, nous avons "non trouvé". Donc, généralement, lorsque vous visitez une URL ou que vous faites une requête pour une ressource spécifique qui n'existe pas, vous obtiendrez ce code d'état 404. Donc, dans le cas 400, disons que vous avez fait une requête avec des paramètres invalides ou un format JSON incorrect. Dans ce cas, vous obtiendrez une mauvaise requête générique 400. Mais si un utilisateur fait une requête pour obtenir un produit qui est, disons, le produit avec cet ID, et qu'il n'existe pas dans la base de données après l'avoir interrogée, alors vous devriez répondre avec le code d'état 404, signifiant que la ressource n'a pas été trouvée. Et enfin, nous avons la série 500. Ce sont des choses qui se produisent lorsqu'une erreur se produit sur votre serveur. Vous ne connaissez pas la raison exacte et ce n'est pas non plus une erreur client, ce qui signifie que le client a tout demandé correctement, et dans ce cas, nous lançons des erreurs inattendues côté serveur. Vous répondez généralement avec un message d'erreur serveur et vous renvoyez le code d'état 500 avec. En ce qui concerne les meilleures pratiques des API RESTful, remarquez d'abord que nous utilisons des noms pluriels pour toutes les ressources. Donc, au lieu de /produit, nous utilisons /produits pour récupérer la collection de produits. Vous devriez donc toujours utiliser le pluriel dans ce cas. De plus, dans les opérations CRUD, nous utilisons les méthodes HTTP appropriées. Par exemple, lorsque nous faisons une requête pour supprimer des utilisateurs, nous nous attendons à faire une requête à /utilisateurs/{id d'un utilisateur} et non une requête POST à /utilisateurs/{id}. Donc, tout d'abord, les méthodes HTTP doivent être correctement configurées, ainsi que l'URL. Nous ne nous attendons pas à des choses aléatoires comme /delete pour supprimer une ressource de la base de données. Comme vous l'avez vu, nous prenons également en charge le filtrage, le tri et la pagination dans les bonnes API REST. Pas seulement la pagination, par exemple, dans ce cas, nous n'avons que la page trois, mais nous ne pouvons pas limiter la quantité de produits que nous voulons récupérer. Alors que dans ce cas, nous pouvons contrôler entièrement ce que nous voulons obtenir de l'API. Nous voulons obtenir les articles de la page trois. Nous voulons que ce nombre de limites soit appliqué aux produits. Et nous voulons également appliquer un tri, comme le tri du prix ou le tri par notes, etc. Et aussi la gestion des versions dans les API RESTful. Comme vous l'avez remarqué dans toutes ces requêtes, elles viennent toutes avec un préfixe qui est /API, puis /l'ID de l'API, qui est soit v1, v2, v3, etc. Parce que, disons, à l'avenir, vous migrez votre API et vous commencez à utiliser un tas de nouvelles fonctionnalités, mais vous cassez également quelque chose dans la version précédente un, alors si vous utilisez la gestion des versions, vous ne la casserez pas sur le front-end, car ils peuvent utiliser l'ancienne version de votre API et toujours utiliser les anciennes fonctionnalités et fonctionnalités pendant que vous continuez à développer la nouvelle version, disons la version trois, et vous supportez de nouvelles fonctionnalités ici, et vous pourriez avoir cassé quelque chose ici, mais ils utilisent toujours l'ancienne API, donc cela n'a pas d'impact sur les utilisateurs finaux. Pour récapituler, nous avons appris les principes et contraintes architecturales REST. Également sur la modélisation des ressources et la conception des URL, et comment nous modélisons le domaine métier dans le domaine des API RESTful. Également les codes d'état, la gestion des erreurs et les méthodes appropriées à utiliser avec les opérations CRUD de base. Et enfin, nous avons couvert les meilleures pratiques pour les API RESTful que vous devriez utiliser pour garder vos API cohérentes et également prévisibles pour les autres développeurs qui les utilisent. Les API RESTful traditionnelles renvoient souvent trop ou trop peu de données, ce qui nous oblige à faire plusieurs requêtes pour une seule vue afin d'obtenir toutes les données dont nous avons besoin. GraphQL résout ce problème en donnant aux clients exactement ce qu'ils ont demandé. Mais la conception des API GraphQL est différente de la conception des API RESTful. C'est pourquoi dans cette vidéo, nous couvrirons les concepts clés de GraphQL et pourquoi il existe. La conception du schéma et le système de types de GraphQL, les requêtes et mutations, la gestion des erreurs, et également les meilleures pratiques pour la conception des API GraphQL. Commençons par comprendre pourquoi GraphQL existe en premier lieu. Il a été créé par Facebook pour résoudre un problème très spécifique : les clients ayant besoin de faire plusieurs appels d'API et de ne toujours pas obtenir les données exactes dont ils avaient besoin. Par exemple, si vous imaginez que nous avons les API Facebook comme l'API utilisateur, l'API des publications, les commentaires et les likes pour la page Facebook. La plupart du temps, le client peut faire des requêtes à toutes ces API séparément et ne toujours pas obtenir toutes les données dont il a besoin, ce qui nécessiterait de faire plusieurs requêtes à la même API. Cela augmente bien sûr la latence globale de la page, car la page ne se charge pas tant que toutes ces requêtes ne sont pas effectuées et que les données ne sont pas récupérées. Mais dans le cas des API GraphQL, vous avez un point de terminaison GraphQL unique. Le client spécifie donc la forme de la réponse, et ce point de terminaison unique gère toutes les interactions de données. Il s'agit toujours d'une requête HTTP, mais comme vous pouvez le voir, nous pouvons spécifier les données exactes dont nous avons besoin. Par exemple, nous avons besoin de l'utilisateur avec l'ID 123 et nous avons seulement besoin du nom de l'utilisateur, ainsi que des publications, et des publications, nous pouvons spécifier seulement le titre. Nous n'avons donc pas besoin des images pour cette vue. Et encore une fois, avec les commentaires, vous pouvez spécifier les données exactes dont vous avez besoin au sein de l'objet, de sorte que vous ne faites pas de sur-récupération de données. Voyons maintenant la conception du schéma et le système de types de GraphQL et comment il diffère des API RESTful. Le schéma dans ce cas est un contrat entre le client et le serveur. Dans le schéma, tout d'abord, vous avez des types qui peuvent être, par exemple, un type utilisateur que vous spécifiez, et vous spécifiez tous les champs qui existent sur ce type utilisateur, qui sont id, nom, publications, etc., et comme vous pouvez le voir, si le type n'est pas un type primitif comme les publications, alors vous pouvez spécifier un autre type de tableau de publications, et ensuite ce type de publication peut être défini séparément. Ensuite, nous avons les requêtes pour lire des données. C'est l'équivalent de faire des requêtes GET dans une API RESTful. Vous spécifiez la requête et la fonction de cette requête. Cela peut être la requête utilisateur, qui récupère l'utilisateur avec un ID spécifique, ainsi que le type de retour de cette requête, qui dans ce cas est le type utilisateur que nous avons défini ci-dessus. Et GraphQL propose également des mutations. Vous pouvez penser à cela comme l'équivalent des méthodes POST, PUT, PATCH et DELETE dans les API RESTful. Donc, chaque fois que vous modifiez des données dans la base de données, vous effectuez une requête de mutation. Ici, comme vous pouvez le voir, nous avons un exemple de méthode de création d'utilisateur qui accepte un nom et bien sûr de nombreuses choses dans le monde réel, puis elle renvoie le type utilisateur que nous avons défini ci-dessus. Donc, si vous avez une bonne conception de schéma dans GraphQL, elle devrait refléter votre modèle de domaine et elle devrait être intuitive et flexible. Ensuite, une fois que vous avez défini la conception du schéma et le système de types, vous pouvez commencer à interroger et à modifier des données avec cette API GraphQL. Pour cela, nous avons des requêtes pour récupérer des données. Encore une fois, c'est comme les requêtes GET dans les API RESTful, et ici vous pouvez spécifier exactement ce dont vous avez besoin de l'utilisateur. C'est la même méthode utilisateur que nous avons définie là dans le schéma. Donc, ici, vous pouvez également spécifier les attributs exacts comme le nom, les publications, et des publications, vous avez seulement besoin du titre, et cela fera une requête à votre API GraphQL et renverra les données exactes que vous avez demandées. De même, vous pouvez également utiliser les mutations que vous avez définies. Par exemple, si vous avez une méthode de création de publication définie comme une mutation, vous pouvez l'utiliser pour modifier la publication. Par exemple, en définissant le titre et le corps de la publication, puis vous spécifiez également quelles données vous avez besoin de récupérer après la création de cette publication, qui sont l'ID et le titre. En ce qui concerne la gestion des erreurs dans les API GraphQL, c'est un peu différent que dans les API RESTful. Étant donné que GraphQL renvoie toujours un code d'état 200 OK pour toutes les réponses, même s'il y a eu une erreur. Dans ce cas, nous devons renvoyer un champ "errors" dans la réponse, qui indiquera qu'il y a eu une erreur. Ainsi, des données partielles peuvent toujours être renvoyées avec des erreurs, comme dans ce cas, nous avons l'utilisateur qui est null, puis nous avons le champ "errors" qui indique que vous avez le code d'état 404, le message "non trouvé", et le chemin, qui est l'utilisateur dans votre schéma. Comme vous pouvez le voir dans ce cas, vous pouvez spécifier le code d'état dans le tableau "errors". Étant donné que nous renvoyons des codes d'état 200 pour toutes les requêtes GraphQL, c'est pourquoi nous avons le code d'état spécifiquement mentionné dans les erreurs afin que nous sachions de quel type d'erreur il s'agit, qui est "utilisateur non trouvé". Il existe également des meilleures pratiques que nous suivons normalement lors de la conception d'API GraphQL. Tout d'abord, les schémas que nous avons vus, il est bon de les garder petits et modulaires. De plus, nous devrions éviter les requêtes profondément imbriquées. Par exemple, vous pouvez avoir un utilisateur, puis des publications imbriquées, puis au sein des publications, vous pouvez avoir un commentaire. Cela peut être infiniment imbriqué, et pour éviter cela, nous implémentons généralement des limites de profondeur de requête, qui est jusqu'où vous pouvez aller, combien de couches imbriquées vous pouvez avoir dans vos données. Vous spécifiez donc quelque chose comme six ou sept couches de profondeur. Nous utilisons également des noms significatifs pour les types et les champs afin que cela soit également clair pour le client, car ils utiliseront tous deux le même schéma. Et lors de la modification de données, nous utilisons toujours les types d'entrée pour les mutations. Avant qu'un système ne puisse autoriser ou restreindre quoi que ce soit, il doit d'abord connaître l'identité du demandeur. Qu'il s'agisse d'un utilisateur accédant à notre service via un navigateur ou une application mobile, ou d'un service tiers essayant d'accéder à notre système. C'est ce que fait l'authentification. Elle vérifie que l'utilisateur ou le service qui tente d'accéder à notre système est bien celui qu'il prétend être. Mais c'est là que la plupart des ingénieurs logiciels se confondent ou mélangent les concepts. Ils mélangent les méthodes d'authentification avec les frameworks d'autorisation. Ils traitent JWT comme une méthode d'authentification alors qu'en réalité, ce n'est qu'un format de jeton. Ils confondent également l'authentification par porteur avec JWT. Ils appellent parfois OAuth 2 une méthode d'authentification alors qu'en réalité, c'est un framework d'autorisation, et ils confondent le Single Sign-On avec les méthodes d'authentification alors qu'il s'agit en réalité d'un modèle d'expérience utilisateur. Dans cette vidéo, nous allons corriger tout cela en abordant tout d'abord ce qu'est l'authentification, puis tous les principaux types d'authentification, de l'authentification de base à l'authentification par résumé, en passant par les clés API, les sessions et les cookies, l'authentification par porteur et les jetons JWT. Que sont les jetons d'accès et de rafraîchissement ? Nous aborderons également OAuth 2, OpenID Connect, ainsi que le Single Sign-On et les protocoles d'identité, et nous comprendrons ce que chacun est réellement et où tout cela s'inscrit. Comprenons d'abord ce qu'est l'authentification, puis nous aborderons les différentes méthodes d'authentification. L'authentification répond donc vraiment à une simple question : qui est l'utilisateur qui tente d'accéder à notre système ? Disons que vous avez votre système comme votre passerelle API, la couche d'API, puis votre couche de service et aussi le stockage de données. Avant que quiconque ne puisse faire des requêtes à votre passerelle API et commencer à accéder aux services et aux données, il doit d'abord être authentifié. C'est là qu'il envoie une requête de connexion. Celle-ci provient soit d'un utilisateur, soit d'un autre service. C'est là que nous confirmons son identité, si elle est valide, et que nous lui accordons l'accès à notre système, à notre passerelle API et à tous les autres services, ou si l'identité n'est pas confirmée, nous la rejetons avec une réponse 401 non autorisée. C'est la première étape avant d'entrer dans l'autorisation, qui est ce à quoi ils peuvent accéder et ce qu'ils peuvent faire une fois qu'ils peuvent se connecter à votre système, mais c'est une discussion distincte en soi. Donc, dans celle-ci, nous nous concentrons principalement sur l'authentification et les différentes méthodes d'authentification que nous pouvons utiliser pour vérifier l'identité de l'utilisateur. Voyons maintenant les différentes méthodes d'authentification que nous avons pour vérifier l'identité du demandeur, et commençons par les méthodes d'authentification de base. Ce sont les bases de l'authentification par résumé, des clés API et de l'authentification basée sur les sessions. Commençons par la toute première de la liste, qui est le flux d'authentification de base. C'est la forme d'authentification la plus simple. Disons que vous faites une requête au serveur pour accéder à une ressource comme /API/utilisateurs pour récupérer les données utilisateur. Vous recevrez d'abord une réponse non autorisée car vous n'avez pas fourni les identifiants. Nous demandons donc à l'utilisateur ou au service de fournir les identifiants avant d'accéder à toute ressource sur le serveur. Donc, dans la requête suivante vers la même ressource, ils fournissent également l'en-tête d'autorisation, et cet en-tête contient la version encodée en base 64 du nom d'utilisateur et du mot de passe de cet utilisateur. C'est là que nous le vérifions côté serveur. Si les identifiants sont valides, nous répondons avec un code d'état 200 OK avec les données utilisateur renvoyées dans le corps, ou nous le refusons à nouveau, marquant ces identifiants comme invalides. Le problème avec cette méthode est que la base 64 est facilement réversible. C'est donc une méthode non sécurisée à moins qu'elle ne soit encapsulée avec le protocole HTTPS, et même alors, elle est rarement utilisée de nos jours en production en dehors des outils internes, car vous envoyez les identifiants à chaque requête, et vous envoyez la version encodée en base 64, qui n'est pas si sécurisée. C'est pourquoi nous avons également l'authentification par résumé, qui est légèrement meilleure et utilise le hachage MD5. Cette méthode fonctionne de manière similaire à l'authentification de base. Donc, vous essayez, disons, d'accéder à la même ressource que les utilisateurs. Elle répondra d'abord avec 401 non autorisé, vous demandant d'inclure les identifiants, puis vous ferez la même requête mais avec la réponse hachée, et cela contiendra également la version hachée MD5 au lieu du mot de passe et du nom d'utilisateur en clair. Et même processus que le précédent. Si les identifiants sont invalides, vous recevrez 401 non autorisé. Sinon, vous recevrez la réponse réussie avec les données utilisateur dans le corps de la requête. C'est légèrement mieux que l'authentification de base car elle utilise le hachage MD5, mais elle est toujours obsolète et rarement utilisée aujourd'hui car nous avons de meilleures options, comme vous le verrez bientôt. Et si vous vous demandez comment nous configurons ces options dans l'autorisation, par exemple, si vous faites la requête depuis Postman ou si vous le faites depuis le code, alors vous l'inclurez comme en-tête dans la requête. C'est là que vous pouvez définir le type d'authentification, et vous remarquerez les choses dont nous parlons ici, comme l'authentification de base, qui était la première version, ou l'authentification par résumé, qui est la deuxième version, et vous verrez également les autres méthodes disponibles ici, ainsi que l'option clé API. Et Postman appelle tous ces types d'authentification pour simplifier l'interface, mais c'est aussi l'une des raisons pour lesquelles les développeurs se confondent et pensent que toutes ces méthodes sont des types d'authentification, alors que certaines sont des méthodes d'authentification, certaines sont des frameworks d'autorisation. Ensuite, nous avons l'authentification par clé API. C'est là que vous générez une clé unique pour chaque client. Ensuite, ils l'envoient avec chaque requête pour accéder aux ressources. Donc, pour la même requête, comme nous l'avons discuté, elle arrive d'abord à votre serveur API, et elle inclura soit l'en-tête d'autorisation, soit X-API-Key, et cela inclura la clé API que vous avez générée pour l'utilisateur. Ces clés API sont généralement stockées dans une base de données avec le hachage de la clé et aussi les scopes pour la clé API. Et par exemple, si vous avez déjà essayé d'accéder à des API en générant une clé sur le tableau de bord, puis qu'il vous renvoie la clé que vous pouvez attacher aux requêtes, c'est là que vous avez déjà utilisé la clé API de ce service pour accéder aux données. Donc, si vous avez inclus cette clé dans la requête, le serveur effectuera d'abord une recherche de clé API dans la table des permissions ou des utilisateurs, et s'il est capable de vérifier que la clé API est valide, alors nous autoriserons la requête et enverrons la réponse réussie avec les données dans le corps de la réponse, sinon l'utilisateur recevra une réponse 401 non autorisée, et si la clé est manquante dans l'ensemble, comme l'en-tête d'autorisation ou X-API-Key, alors nous renvoyons simplement une réponse 400 mauvaise requête, car la clé API est requise pour accéder à ce type de système. Un problème avec les clés API est que si la clé est compromise, alors n'importe qui peut l'utiliser et commencer à accéder aux ressources en votre nom avec votre clé API, et il n'y a pas d'expiration intégrée à moins que vous ne l'implémentiez vous-même. Une autre chose est que cela peut sembler similaire aux jetons Web JSON, mais les clés API sont juste des chaînes aléatoires sans informations intégrées. Alors qu'en JWT, nous pouvons également stocker des informations, comme vous le verrez bientôt. Donc, le serveur ici n'a aucun moyen de savoir qui possède la clé ou quelles permissions il a sans la rechercher dans la base de données. Ensuite, nous avons l'approche Web traditionnelle, qui est l'authentification basée sur les sessions. C'est là qu'un utilisateur se connecte avec ses identifiants, puis nous créons une session dans une sorte de stockage de sessions. Ce stockage de sessions peut être aussi simple qu'en mémoire, comme juste une variable. Mais le problème ici est que nous la perdrons une fois que le serveur redémarrera ou plantera. L'autre option est que nous pouvons utiliser des outils comme Redis, qui est l'un des plus courants en production car il est rapide et prend en charge l'expiration des sessions, ou nous pouvons utiliser une base de données dédiée ici, comme une base de données de type SQL. Une autre option très rare est d'utiliser le système de fichiers du serveur que vous utilisez. Le problème avec celui-ci est qu'il n'est pas évolutif et globalement Redis est généralement la solution pour la production car il est rapide et a une expiration de clé intégrée. Donc, avec la première requête, nous récupérons l'ID de session, puis nous définissons le cookie de session côté client. Ensuite, pour toutes les autres requêtes à venir qui contiennent ce cookie, nous recherchons la session dans le stockage de sessions ici. Et ensuite, si la session est valide, nous obtiendrons les données utilisateur et nous les enverrons avec une réponse autorisée. Sinon, si elle n'est pas trouvée, si nous ne trouvons pas la session, alors cet utilisateur n'est pas authentifié. Nous lui envoyons donc une réponse non autorisée. Un défi avec l'authentification basée sur les sessions est qu'elle est "stateful", ce qui signifie que le serveur doit se souvenir des sessions. Nous avons besoin d'une sorte de stockage de sessions ici, et cela fonctionne très bien pour les applications Web traditionnelles, mais ne peut pas évoluer aussi facilement pour les API ou les systèmes distribués. Examinons maintenant l'authentification basée sur les jetons. Nous couvrirons l'authentification par porteur, les jetons JWT, les jetons d'accès et de rafraîchissement, et comment cela se compare à l'authentification basée sur les sessions. Au lieu de sessions, les applications modernes utilisent généralement des jetons. Le client envoie donc un jeton avec chaque requête. Par exemple, nous avons une connexion avec identifiants où l'utilisateur inclura ses identifiants dans l'en-tête d'autorisation, qui inclura le type d'authentification, qui est "bearer", et aussi le jeton, que nous validerons côté serveur. Une chose que les développeurs confondent ici est le jeton porteur et les jetons Web JSON. Un jeton porteur signifie simplement que quiconque possède ce jeton obtient l'accès. C'est donc un modèle, mais pas une méthode spécifique. Et le type de jeton porteur le plus courant est le JWT (JSON Web Token). C'est essentiellement un objet JSON signé qui contient l'ID utilisateur ou l'e-mail pour que nous puissions valider l'utilisateur, ainsi que l'heure d'expiration et d'autres revendications que nous devons stocker, comme les rôles, les permissions, etc. Donc, ce que nous faisons sur le serveur d'authentification est de valider les identifiants une fois que nous recevons cet en-tête d'autorisation, et il est sans état, ce qui signifie que nous n'avons pas besoin d'une base de données ici pour rechercher, et c'est pourquoi il est également évolutif par rapport à l'authentification basée sur les sessions avant que
JWT, disons, révolution. Un jeton n'était qu'une chaîne de caractères sans information, et ce jeton était envoyé, puis recherché dans une sorte de base de données, et ce n'est qu'alors que nous pouvions vérifier que l'utilisateur avait accès. Le revers de la médaille était qu'il était toujours stateful, car nous avions besoin de l'accès à la base de données ou au cache, ce qui était requis chaque fois que le jeton était utilisé avec les jetons Web JSON.
Maintenant, nous pouvons encoder et vérifier en signant leurs propres revendications. Et c'est ce qui nous permet maintenant d'émettre des jetons JWT de courte durée qui sont stateless, c'est-à-dire qu'ils sont autonomes et ne dépendent de personne d'autre. Ils n'ont pas besoin d'accéder à la base de données, ce qui réduit la charge de la base de données et simplifie également le processus d'authentification pour le serveur.
Ainsi, la première fois, nous recevrons les informations d'identification et validerons l'utilisateur, et si elles sont valides, nous générerons le jeton Web JSON et l'enverrons au client. À partir de ce moment, le client peut faire des requêtes et inclure ce jeton porteur, qui est cet en-tête d'autorisation qui contient l'authentification porteuse avec le jeton, et ce jeton est, dans la plupart des cas, un jeton Web JSON. Nous vérifions cette signature localement sans avoir besoin d'accéder à la base de données. Et si le jeton est valide, nous renvoyons les données demandées. Sinon, nous renvoyons une réponse non autorisée.
Les systèmes modernes utilisent également deux types de jetons. L'un d'eux est le jeton d'accès et l'autre est le jeton de rafraîchissement. La raison pour laquelle nous avons besoin de deux jetons ici est que les jetons d'accès sont de courte durée et sont utilisés pour les appels API au serveur, tandis que les jetons de rafraîchissement sont de longue durée et sont utilisés pour obtenir de nouveaux jetons d'accès, essentiellement pour renouveler le jeton d'accès.
Chaque fois qu'un utilisateur envoie une demande de connexion et se connecte, il reçoit ces deux jetons. Nous générons un jeton d'accès valide pendant 15 minutes à 1 heure, et nous générons un jeton de rafraîchissement qui peut durer des jours, voire des semaines. Le client utilisera maintenant le jeton d'accès pour accéder à l'API et effectuer les requêtes, et il stockera également les jetons de rafraîchissement.
Une note importante ici est que nous ne le stockons jamais dans le stockage local, mais nous le stockons dans des cookies HTTP uniquement. Cela nous protège des attaques XSS côté client. Et après cela, l'utilisateur restera connecté sans avoir à ressaisir ses informations d'identification. Si son jeton d'accès expire, il recevra une réponse non autorisée. Et c'est là que nous utiliserons le jeton de rafraîchissement que nous avons stocké pour générer un nouveau jeton d'accès côté serveur. Nous pouvons faire une requête avec ce nouveau jeton. Et cela nous renverra avec succès les données puisque nous avons renouvelé le jeton d'accès.
Ensuite, abordons OAuth2 et OpenID Connect, qui sont des concepts souvent mal compris, et clarifions s'il s'agit de méthodes d'authentification ou de frameworks d'autorisation et comment ils fonctionnent. OAuth2 est l'un des concepts souvent mal compris. C'est un framework d'autorisation et non d'authentification. Il répond donc à la question : qu'est-ce que cette application peut accéder au nom de l'utilisateur ? Par exemple, si vous souhaitez accorder à une application l'accès à votre Google Drive pour pouvoir y lire vos fichiers, vous connecteriez généralement votre Google Drive à cette application externe et vous donneriez à l'application la permission d'accéder à vos données.
La façon dont cela fonctionne est qu'il vous redirigera d'abord vers un écran de consentement de l'authentification Google, et il vous montrera la demande de permission, et si vous autorisez l'accès à cette application pour pouvoir lire les fichiers du lecteur en votre nom. Ensuite, il renverra le code d'autorisation à cette application externe, ou cela peut aussi être votre application. Et la façon dont cela fonctionne ensuite est que vous échangez le code contre un jeton, et vous renvoyez le jeton d'accès de Google OAuth pour pouvoir lire les données.
C'est la partie confuse, car vous recevez un jeton d'accès pour l'API Google Drive, et vous pourriez penser qu'il s'agit d'une méthode d'authentification, mais le jeton d'accès prouve simplement que l'application peut accéder à vos ressources. Mais cela ne dit pas à l'application qui vous êtes. Cela prouve simplement que l'application est autorisée à accéder à certaines ressources de votre Google Drive. Ainsi, après ce point, l'application sera capable de demander des fichiers avec ce jeton et de renvoyer les fichiers de l'utilisateur depuis l'API Google Drive.
Ensuite, nous avons OpenID Connect, qui ajoute l'authentification par-dessus OAuth2. Ainsi, lorsque vous cliquez sur "Se connecter avec Google", disons via votre application, elle vous redirigera vers le point d'accès d'autorisation, et cela vous montrera l'écran de connexion où vous accordez l'accès pour vous connecter à Google via votre application. Si vous entrez vos informations d'identification et consentez, le fournisseur renverra le code d'autorisation, et après cette étape, votre application échangera le code contre des jetons et renverra le jeton d'accès en combinaison avec le jeton d'identité.
À partir de là, le jeton d'accès est pour l'autorisation OAuth2, mais le jeton d'identité est un jeton Web JSON qui contient votre identité, y compris votre e-mail ou nom d'utilisateur, votre ID utilisateur, ce qui signifie qu'après ce point, votre application est capable de vérifier la signature et d'extraire l'identité de l'utilisateur pour envoyer le jeton d'identité à votre backend pour vérification. Et en ayant ce jeton d'identité, votre application peut maintenant créer sa propre session et accorder le jeton d'accès pour cet utilisateur. C'est une solution moderne. Elle est sécurisée et évolue bien. Et c'est aussi pourquoi la plupart des applications utilisent aujourd'hui ce type d'authentification, comme "Se connecter avec Google", GitHub, Microsoft, etc.
Enfin, abordons l'authentification unique (Single Sign-On) et les protocoles d'identité. L'authentification unique est une expérience utilisateur, pas une méthode d'authentification, ce qui signifie que vous pouvez vous connecter une fois mais accéder à plusieurs services. Par exemple, lorsque vous souhaitez vous connecter à Google ou à Okta, disons que vous souhaitez accéder à votre Gmail, à votre Google Drive, à YouTube, à Google Calendar, vous pouvez le faire en vous connectant une fois au fournisseur d'identité. Disons que cela peut être Google dans ce cas. Si vous souhaitez accéder à ces services, l'authentification unique utilise des protocoles d'identité en arrière-plan pour valider ces sessions.
Ainsi, une fois que vous vous connectez avec le fournisseur d'identité, disons que c'est Google dans ce cas, votre session globale est stockée dans un stockage de session, puis vous recevez un cookie d'authentification unique sur votre client pour pouvoir accéder à d'autres ressources. Ainsi, disons que vous souhaitez accéder à Gmail pour la première fois, une fois que vous vous connectez, vous vérifiez également la session et maintenant vous êtes capable d'accéder à Gmail. Et pour la prochaine requête, si vous avez besoin d'accéder à Google Drive, vous n'avez pas besoin de vous reconnecter car vous avez ce cookie et la session stockée dans le stockage de session. Nous vérifions donc simplement votre session et si elle est valide, vous obtenez également l'accès à Google Drive, et de même à YouTube, à Google Calendar et à d'autres services comme je l'ai mentionné.
L'authentification unique utilise des protocoles d'identité en arrière-plan, et ces protocoles sont SAML (Security Assertion Markup Language) ou OpenID Connect. Les deux sont des protocoles d'identité utilisés en combinaison avec l'authentification unique. Dans le cas de SAML, pour pouvoir accéder à l'application, vous êtes redirigé pour vous connecter, et c'est là que nous utilisons SAML pour l'authentification. C'est une solution courante dans les systèmes d'entreprise et hérités comme Salesforce, les tableaux de bord d'entreprise, etc. C'est un protocole basé sur XML. Ainsi, une fois que vous souhaitez vous connecter, vous êtes redirigé pour vous connecter, puis vous recevez l'assertion SAML au format XML, et après cela, votre identité est confirmée pour l'utilisateur, et maintenant vous êtes capable d'accéder à l'application tierce. SAML est toujours largement utilisé, mais c'est une version plus ancienne par rapport à OpenID Connect.
La prochaine option est OpenID Connect en tant que protocole d'identité. Disons que vous souhaitez accéder à une application, et dans ce cas, c'est Gmail. Vous serez redirigé pour vous connecter afin de fournir vos informations d'identification. Et une fois que vous fournissez vos informations d'identification, l'utilisateur est authentifié. Et maintenant, vous recevez le jeton d'identité au format JSON Web Token. Et c'est ce que vous utiliserez pour confirmer votre identité auprès de Gmail. C'est, par exemple, ce que Google utilise en coulisses. Et c'est une approche plus moderne par rapport à SAML, mais les deux sont toujours très sécurisées et pertinentes.
Ce sont les types d'authentification les plus courants, et ce n'est que la première étape pour accéder à notre système. Une fois que vous savez qui est l'utilisateur grâce à l'authentification, vous devez également savoir ce qu'il peut faire et quelles permissions il a. L'authentification n'est que la première étape avant que les utilisateurs puissent accéder à votre service. Cela vous indique donc qui est l'utilisateur et s'il est autorisé à accéder à votre service. C'est à ce moment-là qu'ils envoient une demande de connexion et que vous confirmez ou refusez leur identité. Mais après cela, vous avez également l'étape d'autorisation, qui vous indique quelles ressources exactement cet utilisateur peut accéder. Essentiellement, cela vous dit ce qu'il peut faire, ce que l'utilisateur peut faire dans votre système. Et c'est ce que nous couvrirons ensuite dans la prochaine vidéo.
L'autorisation est l'étape qui se produit après l'authentification. Une fois que quelqu'un se connecte à notre système. Ainsi, une fois que sa demande de connexion est approuvée, ce qui signifie que le système sait maintenant qui est l'utilisateur. La prochaine étape consiste à décider de ce qu'il peut faire, ce qui est l'étape d'autorisation. Il faut vérifier quelles ressources ou actions l'utilisateur a la permission d'accéder, et aussi quelles sont les actions refusées pour cet utilisateur. C'est ainsi que nous contrôlons la sécurité et la confidentialité dans les systèmes. Et dans cette vidéo, vous apprendrez comment les applications et les systèmes gèrent les permissions en utilisant les trois principaux modèles d'autorisation. Le premier est le contrôle d'accès basé sur les rôles (Role-Based Access Control). Ensuite, nous avons le contrôle d'accès basé sur les attributs (Attribute-Based Access Control). Également les listes de contrôle d'accès (Access Control Lists), qui est une autre façon de gérer l'autorisation. De plus, vous apprendrez comment des technologies comme OAuth2 et les JWT nous aident à appliquer ces règles en pratique.
Ainsi, l'authentification se produit d'abord, ce qui nous indique qui est l'utilisateur et s'il est autorisé à accéder à notre système. Mais à l'étape suivante, nous avons l'autorisation, qui détermine ce que vous pouvez réellement faire en tant qu'utilisateur dans ce système. Si nous prenons GitHub comme exemple et que nous accédons aux dépôts sur GitHub, vous avez différentes permissions pour différents utilisateurs. Par exemple, l'utilisateur A peut avoir un accès en écriture uniquement, ce qui signifie qu'il ne peut que pousser du code vers ce dépôt. Mais d'un autre côté, nous pouvons avoir l'utilisateur B, et ici vous pouvez accorder un accès en lecture uniquement, ce qui signifie qu'il ne peut que lire ce dépôt, mais il ne peut pas y pousser de code, ni créer de requêtes de tirage, etc. Et de l'autre côté, nous pouvons également avoir des utilisateurs administrateurs qui ont un contrôle total. Ils peuvent donc gérer tous les paramètres du dépôt. Ils peuvent même décider de supprimer ce dépôt, etc. Vous pouvez donc voir que différents utilisateurs peuvent avoir différents contrôles d'accès sur les systèmes.
Pour gérer ces contrôles d'accès, nous avons des modèles d'autorisation courants. Ainsi, celui que nous venons de regarder est le modèle d'authentification basé sur les rôles, qui attribue des rôles aux utilisateurs, quelque chose comme administrateur, éditeur ou accès en lecture seule, accès en écriture uniquement. Et c'est l'approche la plus courante parmi ces modèles d'autorisation. Mais nous avons également le contrôle d'accès basé sur les attributs, qui est basé sur les attributs de l'utilisateur ou de la ressource. C'est donc plus flexible et plus complexe par rapport à l'authentification basée sur les rôles. Et l'autre approche courante est d'avoir des listes de contrôle d'accès (ACL), et chaque ressource ici a sa propre liste de permissions. Vous pouvez donc attribuer des listes de permissions à une ressource, et c'est ce qui déterminera les ressources auxquelles vous pouvez accéder. Par exemple, c'est une façon courante de gérer Google Docs, et nous y regarderons plus en détail maintenant. Et chacun de ces modèles a ses compromis, ses avantages et ses inconvénients. Cela dépend donc des exigences spécifiques du système. Mais les systèmes réels combinent souvent plusieurs modèles ensemble pour avoir une configuration plus complexe et plus sécurisée.
Tout d'abord, nous avons le contrôle d'accès basé sur les rôles, ou RBAC comme acronyme. Ici, les utilisateurs sont affectés à des rôles, et chaque rôle a un ensemble défini de permissions. Par exemple, comme vous l'avez vu avec GitHub, vous pouvez avoir des administrateurs, et les administrateurs ont généralement un accès complet à toutes les ressources. Ils peuvent donc créer, lire ou mettre à jour des ressources. Ils peuvent même supprimer des ressources et gérer d'autres utilisateurs dans les rôles. Ensuite, vous avez l'éditeur, qui est généralement un peu moins qu'un administrateur. Ils peuvent donc modifier du contenu, comme créer ou lire du contenu ou mettre à jour des ressources, mais ils ne peuvent pas supprimer de ressources et ils ne peuvent pas non plus gérer d'autres utilisateurs. Ensuite, vous pouvez avoir des utilisateurs spectateurs qui ne peuvent que lire des données. Ils peuvent donc lire les ressources et le contenu, mais ils ne peuvent rien mettre à jour ni créer quoi que ce soit dans votre système. C'est la manière la plus courante dans les modèles d'autorisation, et elle est utilisée dans les applications que vous utilisez quotidiennement, comme vous l'avez vu avec GitHub, les tableaux de bord d'entreprise ou les outils CMS, les outils de gestion d'équipe, etc.
Le modèle suivant est le contrôle d'accès basé sur les attributs, ou ABAC en abrégé. Ce contrôle d'accès va au-delà des rôles. Il utilise les attributs de l'utilisateur ou les attributs de la ressource et les conditions environnementales pour définir l'accès. Vous pouvez voir un exemple de politique ici. Disons que vous souhaitez autoriser l'accès uniquement si certaines conditions sont remplies. Dans ce cas, chaque fois que le département de l'utilisateur est défini sur RH, et vous pouvez combiner cela avec plusieurs conditions, comme chaque fois que l'attribut de la ressource est égal à "interne", et ainsi de suite, et seulement dans ce cas, vous leur autorisez l'accès, et vous autorisez soit l'accès en lecture, soit l'accès en écriture.
Cela peut également être combiné avec l'autorisation basée sur les rôles. Mais dans ce cas, vous vérifiez le modèle utilisateur ou le modèle de ressource dans votre base de données, et en fonction des attributs, vous autorisez ou refusez l'accès. Ainsi, comme vous pouvez le voir ici, nous vérifions les attributs de l'utilisateur comme le département, l'âge ou tout ce que vous voulez vérifier ici. Ensuite, vous pouvez également le combiner avec des attributs de ressources comme la confidentialité, le propriétaire de la ressource ou la classification. Et cela peut également être combiné avec l'environnement, comme l'heure de la journée, la localisation, le type d'appareil, etc.
Étant donné que vous combinez ces attributs pour accorder ou restreindre l'accès, c'est plus flexible que l'autorisation basée sur les rôles, mais cela nécessite une bonne gestion des politiques et est généralement plus complexe, et vous pouvez y rencontrer des conflits avec le contrôle d'accès basé sur les attributs. Le troisième type courant est les listes de contrôle d'accès. Au lieu de fournir un accès basé sur les rôles ou basé sur les attributs, vous pouvez avoir une liste de contrôle d'accès pour la ressource spécifique. Disons que vous avez une ressource comme un document ou un fichier JSON. Et ici, vous pouvez avoir une liste de permissions sur quels utilisateurs peuvent accéder à ce document, comme l'utilisateur Alice n'a qu'un accès en lecture, ou l'utilisateur Bob a un accès en lecture et en écriture, et un autre utilisateur n'a aucun accès à ce document.
Ainsi, comme vous pouvez le voir, nous gérons deux choses ici. Premièrement, quels utilisateurs sont autorisés à accéder à ce document, et deuxièmement, quelles sont leurs permissions ? Ainsi, chacun des utilisateurs a des permissions différentes sur ce document. Les ACL sont très spécifiques et centrées sur l'utilisateur, ce qui signifie qu'il est difficile de les faire évoluer dans des systèmes avec des millions d'utilisateurs ou d'objets, à moins de les gérer avec soin. Mais par exemple, Google Drive en est un exemple, où vous avez des documents comme un Google Doc, puis vous partagez ce Google Doc avec vos collègues, n'est-ce pas ? Vous partagez avec quelqu'un en accès en lecture uniquement, puis vous partagez ce document avec quelqu'un d'autre, mais maintenant ils peuvent également modifier et ajouter des commentaires à ce document. C'est donc un exemple d'ACL, liste de contrôle d'accès, qui est utilisée dans Google Drive et Google Docs. Cela vous donne plus de contrôle sur les ressources et les documents, mais il est également plus difficile de le faire évoluer avec des millions d'utilisateurs, mais c'est possible, comme vous pouvez le voir, car Google Drive l'utilise pour ses documents, ses feuilles Excel, etc.
Ce sont donc les modèles de contrôle d'accès. Mais comment les systèmes appliquent-ils ces autorisations ? C'est là qu'interviennent OAuth2 et les JWT, ou jetons d'accès. Nous avons donc d'abord OAuth2, qui est une autorisation déléguée, un protocole utilisé lorsqu'un service souhaite accéder aux ressources d'un autre service au nom d'un utilisateur. Par exemple, si vous souhaitez permettre à une application tierce de lire vos dépôts GitHub. Disons que vous déployez votre application sur Vercel. Vous devez donc donner à Vercel le contrôle de votre dépôt sur GitHub. Au lieu de donner votre nom d'utilisateur et votre mot de passe à l'application tierce, ce qui ne serait pas du tout sécurisé car vous ne savez pas ce qu'ils peuvent faire avec votre nom d'utilisateur et votre mot de passe. De cette façon, vous leur donnez un contrôle total. Au lieu de cela, GitHub leur donne le jeton qui représente les permissions que vous avez approuvé d'utiliser.
Ainsi, en tant qu'utilisateur, vous envoyez une demande avec l'application tierce pour demander l'accès à vos dépôts, puis GitHub vous donne le jeton d'accès que vous devriez créer. Vous devriez également spécifier quelles ressources, quels dépôts cette application tierce peut accéder, et aussi ce qu'elle peut faire. Peut-elle créer, lire, mettre à jour ou peut-elle supprimer, ou quelles que soient les permissions que vous définissez, puis GitHub leur envoie le jeton qui contient les permissions que cette application tierce est autorisée à utiliser, et OAuth2 définit le flux pour émettre et valider ces jetons en toute sécurité. Vous leur donnez donc le jeton d'accès, et non votre mot de passe, qui représente les permissions que vous avez personnellement approuvées. Il peut s'agir de lire des dépôts spécifiques, ou de créer et de pousser vers ces dépôts, mais pas de supprimer ces dépôts.
Ensuite, nous avons également l'autorisation basée sur les jetons à l'aide de JWT ou de jetons porteurs et de la logique de permission. Une fois qu'un utilisateur est authentifié, la plupart des systèmes utilisent un jeton, généralement un jeton JWT, ou cela peut aussi être un jeton porteur qui contient des informations comme l'ID utilisateur, les rôles comme administrateur ou éditeur, et aussi les scopes, c'est-à-dire ce à quoi ils sont autorisés à accéder, et quand ce jeton expire, et qui est l'émetteur de ce jeton. Ainsi, chaque fois qu'un utilisateur effectue une requête, il transporte toujours ces informations de jeton et atteint le serveur backend. C'est là que le serveur vérifiera votre jeton et sa validité, et appliquera la logique de permission appropriée.
Pour ne pas confondre cela avec les modèles d'autorisation, il y a une distinction clé. Le jeton transporte généralement l'identité et les revendications de votre utilisateur, comme vous le voyez ici. Mais les modèles d'autorisation comme basé sur les rôles ou basé sur les attributs, c'est ce qui définit ce qui est autorisé à accéder en tant qu'utilisateur. Les jetons ne sont donc que des mécanismes, tandis que ce sont des modèles d'autorisation.
En résumé, l'autorisation ne consiste pas seulement à laisser entrer les utilisateurs comme l'authentification, mais elle contrôle également ce qu'ils peuvent accéder une fois qu'ils sont à l'intérieur. Nous avons appris ce qu'est l'autorisation, quels sont les trois modèles d'autorisation les plus courants, à savoir basé sur les rôles, basé sur les attributs et listes de contrôle d'accès. Et vous avez également vu quelques exemples concrets, comme la façon dont GitHub gère vos jetons d'autorisation. Et cela devrait vous donner une idée de quand utiliser chaque modèle en fonction du système que vous construisez. Et vous avez également vu quelques modèles d'implémentation avec OAuth2 ou les jetons JWT. Chacun de ces modèles a ses propres compromis, ses propres avantages et inconvénients, et les systèmes réels combinent souvent plusieurs modèles pour rester flexibles et sécurisés.
Les API sont comme des portes vers votre système. Si vous les laissez sans protection, les attaquants et n'importe qui peuvent entrer et faire ce qu'ils veulent de vos données utilisateur et de votre système en général. C'est pourquoi, dans la vidéo d'aujourd'hui, nous examinerons sept techniques éprouvées qui vous aideront à protéger vos API contre les attaques indésirables.
La première sur la liste est la limitation de débit (rate limiting), qui contrôle le nombre de requêtes qu'un client peut effectuer dans un laps de temps donné. Par exemple, vous pouvez définir une limite pour que l'utilisateur A effectue, disons, 100 requêtes par période de temps à votre API. Et s'il dépasse cette limite et effectue, disons, 101 requêtes, alors vous bloquez la prochaine requête et laissez passer un certain temps avant qu'il ne puisse envoyer sa prochaine requête. Si vous ne définissez pas cela pour votre API, les attaquants peuvent submerger votre système. Ils peuvent envoyer des milliers de requêtes par minute et ainsi submerger votre API, ce qui fera tomber votre système, ou ils peuvent également forcer vos données par force brute.
Et ces limites de débit peuvent être définies par point de terminaison. Par exemple, disons que vous avez un point de terminaison /comments, et ici ils peuvent envoyer une requête pour créer un commentaire ou récupérer des commentaires. Vous pouvez définir cette limite au niveau du point de terminaison. Ainsi, ce point de terminaison de commentaires sera défini sur un nombre strict de requêtes par minute. Vous pouvez également le définir par utilisateur ou par adresse IP. Disons que dans A, nous avons l'adresse IP du premier utilisateur, puis B pour le second, C pour celui-ci, et votre attaquant a une adresse IP qui correspond à D. Si vous recevez la 101ème requête de l'adresse IP D, alors vous saurez que cet utilisateur a surutilisé l'API. Vous la bloquerez donc au niveau de l'IP de l'utilisateur.
Et il y a aussi une limitation de débit globale pour se protéger contre les attaques DDoS. Comme vous pouvez définir la limite de débit pour qu'elle fonctionne par utilisateur ou par adresse IP, cela signifie que cet attaquant seul ne peut pas envoyer autant de requêtes. Vous la bloquerez avec votre limitation de débit dans l'API. Mais ce qu'ils peuvent faire, c'est lancer des bots, et chaque bot aura sa propre limite, n'est-ce pas ? Disons que vous l'avez définie à 100 par adresse IP. Donc, chacun de ces bots a 100, et globalement, ils ont plus que ce que vous autoriseriez ou que votre système pourrait gérer. C'est pourquoi vous avez également des limitations de débit globales, qui peuvent être un nombre plus important. Ainsi, chaque fois que tout le trafic arrivant sur votre serveur atteint ou dépasse ce nombre, vous bloquerez temporairement toutes les requêtes jusqu'à ce que vous trouviez la cause première. Et bien sûr, ces nombres ne sont que des exemples. Donc, en réalité, c'est beaucoup plus que mille, mais c'est juste un exemple.
Le deuxième point sur la liste est CORS, qui signifie Cross-Origin Resource Sharing. Cela contrôle quel domaine peut appeler votre API depuis un navigateur, et sans CORS approprié, des sites Web malveillants pourraient tromper les navigateurs des utilisateurs pour qu'ils effectuent des requêtes en leur nom. Par exemple, si votre API est uniquement destinée à servir votre application front-end qui se trouve à app.youdomain.com, alors seules les requêtes provenant de cette source devraient être autorisées. Si quelqu'un d'autre vous envoie une requête, comme app.anotherdomain.com, alors vous devriez bloquer cette requête et ne pas leur permettre d'utiliser votre API pour l'authentification ou l'utilisation de ses données.
Le troisième point est également courant : les injections SQL et NoSQL. Les attaques par injection peuvent se produire lorsque l'entrée de l'utilisateur est directement incluse dans la requête de base de données. Par exemple, un attaquant peut la modifier et envoyer des requêtes pour lire ou supprimer vos données. Ici, par exemple, cette partie contourne complètement les vérifications, et l'attaquant peut utiliser cette requête pour commencer à lire des données de votre base de données, ou modifier quoi que ce soit, ou il peut également supprimer toutes les données, toutes les données utilisateur et toutes les autres tables que vous avez dans cette base de données. Pour corriger cela, nous utilisons toujours des requêtes paramétrées ou des protections ORM.
La prochaine technique à utiliser est les pare-feu. Un pare-feu agit comme un gardien, filtrant le trafic malveillant du trafic normal. Vous l'avez donc généralement entre votre API et le trafic entrant. Par exemple, si vous utilisez le pare-feu d'applications Web d'AWS, ceux-ci peuvent bloquer les requêtes avec des modèles d'attaque inconnus, tels que des mots-clés SQL suspects ou des méthodes HTTP étranges, ce qui signifie qu'ils bloqueront toutes les requêtes suspectes des attaquants, mais permettront aux autres de contourner la requête et d'atteindre votre API.
Certaines API sont également privées et ne devraient être accessibles que depuis des réseaux spécifiques. C'est pourquoi nous avons également les VPN, qui signifient Virtual Private Networks. Les API qui se trouvent dans le réseau VPN ne peuvent être accessibles que par quelqu'un qui se trouve également dans le même réseau. Cela signifie que certaines API sont publiques, ce qui signifie que ces API autoriseront toutes les requêtes provenant d'Internet de vos utilisateurs. Mais cela, par exemple, peut se trouver dans le réseau VPN. Cela signifie que si un utilisateur du Web essaie d'atteindre votre API, cette requête sera bloquée car l'utilisateur ne se trouve pas dans le même réseau. Mais d'un autre côté, si vous avez un autre utilisateur ici qui se trouve dans le réseau VPN, il peut faire une requête à ces API, et dans ce cas, il contournera les vérifications et sa requête atteindra vos API. Ceci est utile lorsque vous avez des outils internes. Disons que vous avez un tableau de bord d'administration interne, et l'API pour ce panneau d'administration ne sera accessible que par les employés connectés au VPN de l'entreprise.
Ensuite, nous avons le CSRF, qui signifie Cross-Site Request Forgery. Cela trompe le navigateur d'un utilisateur connecté pour qu'il effectue des requêtes indésirables à l'API. Disons que vous, en tant qu'utilisateur, êtes connecté à votre système bancaire, et que votre système bancaire utilise des cookies pour l'authentification. Si le système bancaire n'est pas sécurisé et qu'il n'utilise que des cookies de session, un autre site malveillant pourrait utiliser votre cookie et soumettre une requête cachée de transfert d'argent via votre cookie. Pour prévenir de telles attaques, les entreprises utilisent également des jetons CSRF en combinaison avec des cookies de session. Ainsi, le système bancaire vérifiera si le cookie de session est présent, mais il vérifiera également si le jeton CSRF correspond à celui qu'ils ont. Et si ce n'est pas le cas, il bloquera cette requête provenant d'une autre source inconnue, tout en autorisant les requêtes en votre nom.
Et le dernier que nous avons est le XSS, ou il est également appelé Cross-Site Scripting. Cela permet aux attaquants d'injecter des scripts dans les pages Web servies à d'autres utilisateurs. Par exemple, si vous avez une section de commentaires et que ce commentaire est soumis à votre API. Ensuite, votre API le stockera également dans une base de données. Vous pouvez recevoir des requêtes normales comme "belle photo" ou quelque chose comme ça, et cela atteindra votre API. Votre API le stockera dans la base de données. Tout va donc bien. Mais que se passe-t-il si un attaquant place un script dans cette section de commentaires, et dans ce script, il peut essayer de faire beaucoup de choses différentes. Par exemple, il peut essayer de récupérer le cookie d'un autre utilisateur, ou il peut essayer d'injecter quelque chose dans votre base de données. Et si vous autorisez cela, cela atteindra votre serveur et les informations seront écrites dans la base de données. Plus tard, lorsque d'autres utilisateurs chargeront cette section de commentaires sur leur écran, ils recevront également le commentaire injecté directement sur leur page Web, et le navigateur exécutera ce code JavaScript malveillant dans le navigateur des autres utilisateurs.
Ce que vous venez de regarder sont les deux premières parties de mon cours "System Design Mastery". J'ai également des analyses approfondies sur les bases de données, la mise en cache, les CDN et l'infrastructure de production sur ma chaîne YouTube. Recherchez simplement Hikimmon sur YouTube ou consultez le premier lien dans la description. Je couvre également des études de cas complètes où je prends des systèmes réels comme WhatsApp, Spotify, Tiny URL, et plus encore, et je les conçois à partir de zéro en utilisant les composants exacts que vous avez appris ici. Et si vous souhaitez également accéder au cours complet "System Design Mastery" où j'approfondis tous ces piliers, vous trouverez plus d'informations à ce sujet sur ma chaîne.