Accès depuis le navigateur et CORS
- Leçon 9
- 40 min
- Module 3 : Concevoir un contrat utilisable
Leçon 9 sur 21 · Créer et déployer votre première APIAccès depuis le navigateur et CORS
1. Comprendre la requête Web
2. Créer un petit service FastAPI
3. Concevoir un contrat utilisable
4. Documenter l'API avec OpenAPI et Swagger UI
5. Explorer et tester avec Postman
6. Tester et renforcer le service
7. Déployer et vérifier
Ce que vous saurez faire
Permettez à une page Web à une autre adresse d’appeler votre API depuis un navigateur, comme celle d’une équipe d’interface utilisateur. Autorisez seulement les pages que vous choisissez. Sachez exactement ce que ce réglage protège, et ce qu’il ne protège pas.
Avant de commencerCe qu’il faut savoir et avoir sous la main
Ce que vous devez savoir
- Les outils de développement du navigateur, présentés dans Examiner une requête : les outils de développement du navigateur.
- Le schéma, l’hôte et le port d’une URL, présentés dans Du navigateur au serveur : les bases : les parties d’une URL.
- Les en-têtes de requête et de réponse, présentés dans Les bases de HTTP : en-têtes.
Ce qu’il vous faut sur votre ordinateur
- L’API avec une structure commune à toutes les erreurs, de Validation et réponses d’erreur : exercice guidé : une seule structure. Si vous avez ajouté
GET /v1/categories/{name}dans la tâche « À vous de jouer », supprimez-la, comme demandé. - Le serveur de développement en marche, comme dans Environnement et premier point de terminaison : étape 3 : démarrer le serveur. Dans cette leçon, vous l’arrêtez et le redémarrez avec un réglage.
- Un navigateur de bureau, comme dans Examiner une requête. Les exemples utilisent Chrome.
- Python dans un deuxième terminal, pour servir la page de test avec
python -m http.server. - Si votre projet manque ou ne fonctionne plus, copiez l’instantané
end-of-m02, le projet à la fin du module 2. Suivez Utiliser un instantané. Refaites ensuite les exercices guidés de Conception des ressources et des points de terminaison et de Validation et réponses d’erreur.
Testé avec Python 3.14.6, FastAPI 0.142.2 et Chrome 141 sur macOS.
La règle de même origine
Section intitulée « La règle de même origine »Une origine comprend le schéma, l’hôte et le port d’une page. http://localhost:5500 et http://127.0.0.1:8000 sont deux origines différentes, même si elles sont toutes deux sur votre ordinateur.
Les navigateurs ont une protection appelée règle de même origine. Le code d’une page peut lire seulement les réponses de sa propre origine. Sans cette règle, toute page Web ouverte pourrait utiliser votre navigateur pour lire vos courriels ou votre compte bancaire dans un autre onglet. C’est parce que le navigateur envoie vos témoins avec chaque requête.
Votre API a une origine différente de celle de la page créée par une équipe d’interface utilisateur. Par défaut, le navigateur ne permet donc pas à cette page de lire les réponses de votre API.
CORS (partage des ressources entre origines) permet au serveur de dire au navigateur : « les pages de ces origines peuvent lire mes réponses ». Le serveur l’indique avec des en-têtes de réponse, comme Access-Control-Allow-Origin.
Observer le blocage
Section intitulée « Observer le blocage »Amira crée une petite page de test dans un dossier browser-test de son projet. Elle envoie une requête à l’API avec fetch, la fonction de requête du navigateur :
<!doctype html><html lang="en"><head><meta charset="utf-8"><title>Ticket classifier test</title></head><body> <h1>Ticket classifier test</h1> <button id="send">Classify "Charged twice for March"</button> <pre id="result">No request yet.</pre> <script> document.getElementById("send").addEventListener("click", async () => { const result = document.getElementById("result"); try { const response = await fetch("http://127.0.0.1:8000/v1/classify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ subject: "Charged twice for March" }), }); result.textContent = response.status + " " + (await response.text()); } catch (error) { result.textContent = "The request failed: " + error; } }); </script></body></html>Dans un nouveau terminal, elle sert la page depuis une deuxième origine avec le serveur Web intégré de Python :
python -m http.server 5500Elle ouvre http://localhost:5500 et sélectionne le bouton. La page affiche The request failed: TypeError: Failed to fetch (la requête a échoué : erreur de type, récupération impossible). L’onglet Console (console) des outils de développement explique pourquoi :
Access to fetch at 'http://127.0.0.1:8000/v1/classify' from origin 'http://localhost:5500' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.Le terminal de l’API affiche quelque chose de surprenant :
INFO: 127.0.0.1:59691 - "OPTIONS /v1/classify HTTP/1.1" 405 Method Not AllowedLe navigateur n’a pas du tout envoyé la requête POST. Il a d’abord envoyé une requête OPTIONS.
La requête préliminaire
Section intitulée « La requête préliminaire »Pour une requête qui pourrait modifier des données, comme un POST avec du JSON, le navigateur demande d’abord l’autorisation au serveur. Cette première requête est la requête préliminaire. C’est une requête OPTIONS avec l’origine de la page dans un en-tête Origin. Le navigateur envoie la vraie requête seulement si le serveur répond avec les bons en-têtes Access-Control-Allow-....
Description textuelle du diagramme
La page appelle fetch avec un POST. Le navigateur envoie d’abord une requête préliminaire à l’API : OPTIONS /v1/classify, avec Origin: http://localhost:5500. Si cette origine figure dans la liste autorisée de l’API, celle-ci répond 200 avec Access-Control-Allow-Origin: http://localhost:5500. Le navigateur envoie ensuite le vrai POST, reçoit 200 et le résultat JSON, puis le transmet à la page. Si l’origine n’est pas autorisée, la réponse de l’API n’a pas d’en-tête Access-Control-Allow-Origin. Le navigateur n’envoie alors pas la vraie requête, et la page reçoit seulement TypeError: Failed to fetch (erreur de type, récupération impossible).
Exemple détaillé : autoriser une origine
Section intitulée « Exemple détaillé : autoriser une origine »FastAPI fournit un intergiciel prêt à l’emploi pour CORS. Un intergiciel est du code qui s’exécute autour de chaque requête : avant la route, puis après. L’intergiciel CORS répond aux requêtes préliminaires et ajoute les en-têtes aux vraies réponses.
La liste des origines autorisées diffère selon l’endroit où l’API s’exécute : http://localhost:5500 sur l’ordinateur d’Amira, l’adresse réelle de l’interface utilisateur en production. C’est donc de la configuration. Elle vient d’une variable d’environnement, comme dans Configuration et secrets :
import os
from fastapi import FastAPIfrom fastapi.middleware.cors import CORSMiddleware
from ticket_api.classifier import KeywordClassifierfrom ticket_api.errors import add_error_handlersfrom ticket_api.routes import router
app = FastAPI(title="Ticket Classifier API")app.state.classifier = KeywordClassifier()add_error_handlers(app)
allowed_origins = [ origin.strip() for origin in os.environ.get("ALLOWED_ORIGINS", "").split(",") if origin.strip()]if allowed_origins: app.add_middleware( CORSMiddleware, allow_origins=allowed_origins, allow_methods=["GET", "POST"], allow_headers=["Content-Type"], )app.include_router(router)
@app.get("/health")def health() -> dict[str, str]: return {"status": "ok"}| Partie | Pourquoi |
|---|---|
ALLOWED_ORIGINS |
Une liste séparée par des virgules, par exemple http://localhost:5500,https://help.example.com. Sans valeur, aucune page de navigateur ne peut appeler l’API. C’est le réglage sûr par défaut. |
allow_origins |
Les origines exactes, avec le schéma et le port. Jamais ["*"] (toutes les origines) pour une API avec des clés ou des données privées. |
allow_methods, allow_headers |
Seulement ce dont les pages ont besoin. |
Elle arrête l’API avec Ctrl+C, puis la redémarre avec la variable :
ALLOWED_ORIGINS=http://localhost:5500 fastapi dev ticket_api/main.py$env:ALLOWED_ORIGINS = "http://localhost:5500"fastapi dev ticket_api/main.pyALLOWED_ORIGINS=http://localhost:5500 fastapi dev ticket_api/main.pyElle sélectionne encore le bouton. La page affiche 200 {"category":"billing","priority":1,...}, et le terminal de l’API affiche les deux requêtes :
INFO: 127.0.0.1:59702 - "OPTIONS /v1/classify HTTP/1.1" 200 OKINFO: 127.0.0.1:59703 - "POST /v1/classify HTTP/1.1" 200 OKCORS n’est pas de l’authentification
Section intitulée « CORS n’est pas de l’authentification »Amira se fait maintenant passer pour une page d’une autre origine, avec curl. D’abord, la requête préliminaire :
curl -i -X OPTIONS http://127.0.0.1:8000/v1/classify -H "Origin: https://evil.example" -H "Access-Control-Request-Method: POST"HTTP/1.1 400 Bad Request
Disallowed CORS originRefusée, comme prévu. Ensuite, la vraie requête, depuis la même « origine » :
curl -X POST http://127.0.0.1:8000/v1/classify -H "Origin: https://evil.example" -H "Content-Type: application/json" --data @requests/billing.json{"category":"billing","priority":1,"confidence":0.9,"model_version":"keywords-1.0"}Ça fonctionne. curl n’est pas un navigateur : il n’envoie pas de requête préliminaire et ne vérifie pas les en-têtes CORS. Postman, un script Python ou un serveur ne le font pas non plus. CORS contrôle seulement ce que les pages d’un navigateur peuvent lire. Il n’empêche personne d’appeler l’API. Pour contrôler qui peut appeler l’API, il faut de l’authentification, comme la clé d’API que vous ajoutez au module 6.
Exercice guidé : autoriser la page de test
Section intitulée « Exercice guidé : autoriser la page de test »- Créez
browser-test/index.htmlavec le code ci-dessus. - Dans un nouveau terminal, allez dans
browser-testet exécutezpython -m http.server 5500. - Ouvrez
http://localhost:5500, ouvrez la Console des outils de développement et sélectionnez le bouton. Vous voyez l’erreur CORS, et le terminal de l’API afficheOPTIONS ... 405. - Ajoutez le code CORS à
main.py. Arrêtez l’API, puis redémarrez-la avecALLOWED_ORIGINS=http://localhost:5500. - Sélectionnez encore le bouton. La page affiche
200et le résultat. - Envoyez les deux requêtes curl avec
Origin: https://evil.example. La requête préliminaire est refusée, et la vraie requête fonctionne. - Arrêtez le serveur de la page de test avec Ctrl+C.
À vous de jouer : une deuxième origine
Section intitulée « À vous de jouer : une deuxième origine »Servez aussi la même page sur le port 5600 (python -m http.server 5600 dans un troisième terminal), puis ouvrez http://localhost:5600. Prédisez ce qui se passe quand vous sélectionnez le bouton. Modifiez ensuite ALLOWED_ORIGINS pour que les deux pages de test fonctionnent, et aucune autre.
Afficher la réponse
Sur le port 5600, la requête est bloquée : http://localhost:5600 est une autre origine, absente de la liste. Démarrez l’API avec ALLOWED_ORIGINS=http://localhost:5500,http://localhost:5600. Notez que http://127.0.0.1:5600 reste bloquée. Pour CORS, localhost et 127.0.0.1 sont des hôtes différents, donc des origines différentes.
Erreur courante
Autoriser toutes les origines pour faire disparaître l'erreur
Symptôme : une erreur CORS bloque une page, et une recherche rapide suggère allow_origins=["*"]. L’erreur disparaît.
Cause : * permet à une page de n’importe quel site Web de lire les réponses de l’API dans le navigateur d’un visiteur. Pour une API entièrement publique, sans clés ni données privées, cela peut être acceptable. Certaines API sont appelées par un navigateur avec des identifiants, ou renvoient des données sur un utilisateur. Pour ces API, ce réglage permet à n’importe quel site Web de lire ces données par l’intermédiaire de ses visiteurs.
Solution :
- Trouvez l’origine exacte de la page : dans l’onglet Network (réseau) des outils de développement, l’en-tête
Originde la requête préliminaire. - Mettez exactement cette origine dans
ALLOWED_ORIGINS, avec son schéma et son port. - Gardez une liste distincte pour chaque endroit où l’API s’exécute. En production, inscrivez seulement l’origine réelle de l’interface utilisateur.
Vérifiez votre compréhension
Un collègue dit : « Notre API est protégée contre les inconnus, car CORS autorise seulement notre propre site Web. » Qu'est-ce qui cloche?
Afficher la réponse
CORS indique seulement aux navigateurs quelles pages peuvent lire les réponses. N’importe qui peut appeler directement l’API avec curl, Postman ou un script et obtenir la réponse, comme l’exemple evil.example l’a montré. Pour bloquer les inconnus, l’API a besoin d’authentification, par exemple une clé d’API ou une connexion utilisateur. CORS et l’authentification règlent des problèmes différents.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Vérification du module : dans votre tableau de contrat, faites l'appel de chaque ligne pour /v1/classify et /v1/categories. Confirmez le code d'état et la structure du corps. Quelle ligne une page de navigateur ne pourrait-elle pas tester, et pourquoi?
Afficher la réponse
Chaque ligne donne le code d’état prévu et, pour les erreurs, la structure {"error": {"code", "message", "fields"}}. Une page peut toutes les tester une fois son origine autorisée. Mais un navigateur ne peut pas envoyer un corps JSON invalide avec fetch et JSON.stringify, ni omettre l’en-tête Origin. Testez ces lignes avec curl ou Postman.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
- Une origine comprend un schéma, un hôte et un port. Les navigateurs laissent une page lire seulement les réponses de sa propre origine, sauf si le serveur en autorise d’autres avec CORS.
- Pour un
POSTavec du JSON, le navigateur envoie d’abord une requête préliminaireOPTIONS. L’intergiciel CORS y répond. - Autorisez des origines exactes depuis la configuration (
ALLOWED_ORIGINS). N’utilisez pas*pour une API avec des clés ou des données privées. - CORS n’est pas de l’authentification. Les clients qui ne sont pas des navigateurs l’ignorent.
Vous avez maintenant conçu un contrat : des routes claires, une seule structure d’erreur et un accès sûr depuis le navigateur. Dans le prochain module, vous le documentez avec OpenAPI et Swagger UI.
Leçon suivante : OpenAPI et Swagger UI