PHP-Astux.info

Tux ce héros (les faits Tux) sur ce site !

Dernière màj : 17-07-2012

Créer une section membre

Sommaire

  1. Qu'est-ce que la section membre ?
  2. Méthode 1 : fichiers .htaccess et .htpasswd
  3. Méthode 2 : en utilisant la Session PHP
    • Réflexions sur la (future) section membre
    • Création de la table dans la base de données
    • Création du jeu de pages
    • Verrous dans la session : Contrôle de l'accès aux pages
    • Verrous dans la session : Contrôle du contenu des pages
    • Déconnexion
    • Limitations du système
    • Compléments à cette section membre

1. Qu'est-ce que la section membre ?

Une section membre est une zone de site protégée et dont les fonctionnalités sont réservées à certaines personnes. Concrètement, il faut des identifiants (login, mot de passe) pour accéder à une ou plusieurs pages, à un ou pluieurs fichiers. Il existe différentes manières de faire une section protégée par mot de passe, nous allons expliciter les 2 principales ici, sous un environnement Apache.

Méthode 1 : fichiers .htaccess et .htpasswd

Cette méthode met donc en jeu ces 2 fichiers. Il s'agit de fichiers sans extension, le point devant leur nom étant pour les cacher dans une arborescence Linux. Si votre éditeur de texte grogne quand vous voulez enregistrer sous ".htaccess", vous pouvez toujours enregistrer sous "aaa.htaccess", transférer via FTP sur le serveur, et renommer sur le serveur aaa.htaccess => .htaccess.

Le fichier .htaccess se place à la racine du site (c.à.d. le dossier de plus haut niveau, celui qui n'est contenu dans personne d'autre lorsque vous vous connectez par un client FTP au serveur). Le fichier .htpasswd, quant à lui, doit se placer ailleurs autant que faire se peut (en effet, si vous le placez dans le dossier concerné par la protection, les gens qui auront l'accès pourront voir tous les couples login/mot de passe). Les mots de passe sont en général cryptés dans celui-ci.

Pour plus d'explication sur cette méthode, voir Webmaster-Hub, § La restriction d'accès par mot de passe (à peu près en milieu de page).

Méthode 2 : en utilisant la Session PHP

Dans cette méthode, nous allons créer de A à Z une petite section membre, que vous pourrez à loisir compléter, modifier par la suite. Pour réaliser ce tutorial, il faut des connaissances en :

Explicitons tout d'abord le principe de la session. Une session est une suite d'actions délimitée par un événement déclencheur (l'ouverture de session) et un événement de cloture (la fermeture de session). Pour comparer au monde réel, une session de jeu par exemple commence à l'entrée du sportif sur un terrain (foot, tennis ...), le sportif joue des matchs (= ce qu'il se passe pendant la session) jusqu'à sa sortie du terrain (= fin de session). En informatique, la session est lorsqu'on "ouvre" un programme, par exemple Word, tout ce qui est réalisé (copie de texte, impression de document, etc.) est fait pendant la session Word. On ferme la session lorsqu'on quitte Word. C'est pareil pour Windows, d'ailleurs, ou n'importe quel système d'exploitation.

PHP a son système de session également, et c'est un tableau de variables qui ont chacune une valeur conservée de page en page, tant que la session n'est pas fermée. Si vous avez lu - à ce point du discours - l'article sur les Sessions PHP Vous savez à quoi ressemble ce tableau et la suite du tutorial sera plus facile à lire.

Réflexions sur la (future) section membre

Ce court paragraphe permet de se poser d'emblée les bonnes questions sur la section membre en PHP : quelles données vais-je stocker ? Quel degré de protection dois-je donner ? Quel type de permissions puis-je offrir à mes "utilisateurs" ?

C'est une étape très importante, il ne faut pas se jeter sur le code source avant de l'avoir bien réalisée ... Elle permet de délimiter toutes les bordures de la section membre

L'exemple de section membre que nous allons réaliser ici est une série de documents numériques (des photos ;o)) que nous allons présenter en partie ou en totalité selon la proximité des "amis". Comprenons-nous bien : il y a des photos à ne surtout pas montrer à votre petite amie...

Pour cet article, nous définirons l'utilisateur par ces valeurs :

Création de la table dans la base de données

On peut à loisir compléter cette liste, elle n'est pas exhaustive. Maintenant, créons la base de données MySQL associée :

-- TABLE : utilisateurs
CREATE TABLE utilisateurs (
  login      VARCHAR(20) NOT NULL,
  passwd     VARCHAR(50) NOT NULL,
  nom        VARCHAR(50) NOT NULL,
  prenom     VARCHAR(50) NOT NULL,
  grade      ENUM ('ami', 'famille', 'administrateur') NOT NULL DEFAULT 'ami',
  email      VARCHAR(50) NOT NULL,

  PRIMARY KEY(login)
) Engine = MyISAM;

Explications sur la table : J'ai arbitrairement choisi de mettre le login comme clé primaire, cela m'assure qu'il n'y aura pas 2 fois le même login dans ma table. Attention cependant pour des grosses tables, le traitement des clés primaires est plus rapide sur des nombres que sur du texte. Le mot de passe contient jusqu'à 50 caractères, ça tombe bien : il sera crypté par sécurité pour le membre.

Création du jeu de pages

Nous sous-entendons ici que le sous dossier photos/ de votre site doit être protégé. Ce dossier contient les fichiers suivants :

On va donc utiliser une session PHP pour protéger toutes ces pages. Le principe est simple (et est déjà expliqué dans l'article sur les Sessions PHP d'ailleurs) : lorsque la personne ouvre le dossier photos, une session s'ouvre. Dès qu'elle est ouverte, la session vérifie si la personne est passée par le formulaire de connexion : si oui (et si le couple login/mot de passe est juste) alors le serveur envoie la suite de la page, si non, il envoie juste un lien pour se connecter ...

<apparté>Et par pitié ... Rendez à César ce qui est à César, et à Dieu ce qui est à Dieu : FRANCAIS : conneXion // ANGLAIS : conneCTion</apparté>.

Bref, concrètement, cela donne ceci :

Page index.php : On démarre la session. Si le formulaire a été soumis, on le traite : on regarde s'il existe quelqu'un correspondant au couple login/mot de passe renseigné, si oui : on enregistre dans la session le login, si non ... On affiche une erreur. En cas d'erreur (ou de formulaire non soumis, ce qui est le cas quand on arrive sur la page la première fois), on affiche le formulaire de connexion.

Voici la page index.php de notre sous dossier, toute prête :

<?php
session_start();

// Attention, il est préférable d'utiliser une autre façon de se connecter à MySQL
// Voyez l'article sur mysql + PDO
// ici le but n'est pas d'illustrer la connexion MySQL.

if (!isset($_POST['submit']))
{
	// c'est la première fois qu'on vient sur cette page, aucun formulaire soumis
	echo '
	<form id="connect" method="post" action="">
	<fieldset><legend>Formulaire de connexion</legend>
		<p><label for="login">Login : </label><input type="text" id="login" name="login" tabindex="1" value="" /></p>
		<p><label for="pwd">Mot de passe :</label><input type="password" id="pwd" name="pwd" tabindex="2" /></p>
	</fieldset>
	<div><input type="submit" name="submit" value="Connexion" tabindex="3" /></div>
	</form>';
}
else
{
	// le formuaire vient d'être soumis
	$login  = (isset($_POST['login'])) ? htmlentities(trim($_POST['login'])) : '';
	$pwd   = (isset($_POST['pwd']))    ? htmlentities(trim($_POST['pwd']))   : '';

	if (($login != '') && ($pwd != ''))
	{
		// Login et pwd non vides, on  vérifie s'il y a quelqu'un qui correspond
		$req_utilisateur = sprintf("SELECT
							nom,
							prenom,
							grade,
							email
						FROM
							utilisateurs
						WHERE
							(login = '%s' AND passwd = '%s')",$login, md5($pwd));
		$utilisateur = mysql_query($req_utilisateur) or die($req_utilisateur."<br />\n".mysql_error());

		if (mysql_num_rows($utilisateur) == 1)
		{
			// Oui il y a quelqu'un ...
			$personne = mysql_fetch_array($utilisateur);

			// On  enregistre ses données dans la session
			$_SESSION['login'] = $login; // permet de vérifier que l'utilisateur est bien connecté
			$_SESSION['nom'] = $personne['nom'];
			$_SESSION['prenom'] = $personne['prenom'];
			$_SESSION['grade'] = $personne['grade'];
			$_SESSION['email'] = $personne['email'];

			// Maintenant que tout est enregistré dans la session, on redirige vers la page des photos
			echo '<p>Vous êtes correctement identifié(e), <a href="liste-photos.php">cliquez ici pour visualiser les photos</a></p>'."\n";
		}
		else
		{
			// Erreur dans le login et / ou dans le mot de passe ...
			echo '<p>Désolé, vous avez peut-être fait une erreur dans la saisie des identifiants, mais votre parcours se finit là ... </p>'."\n";
		}
	}
	else
	{
		// il n'y a personne qui répond à ces 2 identifiants
		echo '<p>Désolé, vous avez peut-être fait une erreur dans la saisie des identifiants, mais votre parcours se finit là ... </p>'."\n";
	};
} // end of (isset($_POST['submit']))
?>

Si la base de donnée retourne un seul enregistrement, alors ses valeurs sont stockées dans la session et on affiche le lien vers la page liste-photos.php.

Remarques annexes :

Mais qu'en est-il si on tente de contourner le système ? Si un utilisateur, malintentionné, rentre dans son navigateur l'adresse directe vers la page liste-photos.php ?

Verrous dans la session : Contrôle de l'accès aux pages

Le principe de la session est de verrouiller un ensemble de pages à certains utilisateurs pendant leur navigation, entre leur connexion et leur déconnexion.

Ainsi, chaque page qui doit être protégée par ce système doit porter l'extension .php (pour utiliser la session PHP !) et en tête de page, doit porter ces quelques lignes :

<?php
session_start(); // ici on continue la session
if ((isset($_SESSION['login'])) && ($_SESSION['login'] != ''))
{
	// la session est bien active, et la personne est bien connectée ...
	// on affiche ici le contenu de la page
}
else
{
	// aie, quelqu'un tente de contourner mon système sans passer par le formulaire de connexion !
	echo '<p>Petit coquin, va ...</p>';
	exit(); // par cette commande, on coupe l'exécution de la page
}
?>

Grâce à cette longue commande de vérification, on est sûr que, lorsque la page est chargée, le visiteur est passé par le formulaire de connexion et s'est connecté sans erreur.

Astuce : pour vous éviter de recopier cette ligne à chaque fichier (et, potentiellement, de faire une faute de frappe), on va utiliser la puissance de PHP.

On va créer un fichier nommé control-session.php qui contient les lignes suivantes :

<?php
session_start(); // ici on continue la session
if ((!isset($_SESSION['login'])) || ($_SESSION['login'] == ''))
{
	// La variable $_SESSION['login'] n'existe pas, ou bien elle est vide
	// <=> la personne ne s'est PAS connectée
	echo '<p>Vous devez vous <a href="index.php">connecter</a>.</p>'."\n";
	exit();
}
?>

Explication : si la variable $_SESSION['login'] n'existe pas ou bien n'est pas remplie, alors on affiche un lien pour le formulaire de connexion (index.php) et on arrête là la page (exit) : grâce à ce mécanisme, la personne qui veut voir les photos sans se connecter sera confrontée au terrible formulaire de connexion ...

Et dans chacune des pages qu'on veut protéger par notre session (donc chaque page accessible uniquement aux membres s'étant connectés), on rajoute cette ligne :

<?php
	require('control-session.php');
?>

<!-- ici le reste de la page ... HTML ou PHP ... -->

Et notre section membre est presque terminée ... Epatant n'est-ce pas ? Le fait de faire un "require" va appeler le fichier de contrôle de la session, celui-ci va faire un test sur la variable de session ... Et si le test échoue, il bloque la page pour n'afficher que le lien de connexion. Le reste de la page après le require peut être de l'HTML, ou du PHP (en partie ou en intégralité), peu importe...

Si vous avez compris ce principe, vous avez compris 80% de la section membre ...

Remarque : le fichier "control-session.php" teste la variable $_SESSION['login'], mais j'aurais pu tester n'importe laquelle des variables définies dans index.php juste avant le lien vers la page de listing des photos... Mais, dans la mesure où j'imagine que le reste des données est potentiellement vide, je préfère m'assurer de tester une variable qui, j'en suis sûr, sera toujours remplie ... ;o)

Verrous dans la session : Contrôle du contenu des pages

Vous vous souvenez certainement qu'il y a des photos que nous ne devons PAS montrer à tout le monde. Surtout pas. C'est pour cela que nous avons créé des grades :

Pour contrôler le contenu d'une page en fonction du grade, le principe est exactement le même ... Supposons 3 galeries de photos, respectivement "à la montagne", "à la campagne" et "à la mer"... Avec "à la mer" qui contient des photos personnelles.

On va donc créer des liens pour que les gens connectés affichent les galeries : pour le grade "ami", les 2 premières galeries seront affichées, et pour les autres grades, toutes les galeries seront affichées.

De la même manière qu'on contrôle qu'il n'y ait pas d'utilisateur mal intentionné qui tente de contourner le formulaire de connexion, on contrôlera également qu'un "ami" ne tente pas de contourner le système de grades ...

Voici le fichier "liste-photos.php" :

<?php
	require('control-session.php'); // session + on vérifie que l'utilisateur est bien connecté

	// maintenant on affiche les liens ves les galeries photos :
	echo '<p>';
	echo '<a href="photos-montagne.php">Photos à la montagne</a> - ';
	echo '<a href="photos-campagne.php">Photos à la campagne</a> - ';

	if ($_SESSION['grade'] != 'ami')
	{
		echo '<a href="photos-mer.php">Photos à la mer</a>';
	}
	echo '</p>';
?>

Voilà, rien de bien sorcier, juste 3 liens affichés ou 2 selon le grade ... N'oubliez pas, dans les 3 fichiers "photos" listés de faire un require sur le fichier control-session.php comme fait précédemment. La section membre est presque terminée. Maintenant dans la page "photos-mer.php" (puisque c'est la catégorie sensible), on doit effectuer un contrôle pour vérifier que c'est bien un membre de la famille qui tente d'afficher cette page :

<?php
	require('control-session.php'); // session + on vérifie que l'utilisateur est bien connecté
	if ($_SESSION['grade'] == 'ami') // ah non, pas eux !!!
	{
		echo '<p>Cette galerie photo sera remplie prochainement ...</p>';
	}
	else
	{
		// ce n'est pas un "ami" => un membre de la famille ou un admin
		echo '<img src="photos/mer/DSCN001.jpg">';
		echo '<img src="photos/mer/DSCN002.jpg">';
		echo '<img src="photos/mer/DSCN003.jpg">';
		echo '<img src="photos/mer/DSCN004.jpg">';
		echo '<img src="photos/mer/DSCN005.jpg">';
		echo '<img src="photos/mer/DSCN006.jpg">';
		echo '<img src="photos/mer/DSCN007.jpg">';
	};
?>

Et c'est ainsi que les membre de la famille voient 7 photos inédites... !

Limitations du système

Ce système offre bien des avantages, mais il souffre aussi de quelques limitations ... Par exemple, les photos de la mer ... La session PHP contrôle chaque page Web (PHP) que le navigateur affiche, mais rien n'empêche un "ami" de taper l'adresse directement d'une photo : http://votre_site/photos/mer/DSCN0005.jpg ! Et il verra la photo ... Pour résoudre ce problème, il existe plusieurs moyens, certains sûrs et d'autres moins sûrs :

OK.
Ces 3 premiers conseils sont une base minimale, mais on est loin de la sécurité absolue. Continuons.

<?php
	require('control-session.php'); // session + on vérifie que l'utilisateur est bien connecté
	if ($_SESSION['grade'] == 'ami') // ah non, pas eux !!!
	{
		echo '<p>Cette galerie photo sera remplie prochainement ...</p>';
	}
	else
	{
		// ce n'est pas un "ami" => un membre de la famille ou un admin
		echo '<img src="voir-photo.php?album=mer_perso&photo=DSCN001.jpg">';
		echo '<img src="voir-photo.php?album=mer_perso&photo=DSCN002.jpg">';
		echo '<img src="voir-photo.php?album=mer_perso&photo=DSCN003.jpg">';
		echo '<img src="voir-photo.php?album=mer_perso&photo=DSCN004.jpg">';
		echo '<img src="voir-photo.php?album=mer_perso&photo=DSCN005.jpg">';
		echo '<img src="voir-photo.php?album=mer_perso&photo=DSCN006.jpg">';
		echo '<img src="voir-photo.php?album=mer_perso&photo=DSCN007.jpg">';
	};
?>

Avec la page "voir-photo.php" comme suit :

<?php
	require('control-session.php'); // session + on vérifie que l'utilisateur est bien connecté

	if ($_SESSION['grade'] == 'ami') // ah non, pas eux !!!
		exit(); // pas d'intrus sur cette photo

	// les lignes suivantes ne s'exécutent que si le "exit" n'a pas été utilisé
	$album = (isset($_GET['album'])) ? $_GET['album'] : '';
	$photo = (isset($_GET['photo'])) ? $_GET['photo'] : '';

	// vérifications basiques. On devrait en rajouter d'autres, d'ailleurs.
	$array_test_valeurs = array('', '.', '..'); // valeurs interdites
	if ((in_array($album, $array_test_valeurs)) || (in_array($photo, $array_test_valeurs)))
		exit();

	// on suppose que les vrais fichiers ont été déposés dans /images/photos/32454Z5432543DSFDSFC/mer/
	// de façon à avoir un nom de dossier suffisamment complexe pour ne pas être deviné
	// et de toute façon, nous allons le masquer ...
	$array_albums = array('mer_perso', 'montagne_perso', 'campagne_perso'); // les albums "autorisés"

	// le dossier de stockage des photos persos
	$dossier = '/images/photos/32454Z5432543DSFDSFC/';

	if (!in_array($album, $array_albums)) // l'album demandé par URL n'existe pas dans la liste
		exit(); // hop, fini. Pas d'affichage.

	// on teste la photo
	if (!file_exists($photo, $dossier.$album.'/'))
		exit(); // le fichier photo n'existe pas dans cet album, on arrête le script

	// si on arrive ici, c'est que l'album existe et le fichier photo dans cet album aussi
	// on va donc afficher le contenu de la photo
	// 
	// l'astuce est ici : le visiteur n'est jamais redirigé sur le fichier Jpeg, c'est
	// son contenu qui est lu, puis envoyé au navigateur.
	@ob_end_clean();

	// Set headers
	header("Pragma: public");
	header("Expires: Tue, 1 Dec 2000 00:30:00 GMT");
	header("Cache-Control: must-revalidate, post-check=0, pre-check=0");
	header("Cache-Control: no-store, no-cache, must-revalidate");
	header("Cache-Control: private");
	header("Content-Transfer-Encoding: binary");

	header("Content-Type: image/jpeg");

	header('Content-Disposition: attachment; filename="'.$photo.'"');
	header("Content-Length: ".filesize($dossier.$album.'/'.$photo));

	if (($f = fopen($dossier.$album.'/'.$photo, 'rb')) === false) exit;

	// Push
	while (!feof($f))
	{
		echo fread($f, (1*(1024*1024)));
		flush();
		@ob_flush();
	};

	fclose($f);
	exit;
?>

C'est un degré de sécurité supplémentaire, mais encore loin de l'absolu. En effet, une fois l'image affichée, rien ne prouve que le frère ne fera pas "clic droit, enregistrer sous". (Et par pitié, ne cherchez pas à désactiver le clic droit, c'est inutile !).

En réalité, pour couper court à la discussion, la solution la plus sécurisée (et là, on est dans la sécurité absolue) pour ne pas se faire piquer ses photos (ou autres documents, peu importe leur nature au final) est ... tout simplement de ne pas les mettre sur le serveur ! Même avec un système régulé par .htaccess (cf. Méthode 1 ci-dessus), rien ne prouve dans l'absolu que les photos ne seront jamais accessibles (plantage d'Apache ...)

Déconnexion

La déconnexion (= terminaison de la session) se fait par une destruction du tableau. Concrètement, le moyen le plus simple consiste à appeler une page (par exemple, index.php : cela permet ensuite de se reconnecter s'il le faut) avec une variable dans l'URL : index.php?act=logout. Dans index.php on teste la présence de ce "logout" et si existant, on détruit la session (c'est le même index.php qu'au dessus, avec le paragraphe "logout" rajouté - je suppose donc qu'il y a quelque part un lien défini comme suit : <a href="index.php?act=logout">Déconnexion</a>) :

<?php
session_start();

// Déconnexion
if ((isset($_GET['act'])) && ($_GET['act'] == 'logout'))
{
	$_SESSION = array();
	session_destroy();

	// on relance une session pour une éventuelle reconnexion
	session_start();
};

if (!isset($_POST['submit']))
{
	// c'est la première fois qu'on vient sur cette page, aucun formulaire soumis
	echo '
	<form id="connect" method="post" action="">
	<fieldset><legend>Formulaire de connexion</legend>
		<p><label for="login">Login : </label><input type="text" id="login" name="login" tabindex="1" value="" /></p>
		<p><label for="pwd">Mot de passe :</label><input type="password" id="pwd" name="pwd" tabindex="2" /></p>
	</fieldset>
	<div><input type="submit" name="submit" value="Connexion" tabindex="3" /></div>
	</form>';
}
else
{
	// le formuaire vient d'être soumis
	$login  = (isset($_POST['login'])) ? htmlentities(trim($_POST['login'])) : '';
	$pwd   = (isset($_POST['pwd']))    ? htmlentities(trim($_POST['pwd']))   : '';

	if (($login != '') && ($pwd != ''))
	{
		// Login et pwd non vides, on  vérifie s'il y a quelqu'un qui correspond
		$req_utilisateur = sprintf("SELECT
							nom,
							prenom,
							grade,
							email
						FROM
							utilisateurs
						WHERE
							(login = '%s' AND passwd = '%s')",$login, md5($pwd));
		$utilisateur = mysql_query($req_utilisateur) or die($req_utilisateur."<br />\n".mysql_error());

		if (mysql_num_rows($utilisateur) == 1)
		{
			// Oui il y a quelqu'un ...
			$personne = mysql_fetch_array($utilisateur);

			// On  enregistre ses données dans la session
			$_SESSION['login'] = $login; // permet de vérifier que l'utilisateur est bien connecté
			$_SESSION['nom'] = $personne['nom'];
			$_SESSION['prenom'] = $personne['prenom'];
			$_SESSION['grade'] = $personne['grade'];
			$_SESSION['email'] = $personne['email'];

			// Maintenant que tout est enregistré dans la session, on redirige vers la page des photos
			echo '<p>Vous êtes correctement identifié(e), <a href="liste-photos.php">cliquez ici pour visualiser les photos</a></p>'."\n";
		}
		else
		{
			// Erreur dans le login et / ou dans le mot de passe ...
			echo '<p>Désolé, vous avez peut-être fait une erreur dans la saisie des identifiants, mais votre parcours se finit là ... </p>'."\n";
		}
	}
	else
	{
		// il n'y a personne qui répond à ces 2 identifiants
		echo '<p>Désolé, vous avez peut-être fait une erreur dans la saisie des identifiants, mais votre parcours se finit là ... </p>'."\n";
	};
} // end of (isset($_POST['submit']))
?>

Compléments à cette section membre

Nous n'avons pas exploité le grade "administrateur", pour le moment, ni le profil. Vous pouvez tout à fait créer une page nommée "profil.php" avec un formulaire reprenant le nom, le prénom, l'email. Un bouton "mise à jour" et chaque membre peut mettre à jour son profil. Pour l'administrateur, vous pouvez mettre un lien "voir tous les profils", et il peut si vous codez la fonctionnalité, éditer chaque profil ... Vous voyez que cet article est loin d'être exhaustif, il avait juste pour objectif de présenter la création d'une section membre, mais celle-ci doit être bien pensée à la base pour voir les limites du système (on y passe vite des heures).

Bon courage !