Maintenant que nous avons des utilisateurs dans notre CMS, nous devons leur donner la possibilité de se connecter et appliquer une gestion basique de contrôle d’accès à la création et à la modification d’articles.
Si vous créez ou mettez à jour un utilisateur, vous remarquerez que les mots de passe sont stockés en clair, ce qui est évidemment très mauvais en terme de sécurité.
Corriger ce point nous permet de parler un peu plus de la couche model de CakePHP.
Dans CakePHP, nous séparons les méthodes qui s’occupent des collections d’objets
et d’un seul objet en différentes classes. Les méthodes qui s’occupent de
collections d’entity sont dans les classes Table
tandis que les fonctionnalités
liées à un seul enregistrement sont mises dans les classes Entity
.
Par exemple, hasher un mot de passe se fait par enregistrement, c’est pourquoi nous allons implémenter ce comportement dans l’objet Entity. Puisque nous voulons hasher le mot de passe à chaque fois qu’il est défini, nous allons utiliser une méthode mutator / setter. Par convention, CakePHP appellera les méthodes de setter chaque fois qu’une propriété se voit définie une valeur dans une entity. Ajoutons un setter pour le mot de passe. Dans src/Model/Entity/User.php, ajoutez le code suivant:
<?php
namespace App\Model\Entity;
use Cake\Auth\DefaultPasswordHasher; // Ajouter cette ligne
use Cake\ORM\Entity;
class User extends Entity
{
// Tout le code de bake sera ici.
// Ajoutez cette méthode
protected function _setPassword($value)
{
if (strlen($value)) {
$hasher = new DefaultPasswordHasher();
return $hasher->hash($value);
}
}
}
Maintenant, rendez-vous sur http://localhost:8765/users pour voir une liste des utilisateurs existants. Vous pouvez modifier l’utilisateur par défaut qui a été créé pendant le chapitre Installation du tutoriel. Si vous changez le mot de passe de l’utilisateur, vous devriez voir une version hashé du mot de passe à la place de la valeur par défaut sur l’action index ou view. CakePHP hash les mots de passe, par défaut, avec bcrypt. Vous pouvez aussi utiliser SHA-1 ou MD5 si vous travaillez sur une base de données déjà existante mais nous vous recommandons d’utiliser bcrypt pour toutes vos nouvelles applications.
Dans CakePHP, l’authentification est gérée via Components (Composants). Les Components peuvent être considérés comme un moyen de créer des morceaux de code réutilisables pour les controllers en leur donnant un concept ou une fonctionnalité spécifique. Les Components peuvent se greffer aux événements du cycle de vie des événements des controllers et intéragir avec l’application de cette manière. Pour commencer, nous allons ajouter AuthComponent à notre application. Puisque nous voulons que les méthodes create, update et delete requièrent l’authentification, nous allons ajouter AuthComponent dans notre AppController:
// Dans src/Controller/AppController.php
namespace App\Controller;
use Cake\Controller\Controller;
class AppController extends Controller
{
public function initialize()
{
// Code existant
$this->loadComponent('Auth', [
'authenticate' => [
'Form' => [
'fields' => [
'username' => 'email',
'password' => 'password'
]
]
],
'loginAction' => [
'controller' => 'Users',
'action' => 'login'
],
// Si pas autorisé, on renvoit sur la page précédente
'unauthorizedRedirect' => $this->referer()
]);
// Permet à l'action "display" de notre PagesController de continuer
// à fonctionner. Autorise également les actions "read-only".
$this->Auth->allow(['display', 'view', 'index']);
}
}
De cette manière, nous disons à CakePHP de charger les Components Flash
et
Auth
. De plus, nous avons personnaliser la configuration de AuthComponent
car notre tables d’utilisateurs (users), utilise le champ email
comme
identifiant. À partir de maintenant, si vous vous rendez sur n’importe laquelle
des URLs protégées, comme /articles/add
, vous allez être redirigé sur
/users/login, ce qui devrait vous afficher une page d’erreur puisque nous
n’avons pas encore écrit le code qui gère cette page. Créons maintenant l’action
login:
// Dans src/Controller/UsersController.php
public function login()
{
if ($this->request->is('post')) {
$user = $this->Auth->identify();
if ($user) {
$this->Auth->setUser($user);
return $this->redirect($this->Auth->redirectUrl());
}
$this->Flash->error('Votre identifiant ou votre mot de passe est incorrect.');
}
}
Et dans src/Template/Users/login.ctp, ajoutez:
<h1>Login</h1>
<?= $this->Form->create() ?>
<?= $this->Form->control('email') ?>
<?= $this->Form->control('password') ?>
<?= $this->Form->button('Connexion') ?>
<?= $this->Form->end() ?>
Note
La méthode control()
est disponible depuis 3.4. Pour les versions précédentes,
utilisez la méthode input()
à la place.
Maintenant que nous avons un formulaire de connexion basique, nous devrions être capable de nous connecter avec un utilisateur qui a un mot de passe hashé.
Note
Si aucun de vos utilisateurs a un mot de passe hashé, commentez le bloc
loadComponent('Auth')
et les appels à $this->Auth->allow()
.
Puis allez éditer un utilisateur pour lui sauvegarder un nouveau mot de passe.
Après avoir sauvegardé le mot de passe pour l’utilisateur, décommentez les
lignes que vous venez tout juste de commenter.
Avant de vous connectez, visitez /articles/add
. Puisque l’action n’est pas
autorisée, vous serez redirigé sur la page de connexion. Après vous être connecté
CakePHP vous redirigera automatiquement sur /articles/add
.
Maintenant que vos utilisateurs peuvent se connecter, il faut leur donner la possibilité
de se déconnecter. Ajoutez le code suivant dans le UsersController
:
public function initialize()
{
parent::initialize();
$this->Auth->allow(['logout']);
}
public function logout()
{
$this->Flash->success('Vous avez été déconnecté.');
return $this->redirect($this->Auth->logout());
}
Ce code ajoute l’action logout
à la liste des actions qui ne nécessitent pas
d’être authentifié et implémente la logique de déconnexion. Vous pouvez vous rendre
à l’adresse /users/logout
pour vous déconnecter. Vous serez ensuite redirigé
sur la page de connexion.
Si vous n’êtes pas connecté et essayez de visiter /users/add, vous serez
redirigé sur la page de connexion. Puisque nous voulons autoriser nos utilisateurs
à créer un compte sur notre application, ajoutez ceci à votre UsersController
:
public function initialize()
{
parent::initialize();
// Ajoute l'action 'add' à la liste des actions autorisées.
$this->Auth->allow(['logout', 'add']);
}
Le code ci-dessus indique à AuthComponent
que la méthode add()
du
UsersController
peut être visitée sans être authentifié ou avoir besoin
d’autorisation. Pour avoir une page de création plus propre, nous vous invitons
à retirer les liens et autres contenus qui n’ont plus de sens pour cette page de
création de compte. De même, nous ne nous occuperons pas des autres actions
spécifiques aux utilisateurs, mais c’est quelque chose que vous pouvez faire vous
même comme exercice.
Maintenant que nos utilisateurs peuvent se connecter, nous souhaitons limiter
l’édition seulement aux articles qu’ils ont rédigé. Nous allons implémenter cette
fonctionnalité à l’aide d’un adapter “authorization”. Puisque nos besoins sont
assez limités, nous pouvons rediger cette logique dans le ArticlesController
.
Mais avant, nous devons indiquer à AuthComponent
comment notre application
va gérer l’accès à nos actions. Mettez à jour votre AppController
avec ceci:
public function isAuthorized($user)
{
// Par défaut, on refuse l'accès.
return false;
}
Ensuite, nous allons indiquer à AuthComponent
que nous voulons utiliser les
méthodes de hooks des controllers pour gérer l’authorization. Votre méthode
AppController::initialize()
devrait maintenant ressembler à ceci:
public function initialize()
{
// Code existant code
$this->loadComponent('Flash');
$this->loadComponent('Auth', [
// La ligne suivante a été ajoutée
'authorize'=> 'Controller',
'authenticate' => [
'Form' => [
'fields' => [
'username' => 'email',
'password' => 'password'
]
]
],
'loginAction' => [
'controller' => 'Users',
'action' => 'login'
],
// Si pas autorisé, on renvoit sur la page précédente
'unauthorizedRedirect' => $this->referer()
]);
// Permet à l'action "display" de notre PagesController de continuer
// à fonctionner. Autorise également les actions "read-only".
$this->Auth->allow(['display', 'view', 'index']);
}
Par défaut, nous empêchons l’accès et nous allons donner accès au fur et à mesure,
en fonction des cas. Pour commencer, nous allons ajouter la logique d’autorisation
pour les articles. Dans votre ArticlesController
, ajoutez le code suivant:
public function isAuthorized($user)
{
$action = $this->request->getParam('action');
// Les actions 'add' et 'tags' sont toujours autorisés pour les utilisateur
// authentifiés sur l'application
if (in_array($action, ['add', 'tags'])) {
return true;
}
// Toutes les autres actions nécessitent un slug
$slug = $this->request->getParam('pass.0');
if (!$slug) {
return false;
}
// On vérifie que l'article appartient à l'utilisateur connecté
$article = $this->Articles->findBySlug($slug)->first();
return $article->user_id === $user['id'];
}
Maintenant, si vous essayez de modifier ou supprimer un article qui ne vous appartient pas, vous serez redirigé sur la page où vous étiez avant. Si aucun message d’erreur n’apparaît, ajoutez ceci à votre layout:
// Dans src/Template/Layout/default.ctp
<?= $this->Flash->render() ?>
Bien que le code ci-dessus soit très simple, cela démontre comment vous pouvez facilement construire des logiques d’autorisation flexibles qui impliquent l’utilisateur connecté et / ou les données de la requête.
Bien que nous ayons bloqué l’accès de l’action edit, nous sommes toujours
vulnérables aux utilisateurs qui changeraient l’attribut user_id
des
articles pendant la modification. Mais nous allons commencer par nous occuper
de l’action add
en premier.
Lorsque vous créez des articles, on veut forcer le user_id
à celui de
l’utilisateur actuellement connecté. Remplacer le code de votre action add
par le code suivant:
// dans src/Controller/ArticlesController.php
public function add()
{
$article = $this->Articles->newEntity();
if ($this->request->is('post')) {
$article = $this->Articles->patchEntity($article, $this->request->getData());
// Changé : On force le user_id à celui de la session
$article->user_id = $this->Auth->user('id');
if ($this->Articles->save($article)) {
$this->Flash->success(__('Votre article a été sauvegardé.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('Impossible d\'ajouter votre article.'));
}
$this->set('article', $article);
}
Ensuite, nous allons nous occuper de l’action edit
. Remplacez le code de
l’action par ceci:
// Dans src/Controller/ArticlesController.php
public function edit($slug)
{
$article = $this->Articles
->findBySlug($slug)
->contain('Tags') // Charge les tags associés
->firstOrFail();
if ($this->request->is(['post', 'put'])) {
$this->Articles->patchEntity($article, $this->request->getData(), [
// Ajouté : Empêche la modification du user_id.
'accessibleFields' => ['user_id' => false]
]);
if ($this->Articles->save($article)) {
$this->Flash->success(__('Votre article a été modifié.'));
return $this->redirect(['action' => 'index']);
}
$this->Flash->error(__('Impossible de mettre à jour l\'article.'));
}
$this->set('article', $article);
}
Ici, nous avons modifier les propriétés qui peuvent être assignées en masse
via les options de patchEntity()
. Référez-vous à la section Changer les Champs Accessibles
pour plus de détails. Pensez également à retirer l’élément de contrôle de
user_id
sur src/Templates/Articles/edit.ctp.
Nous avons créer une application CMS simple qui permet à nos utilisateurs de se connecter, de poster des articles, leur ajouter des tags, récupérer les articles par leurs tags et nous avons fini par ajouter une couche de contrôle d’accès à nos articles. Nous avons également ajouté des améliorations UX en tirant avantage du FormHelper et de l’ORM.
Merci d’avoir pris le temps d’explorer CakePHP. Pour les prochaines étapes de votre apprentissage, nous vous conseillons la documentations de l’ORM ou bien de vous diriger vers la section topics.