Development

Documentation/fr_FR/askeet/trunk/D6

You must first sign up to be able to contribute.

Version 3 (modified by mikael.randy, 10 years ago)
Poursuite de la traduction

Cette partie de la documentation est en cours de traduction. Cela signifie qu'elle est traduite de manière soit incomplète, soit inexacte. En attendant que cette traduction soit terminée, vous pouvez consulter la version en anglais pour des informations plus fiables.

Calendrier de l'avent sixième jour: Sécurité et validation de formulaire

Précédemment dans Symfony

Au cours du cinquième jour, vous avez dû manipuler les templates et les actions. Les formulaires et les pagineurs n'ont plus aucun secret pour vous. Mais après avoir construit le formulaire de connexion, vous attendez sûrement de nous que nous vous montrions comment restreindre l'accès à certaines fonctionnalités pour les membres non-autorisés. C'est ce que nous allons réaliser aujourd'hui, a l'aide de plusieurs validation de formulaires. Comme nous allons également ajouter des classes personnelles à l'application, vous devriez également comprendre les concepts exposés dans le chapitre concernant les extensions personnelles dans le livre Symfony.

Validation du formulaire de connexion

Fichier de validation

Le formulaire de connexion dispose de deux champs : nickname et password Mais que doit-il se passer si l'utilisateur soumets des données incorrectes ? Pour gérer ce cas de figure, vous devez créer un fichier login.yml dans le répertoire /frontend/modules/user/validate (Notez que login est le nom de l'action dont le formulaire doit être validé). Ajouter le contenu suivant à ce fichier :

[php]
methods:
  post: [nickname, password]

names:
  nickname:
    required:     true
    required_msg: your nickname is required
    validators:   nicknameValidator

  password:
    required:     true
    required_msg: your password is required

nicknameValidator:
    class:        sfStringValidator
    param:
      min:        5
      min_error:  nickname must be 5 or more characters

Premièrement, sous l'entête methods, vous devez lister les champs à valider pour la méthode du formulaire (nous ne définissons ici que la méthode POST car les données GET ne sont utilisées que pour afficher le formulaire et n'ont pas besoin d'être validée). Puis, sous l'entête names, les critères de validation de chaque champs à valider sont listés, suivi du message d'erreur correspondant. Eventuellement, à l'image du champs nickname, il est possible de définir des règles de validation spécifiques, sous l'entête correspondant au nom de cette règle. Dans cet exemple, le sfStringValidator est un validateur proposé par symfoy qui valide le format d'une chaîne de caractères (les validateurs par défaut proposés par Symfony sont détaillés dans le chapitre exposant comment valider un formulaire dans le livre Symfony)

Gestionnaire d'erreur

Maintenant, qu'est-il censé se passer si un utilisateur valide des données invalides ? Les conditions écrites dans le fichier login.yml ne seront pas satisfaites, et le contrôleur de symfony va transmettre la requête à la méthode handleErrorLogin de la classe userActions - à l'image de la méthode executeLogin(), tel que défini dans l'argument de la balise form_tag. Si cette méthode n'existe pas, le comportement par défaut est d'afficher le template loginError.php. La raison de ce comportement est que la méthode handleError() d'origine retourne :

[php]
public function handleError()
{
  return sfView::ERROR;
}

Nous avons un nouveau gabarit à écrire. Or, il serait préférable de ré-afficher le formulaire de connexion, avec les messages d'erreur affichés près des champs invalides. Donc, nous allons modifier le comportement de l'erreur de connexion pour afficher le template loginSuccess.php en cas d'erreur.

[php]
public function handleErrorLogin()
{
  return sfView::SUCCESS;
}

NOTE La convention de nommage qui lie le nom de l'action, sa valeur de retour et le nom du fichier template est détaillée dans le chapitre concernant la couche "Vue" du livre Symfony

Les helpers d'affichage des erreurs

Il est désormais temps d'afficher les erreurs dans le template loginSuccess.php. Pour cela, nous allons utiliser le helper form_error() de la librairie de helpers Validation, prévue à cet effet. Remplacez les deux div form_row du templates par le code suivant :

[php]
<?php use_helper('Validation') ?>

<div class="form-row">
  <?php echo form_error('nickname') ?>
  <label for="nickname">nickname:</label>
  <?php echo input_tag('nickname', $sf_params->get('nickname')) ?>
</div>

<div class="form-row">
  <?php echo form_error('password') ?>
  <label for="password">password:</label>
  <?php echo input_password_tag('password') ?>
</div>

Le helper form_helper() à pour comportement d'afficher le message d'erreur défini dans le fichier login.yml si une erreur est détectée dans le champ défini en paramètre.

Il est temps de tester la validation du formulaire en essayant de saisir un valeur de moins de 5 caractères dans le champs nickname, ou alors en ne remplissant pas l'un des champs. Le message d'erreur apparaît juste au dessus du champ concerné :

Erreur dans le formulaire de connexion

Le mot de passe est désormais obligatoire, mais aucun mot de passe n'est présent en base de données ! Pas d'inquiétude, quel que soit le mot de passe que vous saisissiez, la connexion fonctionnera, pour un peu que le login existe en base de données. Ce n'est pas très sécurisé, n'est-ce pas ?

Style des message d'erreur

Si vous avez testé le formulaire et obtenu une erreur, vous avez sûrement remarqué que cette erreur n'est pas affichée comme dans l'image présentée. La raison est que nous avons défini le style de la classe .form_error_class (dans le fichier web/main.css), qui est la classe par défaut associée aux champs générés par le helper form_error():

[php]
.form_error
{
  padding-left: 85px;
  color: #d8732f;
}

Authentifier un utilisateur

Validateur personnalisé

Vous rappelez vous la vérification d'hier, concernant l'existence du nickname saisie dans l'action login ? Cette vérification ressemble à la validation d'un formulaire. Ce code pourrait être placé en dehors de l'action et inclus dans un validateur personnalisé. Vous pensez que c'est compliqué ? On ne peut pas dire ça. Editez le fichier de validation login.yml et modifiez son contenu de la sorte :

[php]
...
names:
  nickname:
    required:      true
    required_msg:  your nickname is required
    validators:    [nicknameValidator, userValidator]
...
userValidator:
    class:         myLoginValidator
    param:
      password:    password
      login_error: this account does not exist or you entered a wrong password

Nous venons d'ajouter un nouveau validateur au champ nickname, basé sur la classe myLoginValidator. Cette classe n'existe pas encore, mais nous savons qu'il lui faudra le password pour complètement valider l'authentification d'un utilisateur, nous le transmettons donc en paramètre sous le label password

Stockage du mot de passe

Mais attendez une minute. Dans notre modèle de données, autant que dans les données de test, aucun mot de passe n'est défini. Il est donc temps de le définir. Mais vous devez savoir que stocker un mot de passe en clair, dans une base de données, est une très mauvaise idée pour des raisons de sécurité. Nous allons donc stocker une version hashé par sha1 du mot de passe, ainsi que la clé aléatoire utilisée pour le hasher. Si vous n'êtes pas familier avec ce principe de 'sel', consultez les techniques de crackage de mot de passe.

Ouvrez le fichier schema.xml et ajouter les colonnes suivantes dans la table User :

[php]
<column name="email" type="varchar" size="100" />
<column name="sha1_password" type="varchar" size="40" />
<column name="salt" type="varchar" size="32" />

Re-génerez le modèle de données Propel via la commande symfony propel-build-all. Vous auriez également pu ajouter les 2 colonnes à la base de données, soit manuellement, soit en utilisant le fichier lib.model.schema.sql généré à la suite de l'exécution de la commande symfony propel-build-sql. Editez maintenant le fichier askeet/lib/model/User.php et ajouter cette méthode setPassword() :

[php]
public function setPassword($password)
{
  $salt = md5(rand(100000, 999999).$this->getNickname().$this->getEmail());
  $this->setSalt($salt);
  $this->setSha1Password(sha1($salt.$password));
}

Cette fonction simule un stockage direct du mot de passe, mais effectue réellement la génération d'une clé aléatoire salt (une chaîne de 32 caractères hashé) et le mot de passe hashé (une chaîne de 40 caractères).

Ajouter le mot de passe dans les données de test

Vous rappelez vous le fichier de données de test du troisième jour ? Il est temps d'ajouter un mot de passe et un email aux données de test. Ouvrez et modifiez le fichier askeet/data/fixtures/test_data.yml comme suit :

[php]
User:
  ...
  fabien:
    nickname:   fabpot
    first_name: Fabien
    last_name:  Potencier
    password:   symfony
    email:      fp@example.com

  francois:
    nickname:   francoisz
    first_name: François
    last_name:  Zaninotto
    password:   adventcal
    email:      fz@example.com

Comme la méthode setPassword() a été définie dans la classe User, l'objet sfPropelData remplira correctement les nouvelles colonnes sha1_password et salt définies dans le schéma lors de l'appel au script de population des données :

$ php batch/load_data.php

NOTE Notez que le l'objet sfPropelData est capable d'utiliser des méthodes qui ne correspondent pas à des colonnes réelles du modèle de données (Et voilà, nous dépassons les capacités du SQL traditionnel !)

Si vous vous demandez comment c'est possible, vous n'avez qu'a jetez un coup d'oeil dans le chapitre de population de base de données du livre symfony

Note: Le fait de ne pas définir de mot de passe pour l'utilisateur 'Anonymous Coward' permet d'interdire la connexion avec ce compte. De plus, ne tentez pas de vous connecter sur le site public Askeet avec les mots de passes donnés en exemple, il est évident que nous ne donnons pas les vrais mots de passe.

Validateur personnalisés

Maintenant, il est temps de définir le validateur myLoginValidator. Vous pouvez créer cette classe myLoginValidator.class.php dans n'importe quel dossier lib/ accessible par le module (c'est à dire, pour le cas présent, dans askeet/lib/, ou dans askeet/apps/frontend/lib/, ou encore dans askeet/apps/frontend/modules/user/lib/). Dans le cas présent, le validateur est considéré comme pouvant être utile à toute l'application, et sera donc créé dans le dossier askeet/apps/frontend/lib/ :

[php]
<?php

class myLoginValidator extends sfValidator
{    
  public function initialize($context, $parameters = null)
  {
    // initialize parent
    parent::initialize($context);

    // set defaults
    $this->setParameter('login_error', 'Invalid input');

    $this->getParameterHolder()->add($parameters);

    return true;
  }

  public function execute(&$value, &$error)
  {
    $password_param = $this->getParameter('password');
    $password = $this->getContext()->getRequest()->getParameter($password_param);

    $login = $value;

    // anonymous is not a real user
    if ($login == 'anonymous')
    {
      $error = $this->getParameter('login_error');
      return false;
    }

    $c = new Criteria();
    $c->add(UserPeer::NICKNAME, $login);
    $user = UserPeer::doSelectOne($c);

    // nickname exists?
    if ($user)
    {
      // password is OK?
      if (sha1($user->getSalt().$password) == $user->getSha1Password())
      {
        $this->getContext()->getUser()->setAuthenticated(true);
        $this->getContext()->getUser()->addCredential('subscriber');

        $this->getContext()->getUser()->setAttribute('subscriber_id', $user->getId(), 'subscriber');
        $this->getContext()->getUser()->setAttribute('nickname', $user->getNickname(), 'subscriber');

        return true;
      }
    }

    $error = $this->getParameter('login_error');
    return false;
  }
}

Lorsque le validateur est appelé - après la validation du formulaire de connexion - la méthode initialize() est tout d'abord appelée. Cette méthode initialise la valeur par défaut du message login_error ('Invalid input') et ajoute les paramètres du validateur (ceux qui se trouvent sous l'entête param: dans le fichier login.yml) dans l'objet de collecte des paramètres.

Puis la méthode execute() est ... exécutée. La variable $password_param contient la valeur définie dans le fichier login.yml sous l'entête password. Cette variable est utilisée pour récupérer une valeur dans les paramètres transmis. Donc, la variable $password contient le mot de passe saisi par l'utilisateur. La variable $value contient la valeur du champ courant - et la classe myLoginValidator est appelée pour le champ nickname. La variable $login contient donc le nickname saisi par l'utilisateur. Désormais, nous avons toutes les données nécessaires pour que le validateur puisse valider un utilisateur.

Ce code remplace un code existant de l'action login. En outre, le test de validité du mot de passe (qui était précédement toujours vrai) est implémenté : un hashage du mot de passe saisi par l'utilisateur (en utilisant le sel stocké en base de données) est comparé au mot de passe hashé de l'utilisateur.

Si le login et le mot de passe sont correct, le validateur retourne true et l'action associée au formulaire (executeLogin()) sera exécutée. Sinon, il retourne false et c'est la fonction handleErrorLogin() qui sera exécutée.