Vamos a mostrar cómo crear un sistema de login utilizando PHP con el driver de PDO (para php5.x) y una base de datos MySQL.
La idea principal es registrar usuarios, loguearlos en el sitio y desloguearlos del mismo de la forma más simple para luego poder proteger áreas con unas simples líneas de código.
Partimos de una base de datos, no muy complicada, desde la consola podemos crearla con la siguientes consultas:

CREATE TABLE `users`.`acccounts` (
`id` INT( 11 ) NOT NULL AUTO_INCREMENT PRIMARY KEY ,
`nick` VARCHAR( 20 ) NOT NULL ,
`email` VARCHAR( 50 ) NOT NULL ,
`password` VARCHAR( 32 ) NOT NULL ,
`salt` CHAR( 6 ) NOT NULL
) ENGINE = MYISAM ;

Bien, nuestra base de datos se llama “users” y dentro tenemos nuestra tabla “accounts” la cual cuenta con una columna id, nick, email, password y salt.
La columna password almacenara la contraseña del usuario concatenada con el salto, todo esto encriptado en MD5, con lo cual conseguimos distintos hashes para mismos passwords (esto evita comparar hashes entre sí o contra alguna lista en internet).
Ok, para intentar hacerlo lo más simple posible, voy a utilizar un archivo con todas las funciones, clases, instancias, etc. y lo incluiré al principio de cada archivo.. evidentemente en un proyecto ordenaríamos el asunto, a modo de funcionar en MVC o como sea.

Bien, iniciamos la sesión y definimos las constantes para la conexión a la base de datos.

1
2
3
4
5
session_start();
defined('DB_HOST')? null : define("DB_HOST", "localhost"); //servidor mysql
defined('DB_USER')? null : define("DB_USER", "root"); // usuario mysql
defined('DB_PASS')? null : define("DB_PASS", ""); // password del usuario mysql
defined('DB_NAME')? null : define("DB_NAME", "users"); // nombre de la base de datos

creamos un objeto $db ,instancia de PDO con los atributos para manejar errores:

1
2
3
4
5
6
try {
$db = new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME, DB_USER, DB_PASS);
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch( PDOException $e ) {
die($e);
}

Nuestra clase Session, la cual manejara métodos para loguear, checar logueo y desloguear a nuestros usuarios.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Class Session{
    public $datos;
    public function logIn($datos){
        $_SESSION['datos']=$datos;
        $_SESSION['status']=true;
    }
    public function logOut(){
        $this->datos=null;
        $_SESSION['status']=false;
       
    }
    public function is_logged_in(){
        if(isset($_SESSION['status'])){
            $this->datos = unserialize($_SESSION['datos']);
            return $_SESSION['status'];
        }return false;     
       
    }
}
$session = new Session();

Creamos una clase Usuario, con métodos logIn y register.
el método register, comprueba que el nickname y el email ya no estén utilizados por algún otro usuario, luego inserta en la base de datos las credenciales si estas son validas.
$result=$result->fetch(PDO::FETCH_ASSOC) almacena en $result un array asociativo con los datos de la primer fila del resultado de la consulta. Para levantar todos los resultados utilizaríamos $result=$result->fetchAll(PDO::FETCH_ASSOC), con FETCH_NUM para recuperar arrays de índice numérico.
$result->rowCount() devuelve la cantidad de filas afectadas por la última consulta SQL, esperamos un 1 para un registro satisfactorio.
catch(PDOException $e) captura excepciones de PDO. Mientras desarrollamos podemos imprimir estos errores en pantalla para depurar, y en producción podemos enviarlos a un archivo de log.

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
public function register($nick, $email, $password, $salt){
        global $db;
        global $session;
        try{
            $result=$db->prepare("SELECT nick, email FROM acccounts WHERE acccounts.nick=:nick OR acccounts.email=:email LIMIT 1");
            $result->bindParam(':nick',$nick);
            $result->bindParam(':email', $email);
            $result->execute();
            $result=$result->fetch(PDO::FETCH_ASSOC);
            if(!empty($result)){
                $msg='';
                if($result['nick']==$nick){ $msg.='<p>Nick name en uso</p>';}
                if($result['email']==$email){ $msg.='<p>Email ya en uso</p>';}
                return $msg;
            }else{
                try{
                    $result=$db->prepare("INSERT INTO acccounts(nick, email, password, salt) VALUES(:nick, :email, :password, :salt)");
                    $result->bindParam(':nick',$nick);
                    $result->bindParam(':email',$email);
                    $result->bindParam(':password',$password);
                    $result->bindParam(':salt',$salt);
                    $result->execute();
                    if($result->rowCount()){
                        $msg='nueva cuenta creada';
                        return $msg;
                    }
               
                }catch(PDOException $e){
                    return false;
                }
            }
        }catch(PDOException $e){
            return false;
        }
    }
$user = new User();

Finalmente una pequeña función para generar saltos al azar, los cual luego concatenaremos al password de nuestro usuario.

1
2
3
4
5
6
7
8
9
10
function randomCH($size){
    $i=0;
    $keyString='';
    while ($i < $size) {
        $str = rand(97, 122);
        $keyString .= chr($str);
        $i++;
    }
    return $keyString;
}

Con esto ya tendríamos para generar registros, loguear, desloguear y chequear usuarios así como acceder a los datos de los mismos.

Aquí cerraríamos nuestro archivo de configuración el cual incluiríamos en todos los archivos que tengan que ver con el manejo de credenciales.
Ahora vamos a crear un índex, con 2 formularios, uno para registrar cuentas y el otro para loguear

1
2
3
4
5
require_once('dbconfig.php');
if($session->is_logged_in()){
    echo 'hello '.$session->datos['nick'].' <a href="logout.php">[ salir ]</a>';
}else{
?>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<div id="login_form">
<form action="login.php" method="POST">
    <p>Nick: <input type="text" name="nick" size="20"/></p>
    <p>Pass: <input type="password" name="password" size="20"/></p>
    <p><input type="submit" name="login" value="Login"/></p>
</form>
</div>
<div id="register_form">
<form action="new_account.php" method="POST">
    <p>Nick: <input type="text" name="nick" size="20"/></p>
    <p>eMail: <input type="text" name="email" size="20"/></p>
    <p>re-pass: <input type="password" name="repass" size="20"/></p>
    <p>Pass: <input type="password" name="password" size="20"/></p>
    <p><input type="submit" name="register" value="Register"/></p>
</form>
</div>
1
<?php } ?>

Utilizando nuestro método $session->is_logged_in(), sabremos si el usuario esta logueado, de ser así, enviamos un saludo junto en enlace al logout y el nombre del usuario levantando desde el array. El método
is_logged_in recoge el estado directo desde la session, y deserializa el array con los datos del usuario (mail y nick en este caso) al cual luego accedemos desde $session->datos['nick']

en caso de no estar logueado, devuelve false y se ejecuta el else en el cual ofrecemos el formulario de login, y también el de registro.

El formulario de login nos lleva a login.php el cual contendría el siguiente código:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
require_once('dbconfig.php');
if(isset($_POST['login'])){
    $nick=isset($_POST['nick'])? trim($_POST['nick']) : '';
    $pass=isset($_POST['password'])? $_POST['password'] : '';
    $err=0;
    $output='';
    if(!preg_match('/^[a-zA-Z0-9]{3,20}$/', $nick)){
        $output.='<p>el nickname ingresado no es valido</p>';
        $err++;
    }
    if(false !== strpos($pass, "\\")){ //pass sin "\"
        $output.='<p>el password no puede contener \\</p>';
        $err++;
    }
    if($err){
        echo $output;
    }else{
        if($msg=$user->logIn($nick, $pass)){
            header('Location: index.php');
        }
    }
}

Aquí requerimos el archivo de configuración con todo el yuyo dentro, y chequeamos los datos que nos devuelven, de la misma forma que lo haremos a la hora de registrar usuarios, con esto nos ahorramos consultar la base de datos si ya sabemos que los datos no deberían de estar presentes.
if(!preg_match(‘/^[a-zA-Z0-9]{3,20}$/’, $nick))
buscamos por un nick compuesto de letras y números, de un mínimo de 3 caracteres a un máximo de 20 (en nuestra SQL 20 fue el máximo de caracteres para el nick del usuario).
if(false !== strpos($pass, “\\”)
con esto chequeamos que el password no contenga una retrobarra “\” podemos agregar los filtros que queramos (máximo mínimo etc.).
cuando estamos ok con los datos enviados, intentamos loguear al usuario:
if($msg=$user->logIn($nick, $pass)){
header(‘Location: index.php’);
}
Aquí es simple y luego lo vamos a modificar, pero por el momento si logramos loguear al usuario (es decir si nos cae algo en $msg distinto de false, re direccionamos al index.php donde el usuario ya estará logueado y vera el mensaje de bienvenida.)
Sin registro, difícil tendremos usuarios, así que vamos a registrar algunos..
Nuestro formulario de registro, envía por métodos post a new_account.php y de esta forma lo registramos:

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
require_once('dbconfig.php');
if(isset($_POST['register'])){
    $nick=isset($_POST['nick'])? trim($_POST['nick']) : '';
    $email=isset($_POST['email'])? trim($_POST['email']) : '';
    $pass=isset($_POST['password'])? $_POST['password'] : '';
    $repass=isset($_POST['repass'])? $_POST['repass'] : '';
    $output='';
    $err=0;
    if(!preg_match('/^[a-zA-Z0-9]{3,20}$/', $nick)){
        $output.='<p>el nickname ingresado no es valido</p>';
        $err++;
    }
    if(!filter_var($email, FILTER_VALIDATE_EMAIL)){
        $output.='<p>el email ingresado no es valido</p>';
        $err++;
    }
    if(false !== strpos($pass, "\\")){ //pass sin "\"
        $output.='<p>el password no puede contener \\</p>';
        $err++;
    }
    if($pass!=$repass){
        $output.='<p>las contraseñas no coinciden</p>';
        $err++;
    }
   
    if($err){
        echo $output;
    }else{
        $salt=randomCH(6);
        $pass=md5($pass.$salt);
        if($msg=$user->register($nick, $email, $pass, $salt)){
            echo $msg;
        }
    }
}

En este caso validamos el mail de igual forma que en login.php, y para el mail utilizamos un filtro:
(!filter_var($email, FILTER_VALIDATE_EMAIL)
no es una genialidad, se puede hacer algo más elaborado pero para este ejemplo nos valdrá.
La contraseña y la verificación de la misma deben ser iguales así que:
if($pass!=$repass){
$output.=’

passwords no coinciden

‘;
$err++;
}
si no hay errores registramos al usuario:
$salt=randomCH(6);
$pass=md5($pass.$salt);
if($msg=$user->register($nick, $email, $pass, $salt)){
echo $msg;
utilizando la función randomCH, pasándole 6 como parámetro (es la cantidad de caracteres para el salto en nuestra SQL)
concatenamos con el password del usuario y creamos un hash en md5
y luego, registramos al usuario e imprimimos el mensaje si nos llega.

Un archivo logout para deshacernos de la gente:

1
2
3
require_once('dbconfig.php');
$session->logOut();
header('Location: index.php');

Ok con eso ya tendríamos, nos valdría colocar al inicio de las paginas que requieran login el siguiente código:
if(!$session->is_logged_in()){
header(‘Location: login.html);
die();
}
aparte claro de incluir nuestro archivo de configuración para poder manejar sessiones y acceder a los métodos.
Bien…. bien feo pero bueno, la idea de este ejemplo es, aparte de ilustrar una forma para loguear usuarios (se puede complicar más, se puede utilizar otra encriptación etc. etc.), pero con este código el cual lo dejo para que lo bajen, en el próximo post, o en alguno de los próximos lo voy a retomar para mostrar una forma de loguear, deslogear y registrar usuarios utilizando Ajax, con jQuery y jSON.

Descargar archivos de ejemplo:
http://hotfile.com/dl/23457316/a5fd117/login.zip.html

Mas informacion:
http://www.php.net/manual/en/class.pdo.php