Dans cette section, nous allons aborder quelques points intéressants qui reviennent souvent dans les questions concernant ce cours.
Les points abordés n’ont pas de relation particulière et peuvent donc être lu dans n’importe quel ordre.
- Comment générer des pages HTML depuis l'application Symfony 3 ?
- Comment autoriser l'accès à certaines urls avec notre système de sécurité ?
Comment générer des pages HTML depuis l'application Symfony 3 ?
La configuration présentée durant ce cours implique que toute l’application ne génère que des réponses en JSON ou en XML. Cependant, il peut arriver qu’une même application puisse servir des réponses en JSON, en HTML voire en CSV.
Pour ce faire nous pouvons utiliser deux options que propose FOSRestBundle.
Utiliser plusieurs règles dans le format_listener
Dans notre fichier de configuration, nous avions :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | fos_rest: serializer: serialize_null: true routing_loader: include_format: false view: view_response_listener: true formats: json: true xml: true format_listener: rules: - { path: '^/', priorities: ['json', 'xml'], fallback_format: 'json', prefer_extension: false } |
format_listener
Pour générer une page HTML, nous pouvons rajouter une nouvelle règle dans la clé format_listener.rules
. L’ordre de déclaration étant important, il faut toujours déclarer les règles les plus spécifiques en premier.
1 2 3 4 5 6 7 8 9 10 11 12 | view: view_response_listener: true formats: json: true xml: true templating_formats: html: true format_listener: rules: - { path: '^/route/json', priorities: ['json'], fallback_format: 'json', prefer_extension: false } - { path: '^/route', priorities: ['html'], fallback_format: 'html', prefer_extension: false } - { path: '^/', priorities: ['json', 'xml'], fallback_format: 'json', prefer_extension: false } |
Avec cette configuration, toutes les URLs commençant par /route/json
renverront du JSON. Par contre, si l’URL commence par /route
(mais sans la partie /json
, /route/other
par exemple) les réponses seront en HTML.
Si nous avions inversé ces deux règles, toutes les URLs /route/json
renverraient aussi du HTML car l’expression régulière ^/route
englobe aussi ^/route/json
.
Comme pour les formats JSON et XML, la génération de réponse au format HTML est déjà supporté par défaut. Mais en rajoutant la clé templating_formats.html
, la configuration est plus lisible. De plus, nous utilisons templating_formats
au lieu de formats
car pour les pages HTML, nous aurons besoin d’un template pour les afficher.
Nous pouvons rajouter autant de règles que nous voulons mais cela peut rapidement montrer ses limites. Nous avons ainsi la possibilité d’utiliser un autre système plus efficace pour isoler la partie API et la partie IHM1 de son application.
Configurer le zone_listener
Il existe un listener de FOSRestBundle que nous n’avons pas abordé qui permet d’isoler la partie API d’une application de manière très simple : le zone_listener
.
Le zone_listener
est un listener qui nous permet de désactiver toutes les fonctionnalités de FOSRestBundle pour un ensemble d’URLs.
Ajoutons d’abord un préfixe /api
à toutes les routes de notre API. La déclaration des routes pourrait ressembler à :
1 2 3 4 5 6 7 8 9 10 | places: type: rest resource: AppBundle\Controller\PlaceController prefix: /api prices: type: rest resource: AppBundle\Controller\Place\PriceController prefix: /api ... |
Tous les appels d’API restent identiques mais sont maintenant préfixés.
La configuration de FOSRestBundle devient maintenant :
1 2 3 4 5 6 7 8 9 10 11 | # src/app/config/config.yml fos_rest: routing_loader: include_format: false view: view_response_listener: true zone: - { path: ^/api } format_listener: rules: - { path: '^/api', priorities: ['json'], fallback_format: 'json' } |
zone_listener
La partie zone
permet d’activer le bundle que pour les routes commençants par /api
. Ainsi, toute requête en dehors de cette zone sera gérée nativement par Symfony. Nous pouvons ainsi faire cohabiter notre API et une IHM complète sans soucis.
En utilisant ce système de zone, il ne faut pas oublier de reconfigurer toute la partie liée au pare-feu de Symfony et à notre système de sécurité pour prendre en compte le préfixe.
La configuration finale serait donc :
1 2 3 4 5 6 7 8 9 10 | # app/config/security.yml security: firewalls: main: pattern: ^/api stateless: true simple_preauth: authenticator: auth_token_authenticator provider: auth_token_user_provider anonymous: ~ |
La clé pattern
prend en compte le préfixe.
1 2 3 4 5 6 7 8 9 10 11 12 13 | <?php # src/AppBundle/Security/AuthTokenAuthenticator.php public function createToken(Request $request, $providerKey) { $targetUrl = '/api/auth-tokens'; // Si la requête est une création de token, aucune vérification n'est effectué if ($request->getMethod() === "POST" && $this->httpUtils->checkRequestPath($request, $targetUrl)) { return; } // ... } // ... } |
L’URL dans targetUrl
contient maintenant notre préfixe /api
.
Il est quand même utile de souligner qu’il est préférable d’utiliser une application à part pour générer ses pages HTML et avoir une application dédiée pour son API. L’intérêt de REST est d’avoir une architecture orientée service et donc de séparer les différents composants.
-
Interface Homme Machine, dans notre cas la page qui s’affiche dans le navigateur. ↩
Comment autoriser l'accès à certaines urls avec notre système de sécurité ?
Comme vous l’avez sans doute remarqué, une fois le système de sécurité est activé, seule la requête de création de token est autorisée. Mais dans les faits, il est assez courant d’avoir plusieurs appels d’API accessible sans authentification.
Par exemple, comment autoriser les utilisateurs à s’inscrire ?
Actuellement cela est impossible mais nous pouvons corriger le tir très facilement.
Pour rappel, l’authentification est géré par la classe AuthTokenAuthenticator
dont voici un extrait du code :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | <?php # src/AppBundle/Security/AuthTokenAuthenticator.php namespace AppBundle\Security; // ... class AuthTokenAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface { /** * Durée de validité du token en secondes, 12 heures */ const TOKEN_VALIDITY_DURATION = 12 * 3600; protected $httpUtils; public function __construct(HttpUtils $httpUtils) { $this->httpUtils = $httpUtils; } public function createToken(Request $request, $providerKey) { $targetUrl = '/auth-tokens'; // Si la requête est une création de token, aucune vérification n'est effectué if ($request->getMethod() === "POST" && $this->httpUtils->checkRequestPath($request, $targetUrl)) { return; } $authTokenHeader = $request->headers->get('X-Auth-Token'); if (!$authTokenHeader) { throw new BadCredentialsException('X-Auth-Token header is required'); } return new PreAuthenticatedToken( 'anon.', $authTokenHeader, $providerKey ); } // ... } |
Pour vérifier si une requête a été faite sur une certaine URL, la méthode checkRequestPath
peut utiliser une route comme nous l’avons spécifié dans l’extrait de code ci-dessus, mais aussi le nom d’une route.
Le code peut donc être simplifié en utilisant directement le nom pour la route auth-tokens
: post_auth_tokens.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | <?php # src/AppBundle/Security/AuthTokenAuthenticator.php namespace AppBundle\Security; // ... class AuthTokenAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface { /** * Durée de validité du token en secondes, 12 heures */ const TOKEN_VALIDITY_DURATION = 12 * 3600; protected $httpUtils; public function __construct(HttpUtils $httpUtils) { $this->httpUtils = $httpUtils; } public function createToken(Request $request, $providerKey) { $targetUrl = 'post_auth_tokens'; // Si la requête est une création de token, aucune vérification n'est effectué if ($request->getMethod() === "POST" && $this->httpUtils->checkRequestPath($request, $targetUrl)) { return; } $authTokenHeader = $request->headers->get('X-Auth-Token'); if (!$authTokenHeader) { throw new BadCredentialsException('X-Auth-Token header is required'); } return new PreAuthenticatedToken( 'anon.', $authTokenHeader, $providerKey ); } // ... } |
Pour obtenir la liste des routes, nous pouvons utiliser la commande php bin/console debug:router
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | get_places GET ANY ANY /places get_place GET ANY ANY /places/{id} post_places POST ANY ANY /places remove_place DELETE ANY ANY /places/{id} update_place PUT ANY ANY /places/{id} patch_place PATCH ANY ANY /places/{id} get_prices GET ANY ANY /places/{id}/prices post_prices POST ANY ANY /places/{id}/prices get_themes GET ANY ANY /places/{id}/themes post_themes POST ANY ANY /places/{id}/themes get_users GET ANY ANY /users get_user GET ANY ANY /users/{id} post_users POST ANY ANY /users remove_user DELETE ANY ANY /users/{id} update_user PUT ANY ANY /users/{id} patch_user PATCH ANY ANY /users/{id} get_user_suggestions GET ANY ANY /users/{id}/suggestions post_auth_tokens POST ANY ANY /auth-tokens remove_auth_token DELETE ANY ANY /auth-tokens/{id} get_preferences GET ANY ANY /users/{id}/preferences post_preferences POST ANY ANY /users/{id}/preferences |
Avec le système de nommage de FOSRestBundle, nous avons des noms simples et surtout qui décrivent aussi le verbe HTTP associé à la route. Dés lors pour autoriser une action, nous pouvons nous baser uniquement sur le nom de la route correspondante (le verbe HTTP est vérifiée indirectement).
Ainsi pour autoriser la création d’utilisateurs et de tokens d’authentification, nous pouvons simplement utiliser respectivement les routes : post_users et post_auth_tokens.
Le code peut devenir :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | <?php # src/AppBundle/Security/AuthTokenAuthenticator.php namespace AppBundle\Security; // ... class AuthTokenAuthenticator implements SimplePreAuthenticatorInterface, AuthenticationFailureHandlerInterface { /** * Durée de validité du token en secondes, 12 heures */ const TOKEN_VALIDITY_DURATION = 12 * 3600; public function __construct() { } public function createToken(Request $request, $providerKey) { $autorisedPaths = [ 'post_users', // Création d'un utilisateur (inscription) 'post_auth_tokens' // Création d'un token (connexion) ]; $currentRoute = $request->attributes->get('_route'); if (in_array($currentRoute, $autorisedPaths)) { return; } $authTokenHeader = $request->headers->get('X-Auth-Token'); if (!$authTokenHeader) { throw new BadCredentialsException('X-Auth-Token header is required'); } return new PreAuthenticatedToken( 'anon.', $authTokenHeader, $providerKey ); } // ... } |
Le service HttpUtils
étant maintenant inutile, nous pouvons même le retirer de la configuration des services.
1 2 3 4 5 6 | # app/config/services.yml auth_token_authenticator: class: AppBundle\Security\AuthTokenAuthenticator # arguments: ["@security.http_utils"] # à supprimer ou à commenter public: false |
Bien sur, libre à vous de gérer la liste de routes autorisées comme bon vous semble (en injectant un paramètre configurable dans le service, en ayant une liste dans une variable statique, etc.).