TP3 : Dialogue front-end/back-end avec une API REST
BUTS PÉDAGOGIQUES
- Jonction front-end et back-end avec un webservice d'API REST
- Authentification des utilisateurs par formulaire
- Restriction des accès à l'API par authentification
- Mise en page responsive pour une application web
- Déploiement par conteneur
Dans ce sujet de TP, nous finalisons notre application web et la préparons pour son déploiement.
Exercice 1 • Consommer l'API depuis le front-end Vue.js
Modifiez l'URL d'API dans le front-end Vue.js pour recevoir la liste des oiseaux. Vérifiez que cela fonctionne : adaptez le code si nécessaire.
Faîtes en sorte que les compteurs mettent à jour le champ nbr dans la BDD.
Créer un formulaire VueJS pour ajouter de nouveaux oiseaux dans la BDD.
Exercice 2 • Authentification des utilisateurs
Nous voulons maintenant que les utilisateurs de l'application soient identifiés. Ils doivent pouvoir créer un compte, se connecter et se déconnecter. Dès lors, le formulaire de comptage des oiseaux sera uniquement accessible aux utilisateurs connectés. De plus, l'accès à l'API sera également restreint par authentification.
2.1 Back-end
Tout d'abord, ajoutez de nouveaux composants dans le projet :
- composer require symfony/security-bundle
- composer require firebase/php-jwt
- composer require doctrine/annotations
2.1.1 Une entité pour les informations des utilisateurs
- Créons une nouvelle entité User :
- php bin/console make:user
- À chaque question, appuyez sur la touche pour prendre le choix par défaut.
- php bin/console make:user
- Ajoutons deux autres champs à cette nouvelle entité User :
- php bin/console make:entity
- Première question : indiquez User comme nom de classe.
- Questions suivantes : créez les deux champs firstname et lastname de type string.
- php bin/console make:entity
- Faîte la migration vers la BDD pour que la table soit créée en accord avec le schéma décrit par l'entité :
- php bin/console make:migration
- php bin/console doctrine:migrations:migrate
2.1.2 Un nouveau controlleur pour gérer les authentifications
- Générez un nouveau contrôleur appelé AuthController :
- php bin/console make:controller AuthController
- Dans le fichier de configuration config/packages/security.yaml, changez l'algorithme de crytpage de auto à bcrypt :
security:
# ...
password_hashers:
# ...
App\Entity\User:
algorithm: bcrypt
- Dans le fichier du nouveau controlleur src/Controller/AuthController.php :
- Ajoutez les importations suivantes :
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use Firebase\JWT\JWT;
use App\Repository\UserRepository;
use App\Entity\User;
- Effacez la méthode entièrement index() : y compris son code et y compris le commentaire qui la précède.
- Ajoutez la nouvelle méthode qui permettra d'inscrire de nouveaux utilisateurs dans la base de données de l'application :
/**
* @Route("/auth/register", name="register", methods={"POST"})
*/
public function register(Request $request, UserPasswordEncoderInterface $encoder)
{
$password = $request->get('password');
$email = $request->get('email');
$firstname = $request->get('firstname');
$lastname = $request->get('lastname');
$user = new User();
$user->setPassword($encoder->encodePassword($user, $password));
$user->setEmail($email);
$user->setFirstname($firstname);
$user->setLastname($lastname);
$em = $this->getDoctrine()->getManager();
$em->persist($user);
$em->flush();
return $this->json([
'user' => $user->getEmail()
]);
}
- Ajoutez la nouvelle méthode qui permettra de vérifier si le mot de passe d'un utilisateur est correct lors d'une tentative de connexion :
/**
* @Route("/auth/login", name="login", methods={"POST"})
*/
public function login(Request $request, UserRepository $userRepository, UserPasswordEncoderInterface $encoder)
{
$user = $userRepository->findOneBy([
'email'=>$request->get('email'),
]);
if (!$user || !$encoder->isPasswordValid($user, $request->get('password'))) {
return $this->json([
'auth' => '0',
]);
}
$payload = [
"user" => $user->getUsername(),
"exp" => (new \DateTime())->modify("+60 minutes")->getTimestamp(),
];
$jwt = JWT::encode($payload, $this->getParameter('jwt_secret'), 'HS256');
return $this->json([
'auth' => '1',
'user' => [
'username' => $user->getUsername(),
'firstname' => $user->getFirstName(),
'lastname' => $user->getLastName(),
],
'token' => sprintf('Bearer %s', $jwt),
]);
}
- Ajoutez les importations suivantes :
- Dans le fichier de configuration config/services.yaml, ajoutez la phrase secrète utilisée dans la fonction ci-dessus pour l'algorithme de crytpage :
parameters:
jwt_secret: ESTIA_SECRET_YOU_WONT_GUESS
2.1.3 Test du nouveau service d'authentification
Pour tester que le service d'authentification fonctionne correctement, nous allons envoyer des requêtes post grâce à la commande curl.
- Tout d'abord, créons un nouvel utilisateur :
- curl -L -X POST 'http://127.0.0.1:8000/auth/register' -F 'email=jesuis.moi@net.estia.fr' -F 'password=1234' -F 'firstname=Jesuis' -F 'lastname=Moi'
- Vérifiez via phpMyAdmin que l'utilisateur a été correctement inséré dans la base de données.
- Testez maintenant la réponse du contolleur d'authentification pour une tentative de connexion correcte :
- curl -L -X POST 'http://127.0.0.1:8000/auth/login' -F 'email=jesuis.moi@net.estia.fr' -F 'password=1234'
- Puis pour une tentative de connexion incorrecte :
- curl -L -X POST 'http://127.0.0.1:8000/auth/login' -F 'email=jesuis.moi@net.estia.fr' -F 'password=5678'
2.1.4 Vérification d'authentification par jeton
Une fois l'utilisateur de l'application connecté, il faudra pouvoir vérifier l'autenticité de toute nouvelle demande venant de l'application : une authentification par jeton est alors utilisée.
- Créer le fichier src/Security/JwtAuthenticator.php contenant la déclaration de classe suivante :
<?php
namespace App\Security;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Guard\AbstractGuardAuthenticator;
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
use App\Entity\User;
class JwtAuthenticator extends AbstractGuardAuthenticator
{
private $em;
private $params;
public function __construct(EntityManagerInterface $em, ContainerBagInterface $params)
{
$this->em = $em;
$this->params = $params;
}
public function start(Request $request, AuthenticationException $authException = null)
{
$data = [
'message' => 'Authentication Required'
];
return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
}
public function supports(Request $request)
{
return $request->headers->has('Authorization');
}
public function getCredentials(Request $request)
{
return $request->headers->get('Authorization');
}
public function getUser($credentials, UserProviderInterface $userProvider)
{
try {
$credentials = str_replace('Bearer ', '', $credentials);
$jwt = (array) JWT::decode(
$credentials,
new Key($this->params->get('jwt_secret'), 'HS256')
);
return $this->em->getRepository(User::class)
->findOneBy([
'email' => $jwt['user'],
]);
} catch (\Exception $exception) {
throw new AuthenticationException($exception->getMessage());
}
}
public function checkCredentials($credentials, UserInterface $user)
{
return true;
}
public function onAuthenticationFailure(Request $request, AuthenticationException $exception)
{
return new JsonResponse([
'auth' => '0',
'message' => $exception->getMessage()
], Response::HTTP_UNAUTHORIZED);
}
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $providerKey)
{
return;
}
public function supportsRememberMe()
{
return false;
}
}
- Complétez les sections main et access_control du fichier config/packages/security.html en ajoutant la déclaration suivante :
security:
# ...
firewalls:
dev:
# ...
api:
pattern: ^/api
guard:
authenticators:
- App\Security\JwtAuthenticator
main:
# ...
# ...
access_control:
- { path: ^/api, roles: ROLE_USER }
- Créez un contrôleur de test :
- php bin/console make:controller ApiController
- Ajoutez-lui la méthode suivante :
/**
* @Route("/api/test", name="testapi")
*/
public function test()
{
return $this->json([
'message' => 'Happy Controller!',
]);
}
- Testez un accès direct avec curl, puis un accès en remplaçant par un jeton obtenu lors d'un login récent :
- curl -L -X GET 'http://127.0.0.1:8000/api/test'
- curl -L -X GET 'http://127.0.0.1:8000/api/test' -H 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyIjoidGVzdEBleGFtcGxlLmNvbSIsImV4cCI6MTY0ODI5MzAyM30.FOrNyl1MS91ZcYrtSts1CmJZQJFh8XwUcNFsU3HI40U'
2.2 Front-end
Nous allons maintenant modifier l'application pour lui ajouter un formulaire d'enregistrement, un formulaire de connexion, une vérification de l'accès au formulaire de comptage, et un bouton de déconnexion.
Tout d'abord, nous allons avoir besoin de stocker des informations :
- Créez un nouveau fichier index.js dans le répertoire « src/store/ » et copiez-y le code suivant :
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
user: null,
token: null,
},
mutations: {
setUser(state, user) {
state.user = user;
},
setToken(state, token) {
state.token = token;
},
},
actions: {
logout(state) {
state.user = null;
state.token = null;
}
},
getters: {
isLoggedIn(state) {
return !!state.token;
},
getUser(state) {
return state.user;
}
},
});
- Dans le fichier main.js :
- Importez le store :
import store from '@/store/';
- Puis, ajoutez-le dans la création de la vue avant la ligne render: ... :
store,
- Importez le store :
2.2.1 Connexion via un formulaire
- Créez un nouveau fichier AppLogin.vue dans le répertoire « src/components/ » et copiez-y le formulaire et le script suivant :
<template>
<div>
<h1>LOGIN</h1>
<form @submit.prevent="login">
<p><input v-model="email" placeholder="email" /></p>
<p><input v-model="password" placeholder="password" type="password" /></p>
<p><button type="submit">Login</button></p>
</form>
</div>
</template>
<script>
import { mapMutations } from 'vuex';
export default {
data: () => {
return {
email: "",
password: "",
};
},
methods: {
...mapMutations(['setUser', 'setToken']),
async login(e) {
e.preventDefault();
console.log('Login...');
},
},
};
</script>
- Ajoutez une route vers cette nouvelle page :
{
path: "/login",
name: "AppLogin",
component: require('@/components/AppLogin.vue').default
},
- Testez le formulaire via l'adresse http://127.0.0.1:8080/login en vérifiant le message produit dans la console lors de la validation.
- Complétez maintenant le code de la fonction login() de sorte à :
- Interroger le back-end en lui envoyant les identifiants de connexion via une requête post de type multipart/form-data et traiter le résultat en fonction que l'utlisateur a été authentifié ou non.
- Dans le cas où l'authentification réussi, mémorisez l'utilisateur puis redirigez vers la page principale :
this.setUser(user);
this.setToken(token);
this.$router.push('/');
- Dans le cas où l'authentification échoue, affichez un message d'information dans un paragraphe que vous ajouterez en dessous du formulaire.
2.2.2 Restriction de l'accès aux pages
- Importez le store dans le fichier src/router/index.js :
import store from '@/store/';
- Puis définissez les routes nécessistant d'être authentifié en ajoutant :
meta: { requiresAuth: true},
- Enfin, ajoutez le code suivant après la création du router :
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
if (store.getters['isLoggedIn'] !== false) {
return next()
} else {
return next({
path: '/login',
query: { redirect: to.fullPath }
})
}
} else {
return next()
}
})
- Vérifiez que l'accès aux pages nécessite désormais d'être authentifié, et que sinon une redirection vers le formulaire de connexion est bien en place.
2.2.3 Utilisation des autorisations
Ajouter le token en autorisation des entêtes de vos requêtes post pour pouvoir continuer d'accéder aux ressouces du back-end (par exemple, pour la liste des oiseaux de la page 4).
2.2.4 Déconnexion via un bouton
Aggrémentez vos pages avec des boutons de connexion et de déconnexion, par exemple :
<template>
<div>
<button v-if="isLoggedIn" v-on:click="logout()">Logout</button>
<router-link v-if="!isLoggedIn" to="/login"><button>Login</button></router-link>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
export default {
computed: {
...mapGetters(['isLoggedIn'])
},
methods: {
logout: function () {
this.$store._actions['logout'];
this.$router.push('/login');
}
},
};
</script>
2.2.5 Enregistrement via un formulaire
En prenant le formulaire de connexion en exemple, créer un formulaire d'enregistrement pour de nouveaux utilisateurs de l'application.
Exercice 3 • Application web responsive
Rendez votre application VueJS responsive pour de petites tailles d'écran :
- Dans le fichier main.js, ajoutez les deux importations suivantes :
import 'materialize-css/dist/css/materialize.min.css'
import 'material-design-icons/iconfont/material-icons.css'
- Dans le fichier App.vue, ajoutez la section de script suivante :
<script>
import M from 'materialize-css'
export default {
mounted () {
M.AutoInit()
}
}
</script>
- Diminuez la taille de votre navigateur web et consultez à nouveau les pages de votre application.
Exercice 4 • Emballer dans des conteneurs Docker
Nous allons maintenant exécuter les deux projets depuis des conteneurs Docker.
Tout d'abord, commencez par stopper les serveurs de « hello-vue » et de « hello-symfony » qui sont en cours d'exécution.
4.1 Back-end
- Créez un nouveau fichier Dockerfile dans le répertoire du projet et copiez-y les appels suivants :
FROM php:8.0-fpm
RUN apt update \
&& apt install -y zlib1g-dev g++ git libicu-dev zip libzip-dev zip \
&& docker-php-ext-install intl opcache pdo pdo_mysql \
&& pecl install apcu \
&& docker-php-ext-enable apcu \
&& docker-php-ext-configure zip \
&& docker-php-ext-install zip
WORKDIR /var/www/html/
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
RUN curl -sS https://get.symfony.com/cli/installer | bash
RUN mv /root/.symfony*/bin/symfony /usr/local/bin/symfony
EXPOSE 8000
CMD php bin/console server:run 0.0.0.0:8000
- Créez un nouveau fichier docker-compose.yml dans le répertoire du projet et copiez-y la structure suivante :
version: "3"
services:
database:
container_name: db_docker_symfony
image: mysql:8.0
command: --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ROOT_PASSWORD: estia
MYSQL_DATABASE: db_poitou_birds
MYSQL_USER: estia
MYSQL_PASSWORD: estia
ports:
- '4306:3306'
volumes:
- ../mysql:/var/lib/mysql
php:
container_name: php_docker_symfony
working_dir: /var/www/html
build:
context: .
ports:
- '8000:8000'
volumes:
- /home/estia/devel/hello-symfony/:/var/www/html
depends_on:
- database
- Dans le fichier .env, modifiez la variable de connexion à la base de données :
DATABASE_URL="mysql://root:estia@database:3306/db_poitou_birds?serverVersion=8.0"
- Préparez les images dockers puis démarrez le service :
- docker-compose build
- Patientez... le chargement des images peut prendre environ 10 minutes.
- docker-compose up
- docker-compose build
- Vérifiez que le service est en marche :
- docker ps
- Accédez au serveur Symfony s'exécutant depuis Docker : http://127.0.0.1:8000
4.2 Front-end
- Créez un nouveau fichier Dockerfile dans le répertoire du projet et copiez-y les appels suivants :
FROM node:lts-alpine
# install simple http server for serving static content
RUN npm install -g http-server
# make the 'app' folder the current working directory
WORKDIR /app
# copy 'package.json' to install dependencies
COPY package*.json ./
# install dependencies
RUN npm install
# copy files and folders to the current working directory (i.e. 'app' folder)
COPY . .
# build app for production with minification
RUN npm run build
EXPOSE 8080
CMD [ 'http-server', 'dist' ]
- Créez un nouveau fichier docker-compose.yml dans le répertoire du projet et copiez-y la structure suivante :
version: "3"
services:
vue-app:
working_dir: /app
build:
context: .
container_name: app_docker_vue
restart: always
ports:
- '8080:8080'
volumes:
- /home/estia/devel/hello-vue:/app
networks:
- vue-network
command: >
sh -c 'npm run serve'
networks:
vue-network:
driver: bridge
- Préparez les images dockers puis démarrez le service :
- docker-compose build
- Patientez... car le chargement des images peut prendre une dizaine de minutes.
- docker-compose up
- docker-compose build
- Vérifiez que le service est en marche :
- docker ps
- Accédez à l'application VueJS s'exécutant depuis Docker : http://127.0.0.1:8080