Creación de una utilidad de recuperación de contraseña avanzada
Spanish (Español) translation by Luis Chiabrera (you can also view the original English article)
En mi último tutorial, 'Un mejor sistema de inicio de sesión', algunas personas comentaron cómo les gustaría ver un tutorial sobre la recuperación de contraseñas, que es algo que no siempre se ve en los tutoriales de acceso para usuarios. El tutorial que te traigo hoy trata de eso. Usando mySQLi, aprenderemos a recuperar contraseñas no cifradas y cifradas (unidireccionales).
Introducción
La recuperación de la contraseña es siempre una característica útil para tener en su sitio; sin embargo, muchos tutoriales que tratan la autenticación de usuarios descuidan este tema. En este tutorial, cubriré el manejo de contraseñas cifradas y no cifradas, funciones básicas de mySQLi, y también crearemos un bloqueo temporal si el usuario responde la pregunta de seguridad de manera incorrecta muchas veces. Espero que para el final de este tutorial, tenga una mejor comprensión de cómo recuperar contraseñas y la interfaz mySQLi.
Para las contraseñas no cifradas, crearemos un correo electrónico que enviará la contraseña a la dirección de correo electrónico registrada del usuario.
Cuando se trata de contraseñas encriptadas, las cosas son un poco más complicadas. Cuando una contraseña se cifra con md5 (), no se puede descifrar. Debido a esto, le enviaremos al usuario un correo electrónico con un enlace que le permitirá restablecer su contraseña. Para asegurar este proceso, utilizaremos una clave de correo electrónico única que se verificará cuando el usuario vuelva a la página de contraseña. Nuestras contraseñas serán encriptadas con una sal para mejorar la seguridad.
NOTA IMPORTANTE: Este tutorial usa la interfaz mysqli en lugar de mysql. Aunque la documentación de PHP dice que debería funcionar, no pude usar las clases mysqli correctamente hasta que actualicé la versión 5.1.4 a 5.2.9. Si recibe errores sobre el uso de un 'búfer no compatible', intente la actualización. También es posible que deba cambiar el archivo php.ini para cargar la extensión mysqli si ve errores al no encontrar la clase mysqli.
Dependiendo de si su sitio utiliza contraseñas de usuario cifradas o no cifradas, debe seguir las instrucciones de manera un poco diferente. Las instrucciones en su conjunto son para contraseñas encriptadas; pero hay notas especiales aquí y allá para tratar con las contraseñas regulares.
Paso 1: Tablas de base de datos
Después de crear una nueva base de datos, necesitamos crear tres tablas:


La tabla recoveryemails_enc contiene información sobre los correos electrónicos que se envían para permitir a los usuarios restablecer las contraseñas (como la clave de seguridad, el ID de usuario y la fecha de caducidad). Las tablas de usuarios contienen la información del usuario. Si está siguiendo el ejemplo encriptado, use la tabla users_enc, de lo contrario, use los usuarios de la tabla.
Si ya tiene una tabla de usuarios, deberá agregar campos de preguntas y respuestas de seguridad. El campo de pregunta contendrá un número entero que equivale a una pregunta de seguridad en una matriz, y el campo de respuesta contiene una respuesta varchar. La pregunta de seguridad se utiliza como verificación antes de enviar una contraseña por correo electrónico. Aquí está el código que debe ejecutar para crear las tablas (también disponible en sql.txt en la descarga):
1 |
|
2 |
CREATE TABLE IF NOT EXISTS `recoveryemails_enc` ( |
3 |
`ID` bigint(20) unsigned zerofill NOT NULL auto_increment, |
4 |
`UserID` bigint(20) NOT NULL, |
5 |
`Key` varchar(32) NOT NULL, |
6 |
`expDate` datetime NOT NULL, |
7 |
PRIMARY KEY (`ID`) |
8 |
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=1 ; |
9 |
CREATE TABLE IF NOT EXISTS `users` ( |
10 |
`ID` bigint(20) unsigned zerofill NOT NULL auto_increment, |
11 |
`Username` varchar(20) NOT NULL, |
12 |
`Email` varchar(255) NOT NULL, |
13 |
`Password` varchar(20) NOT NULL, |
14 |
`secQ` tinyint(4) NOT NULL, |
15 |
`secA` varchar(30) NOT NULL, |
16 |
PRIMARY KEY (`ID`) |
17 |
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=5 ; |
18 |
INSERT INTO `users` (`ID`, `Username`, `Email`, `Password`, `secQ`, `secA`) VALUES (00000000000000000002, 'jDoe', 'jDoe@gmail.com', 'johnDoe2009', 0, 'Smith'), |
19 |
(00000000000000000003, 'envato', 'webmaster@envato.com', 'envatouser', 1, 'Sydney'), |
20 |
(00000000000000000004, 'sToaster', 'toast@yahoo.com', 'toastrules', 3, '2001'); |
21 |
CREATE TABLE IF NOT EXISTS `users_enc` ( |
22 |
`ID` bigint(20) unsigned zerofill NOT NULL auto_increment, |
23 |
`Username` varchar(20) NOT NULL, |
24 |
`Password` char(32) NOT NULL, |
25 |
`Email` varchar(255) NOT NULL, |
26 |
`secQ` tinyint(4) NOT NULL default '0', |
27 |
`secA` varchar(32) NOT NULL, |
28 |
PRIMARY KEY (`ID`) |
29 |
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=4 ; |
30 |
INSERT INTO `users_enc` (`ID`, `Username`, `Password`, `Email`, `secQ`, `secA`) VALUES (00000000000000000001, 'jDoe', 'fd2ba57673c57ac5a0650c38fe60b648', 'jDoe@gmail.com', 0, 'Smith'), |
31 |
(00000000000000000002, 'envato', '1ecc663314777c8e3c2328027447f194', 'webmaster@envato.com', 1, 'Sydney'), |
32 |
(00000000000000000003, 'sToaster', 'e05fd29cbca7ea9add48ba6dafc300e8', 'toast@yahoo.com', 3, '2001'); |
Paso 2: Incluir base de datos


Necesitamos crear un archivo de inclusión para poder conectarnos a nuestra base de datos. Para la interacción de nuestra base de datos, utilizaremos mysqli, que proporciona un enfoque orientado a objetos para la interacción de la base de datos. Crea un archivo llamado asset / php / database.php. Le agregaremos el siguiente código (reemplace los valores de las variables con la información apropiada para su situación de alojamiento):
1 |
|
2 |
<?php
|
3 |
session_start(); |
4 |
ob_start(); |
5 |
$hasDB = false; |
6 |
$server = 'localhost'; |
7 |
$user = 'user'; |
8 |
$pass = 'password'; |
9 |
$db = 'db'; |
10 |
$mySQL = new mysqli($server,$user,$pass,$db); |
11 |
if ($mySQL->connect_error) |
12 |
{
|
13 |
die('Connect Error (' . $mySQL->connect_errno . ') '. $mySQL->connect_error); |
14 |
}
|
15 |
|
16 |
?>
|
En la primera línea del código, llamamos session_start (), no usaremos realmente las variables de sesión, pero necesitará una sesión como parte de un sistema de inicio de sesión de usuario. Luego, llamamos a ob_start () para iniciar el búfer de salida.
Las líneas 4-8 configuran nuestras variables para conectarse al servidor. Luego creamos un nuevo objeto mysqli pasándole nuestras variables. Para el resto de nuestro script, $ mySQL nos permitirá interactuar con la base de datos según sea necesario.
Mysqli maneja los eventos de error de forma ligeramente diferente, por lo que tenemos que verificar la propiedad connect_error después de la conexión e imprimir un mensaje de error si algo salió mal. Esto es lo mismo para hacer consultas; debemos verificar la propiedad de error de nuestra consulta u objeto de conexión para verificar errores.
Paso 3: Crear la página de contraseña olvidada
Comencemos este paso creando el archivo forgotPass.php. Entonces, le agregaremos este código:
1 |
|
2 |
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
|
3 |
<html>
|
4 |
<head>
|
5 |
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"> |
6 |
<title>Password Recovery</title> |
7 |
<link href="assets/css/styles.css" rel="stylesheet" type="text/css"> |
8 |
</head>
|
9 |
<body>
|
10 |
<div id="header"></div> |
11 |
<div id="page"> |
12 |
<!--PAGE CONTENT-->
|
13 |
</div>
|
14 |
</body>
|
15 |
</html>
|
Este html nos da una buena base para construir y nos da esto:


Luego, agregue este código en la parte superior de la página, sobre el html que acabamos de ingresar:
1 |
|
2 |
<?php
|
3 |
include("assets/php/database.php"); |
4 |
include("assets/php/functions.php"); |
5 |
$show = 'emailForm'; //which form step to show by default |
6 |
if ($_SESSION['lockout'] == true && (mktime() > $_SESSION['lastTime'] + 900)) |
7 |
{
|
8 |
$_SESSION['lockout'] = false; |
9 |
$_SESSION['badCount'] = 0; |
10 |
}
|
11 |
if (isset($_POST['subStep']) && !isset($_GET['a']) && $_SESSION['lockout'] != true) |
12 |
{
|
13 |
switch($_POST['subStep']) |
14 |
{
|
15 |
case 1: |
16 |
//we just submitted an email or username for verification
|
17 |
$result = checkUNEmail($_POST['uname'],$_POST['email']); |
18 |
if ($result['status'] == false ) |
19 |
{
|
20 |
$error = true; |
21 |
$show = 'userNotFound'; |
22 |
} else { |
23 |
$error = false; |
24 |
$show = 'securityForm'; |
25 |
$securityUser = $result['userID']; |
26 |
}
|
27 |
break; |
28 |
case 2: |
29 |
//we just submitted the security question for verification
|
30 |
if ($_POST['userID'] != "" && $_POST['answer'] != "") |
31 |
{
|
32 |
$result = checkSecAnswer($_POST['userID'],$_POST['answer']); |
33 |
if ($result == true) |
34 |
{
|
35 |
//answer was right
|
36 |
$error = false; |
37 |
$show = 'successPage'; |
38 |
$passwordMessage = sendPasswordEmail($_POST['userID']); |
39 |
$_SESSION['badCount'] = 0; |
40 |
} else { |
41 |
//answer was wrong
|
42 |
$error = true; |
43 |
$show = 'securityForm'; |
44 |
$securityUser = $_POST['userID']; |
45 |
$_SESSION['badCount']++; |
46 |
}
|
47 |
} else { |
48 |
$error = true; |
49 |
$show = 'securityForm'; |
50 |
}
|
51 |
break; |
52 |
case 3: |
53 |
//we are submitting a new password (only for encrypted)
|
54 |
if ($_POST['userID'] == '' || $_POST['key'] == '') header("location: login.php"); |
55 |
if (strcmp($_POST['pw0'],$_POST['pw1']) != 0 || trim($_POST['pw0']) == '') |
56 |
{
|
57 |
$error = true; |
58 |
$show = 'recoverForm'; |
59 |
} else { |
60 |
$error = false; |
61 |
$show = 'recoverSuccess'; |
62 |
updateUserPassword($_POST['userID'],$_POST['pw0'],$_POST['key']); |
63 |
}
|
64 |
break; |
65 |
}
|
66 |
}
|
Lo primero que hacemos es incluir el archivo de base de datos y un archivo de funciones que crearemos en breve. Luego creamos una variable llamada $ show que determina qué elemento de la interfaz mostrar, por defecto queremos mostrar el formulario de entrada de correo electrónico.
Después de eso, comprobamos algunas variables de sesión. Si la variable de bloqueo de sesión es verdadera, y han transcurrido más de 900 segundos (15 minutos) desde que se realizó el bloqueo, queremos finalizar el bloqueo, lo que las líneas 7 y 8 hacen por nosotros.
Luego creamos un bloque if que verifica si se ha publicado algo en la página. También puede ver que estamos revisando para asegurarnos de que $ _GET ['a'] no esté configurado, esta variable se configurará cuando el usuario haga clic en el enlace en su correo electrónico de recuperación, por lo que si eso sucede, podemos omitir cheque posterior También en el bloque lógico, nos aseguramos de que la variable de bloqueo no esté configurada como verdadera.
Una vez dentro del bloque if, usamos switch($ _ POST ['subStep')) para verificar qué etapa del formulario se ha enviado. Trataremos tres etapas: el Paso 1 significa que acabamos de ingresar un nombre de usuario o una dirección de correo electrónico para la cual queremos restablecer la contraseña. Para ello, llamamos a la función checkUNEmail (), que escribiremos momentáneamente. Esta función devuelve una matriz que tiene un valor booleano para determinar si se encontró el usuario y un entero del ID de usuario si se encontró el usuario. La línea 17 verifica si la verificación del usuario devolvió el valor falso (el usuario no fue encontrado), si es así, establecimos el indicador de error y la variable $ show para que veamos el mensaje "Usuario no encontrado". Si se encontró al usuario, deberíamos mostrar el formulario de pregunta de seguridad y establecer $ securityUser para que sepamos para qué usuario cargar la pregunta.
En la etapa 2, hemos presentado la pregunta de seguridad. Lo primero que hacemos aquí es verificar que se haya devuelto un ID de usuario y una respuesta. Si se omite alguno de los dos, volvemos a mostrar el formulario de pregunta de seguridad. Si se incluyeron ambos valores, llamamos a una función para verificar la respuesta en la base de datos. Esta función devuelve un simple booleano. Si la pregunta fue respondida correctamente, mostraremos la página de éxito, enviaremos el correo electrónico de restablecimiento y estableceremos el recuento de entradas incorrectas en 0. Si la pregunta fue respondida incorrectamente, volvemos a mostrar la página de preguntas y también incrementamos el contador de respuestas incorrectas.
La tercera etapa ocurre después de que el usuario haya recibido el correo electrónico de restablecimiento de contraseña, y haya hecho clic en el enlace y enviado el formulario de restablecimiento de contraseña. Primero, si el ID de usuario o la clave de seguridad están vacías, redirigimos a la página de inicio. Luego, verificamos que las dos contraseñas coincidan y no estén vacías. Si no coinciden, volvemos a mostrar el formulario de restablecimiento de contraseña. Sin embargo, si coinciden, mostramos el mensaje de éxito y llamamos a una función para restablecer la contraseña del usuario.
NOTA: Si está utilizando contraseñas no cifradas, puede omitir el caso 3 anterior. Este es el método que guarda una nueva contraseña después de que se haya restablecido. Ya que podemos enviar contraseñas no cifradas por correo electrónico, no es necesario restablecerlas.
Ahora agregaremos un bloque else a ese mismo bloque de arriba:
1 |
|
2 |
elseif (isset($_GET['a']) && $_GET['a'] == 'recover' && $_GET['email'] != "") { |
3 |
$show = 'invalidKey'; |
4 |
$result = checkEmailKey($_GET['email'],urldecode(base64_decode($_GET['u']))); |
5 |
if ($result == false) |
6 |
{
|
7 |
$error = true; |
8 |
$show = 'invalidKey'; |
9 |
} elseif ($result['status'] == true) { |
10 |
$error = false; |
11 |
$show = 'recoverForm'; |
12 |
$securityUser = $result['userID']; |
13 |
}
|
14 |
}
|
15 |
if ($_SESSION['badCount'] >= 3) |
16 |
{
|
17 |
$show = 'speedLimit'; |
18 |
$_SESSION['lockout'] = true; |
19 |
$_SESSION['lastTime'] = '' ? mktime() : $_SESSION['lastTime']; |
20 |
}
|
21 |
?>
|
Este bloque del otro lado determina si se establece $ _GET ['a'], lo que significa que el usuario ha llegado a la página a través del enlace de restablecimiento de correo electrónico. Si hemos descubierto que hicieron clic en el enlace, queremos comprobar si la clave de correo electrónico es válida mediante checkEmailKey (). Si la clave no se encuentra o no es válida, devuelve booleano falso y mostramos el mensaje de clave no válida. Sin embargo, si la clave es válida, devuelve una matriz de la que podemos extraer el ID de usuario (para saber qué contraseña de usuario restablecer).
NOTA: Si está utilizando contraseñas no cifradas, puede omitir el bloque elseif. Se trata de mostrar el formulario de cambio de contraseña después de hacer clic en el enlace en el correo electrónico.
El segundo bloque si se comprueba para ver si el usuario ha respondido la pregunta de seguridad incorrectamente más de 3 veces. Si es así, establecemos la variable de bloqueo en verdadero y establecemos el tiempo de bloqueo, que es el momento en que el bloqueo entró en vigencia. Tenga en cuenta que si el tiempo de bloqueo ya está establecido, usaremos esa variable, pero si no, generaremos un nuevo tiempo.
Establecer un límite de velocidad es importante para la seguridad. Sobre todo, servirá para disuadir a las personas de simplemente sentarse allí y tratar de adivinar la respuesta a las preguntas de seguridad. También puede ayudar a evitar que los robots envíen el formulario una y otra vez. El principal inconveniente de este método es que un usuario puede simplemente eliminar la cookie PHPSESSID y luego comenzar de nuevo. Si desea una solución muy segura, puede agregar un indicador de 'bloqueo' a la tabla de usuarios y, cuando hayan respondido incorrectamente 3 veces, establezca ese indicador en verdadero. Luego, puede escribir algún código de bloqueo si intentan acceder a la cuenta.
Paso 4: Archivo de funciones


Ahora vamos a crear el archivo asset / php / functions.php para contener todas las funciones a las que nos referíamos anteriormente. Ahora vamos a repasarlos unos a otros:
NOTA: En los archivos de muestra, el ejemplo encriptado usa la tabla 'users_enc', mientras que el ejemplo sin cifrar usa la tabla 'users', por lo que el nombre de la tabla cambiará en sus funciones dependiendo de la versión que use.
1 |
|
2 |
<?php
|
3 |
define(PW_SALT,'(+3%_'); |
4 |
|
5 |
function checkUNEmail($uname,$email) |
6 |
{
|
7 |
global $mySQL; |
8 |
$error = array('status'=>false,'userID'=>0); |
9 |
if (isset($email) && trim($email) != '') { |
10 |
//email was entered
|
11 |
if ($SQL = $mySQL->prepare("SELECT `ID` FROM `users_enc` WHERE `Email` = ? LIMIT 1")) |
12 |
{
|
13 |
$SQL->bind_param('s',trim($email)); |
14 |
$SQL->execute(); |
15 |
$SQL->store_result(); |
16 |
$numRows = $SQL->num_rows(); |
17 |
$SQL->bind_result($userID); |
18 |
$SQL->fetch(); |
19 |
$SQL->close(); |
20 |
if ($numRows >= 1) return array('status'=>true,'userID'=>$userID); |
21 |
} else { return $error; } |
22 |
} elseif (isset($uname) && trim($uname) != '') { |
23 |
//username was entered
|
24 |
if ($SQL = $mySQL->prepare("SELECT `ID` FROM `users_enc` WHERE Username = ? LIMIT 1")) |
25 |
{
|
26 |
$SQL->bind_param('s',trim($uname)); |
27 |
$SQL->execute(); |
28 |
$SQL->store_result(); |
29 |
$numRows = $SQL->num_rows(); |
30 |
$SQL->bind_result($userID); |
31 |
$SQL->fetch(); |
32 |
$SQL->close(); |
33 |
if ($numRows >= 1) return array('status'=>true,'userID'=>$userID); |
34 |
} else { return $error; } |
35 |
} else { |
36 |
//nothing was entered;
|
37 |
return $error; |
38 |
}
|
39 |
}
|
En la parte superior del archivo, queremos definir PW_SALT, que es la sal que usaremos para cifrar las contraseñas en todo. La primera función toma los valores de nombre de usuario y contraseña de $ _POST y ve si existe un usuario coincidente en la base de datos. Lo primero que debemos hacer es hacer $ mySQL global para poder acceder a la base de datos. También creamos una matriz de error genérica que devolveremos si no se encuentra ningún usuario. Luego verificamos si hay un valor para el correo electrónico. Si existiera, construimos una declaración preparada mySQLi. Lo mejor de usar declaraciones preparadas es la seguridad, especialmente cuando se trata de la entrada del usuario, el proceso general es similar al uso de sprintf ().
Observe que llamamos al método -> prepare () con nuestra consulta SQL como parámetro, y usamos signos de interrogación donde irían nuestras variables. También tenga en cuenta que no ponemos ninguna forma de comillas alrededor de los signos de interrogación (incluso para cadenas). Esto es lo que nos permite parametrizar una declaración. mySQLi :: prepare () devolverá verdadero si la declaración se creó correctamente. Si se creó, debemos vincular los parámetros a nuestra consulta utilizando mySQLi :: bind_param (). Esta función acepta al menos dos argumentos. La primera es una cadena de letras que representan los tipos de datos de los datos a enlazar. El resto de los argumentos son las variables que desea que estén en cada parámetro. En nuestro caso, estamos usando 's' (para una cadena) y $ email, ya que esa variable tiene la dirección de correo electrónico. Tenga en cuenta que el orden es importante, por lo que el primer signo de interrogación en su consulta corresponde a la primera letra en la cadena de formato y la primera variable.
Luego necesitamos llamar a -> execute () y -> store_result (). Estos dos métodos ejecutan la consulta preparada y la almacenan en la memoria hasta que se libera. Luego, verificamos el número de filas devueltas para asegurarnos de que se haya encontrado al usuario. -> bind_result () es muy similar a -> bind_param () excepto en la dirección opuesta. Nos permite tomar un valor devuelto de un resultado y asignarlo a una variable local. Las variables se asignan en función de su orden en el conjunto de resultados. La variable local no se escribe realmente hasta que llamamos -> fetch (), después de que llamemos -> fetch (), la variable $ userID mantendrá la variable de la base de datos. Luego usamos -> close () en la consulta para liberar el resultado. Por último, devolveremos una matriz que contiene el valor booleano para el éxito y un entero para el ID de usuario.
1 |
|
2 |
function getSecurityQuestion($userID) |
3 |
{
|
4 |
global $mySQL; |
5 |
$questions = array(); |
6 |
$questions[0] = "What is your mother's maiden name?"; |
7 |
$questions[1] = "What city were you born in?"; |
8 |
$questions[2] = "What is your favorite color?"; |
9 |
$questions[3] = "What year did you graduate from High School?"; |
10 |
$questions[4] = "What was the name of your first boyfriend/girlfriend?"; |
11 |
$questions[5] = "What is your favorite model of car?"; |
12 |
if ($SQL = $mySQL->prepare("SELECT `secQ` FROM `users_enc` WHERE `ID` = ? LIMIT 1")) |
13 |
{
|
14 |
$SQL->bind_param('i',$userID); |
15 |
$SQL->execute(); |
16 |
$SQL->store_result(); |
17 |
$SQL->bind_result($secQ); |
18 |
$SQL->fetch(); |
19 |
$SQL->close(); |
20 |
return $questions[$secQ]; |
21 |
} else { |
22 |
return false; |
23 |
}
|
24 |
}
|
25 |
|
26 |
function checkSecAnswer($userID,$answer) |
27 |
{
|
28 |
global $mySQL; |
29 |
if ($SQL = $mySQL->prepare("SELECT `Username` FROM `users_enc` WHERE `ID` = ? AND LOWER(`secA`) = ? LIMIT 1")) |
30 |
{
|
31 |
$answer = strtolower($answer); |
32 |
$SQL->bind_param('is',$userID,$answer); |
33 |
$SQL->execute(); |
34 |
$SQL->store_result(); |
35 |
$numRows = $SQL->num_rows(); |
36 |
$SQL->close(); |
37 |
if ($numRows >= 1) { return true; } |
38 |
} else { |
39 |
return false; |
40 |
}
|
41 |
}
|
getSecurityQuestion () acepta el ID de un usuario y devuelve su pregunta de seguridad como una cadena. Una vez más, hacemos $ mySQL global. Luego creamos una matriz de las 6 diferentes preguntas de seguridad posibles. Usando un método similar al anterior, vemos qué pregunta de seguridad ha seleccionado el usuario y luego devolvemos ese índice de la matriz.
checkSecAnswer () acepta un ID de usuario y una cadena de respuesta y comprueba la base de datos para ver si es correcta. Tenga en cuenta que estamos convirtiendo la respuesta pasada y la respuesta de la base de datos en minúsculas para aumentar las posibilidades de una coincidencia (opcional para usted). Este es un excelente ejemplo de una declaración preparada de múltiples parámetros. Observe el orden de los argumentos en el método bind_param (). Esta función devolverá verdadero si encuentra un registro donde el ID de usuario y la respuesta coincidan con lo que se pasó. De lo contrario, devolverá falso.
1 |
|
2 |
function sendPasswordEmail($userID) |
3 |
{
|
4 |
global $mySQL; |
5 |
if ($SQL = $mySQL->prepare("SELECT `Username`,`Email`,`Password` FROM `users_enc` WHERE `ID` = ? LIMIT 1")) |
6 |
{
|
7 |
$SQL->bind_param('i',$userID); |
8 |
$SQL->execute(); |
9 |
$SQL->store_result(); |
10 |
$SQL->bind_result($uname,$email,$pword); |
11 |
$SQL->fetch(); |
12 |
$SQL->close(); |
13 |
$expFormat = mktime(date("H"), date("i"), date("s"), date("m") , date("d")+3, date("Y")); |
14 |
$expDate = date("Y-m-d H:i:s",$expFormat); |
15 |
$key = md5($uname . '_' . $email . rand(0,10000) .$expDate . PW_SALT); |
16 |
if ($SQL = $mySQL->prepare("INSERT INTO `recoveryemails_enc` (`UserID`,`Key`,`expDate`) VALUES (?,?,?)")) |
17 |
{
|
18 |
$SQL->bind_param('iss',$userID,$key,$expDate); |
19 |
$SQL->execute(); |
20 |
$SQL->close(); |
21 |
$passwordLink = "<a href=\"?a=recover&email=" . $key . "&u=" . urlencode(base64_encode($userID)) . "\">http://www.oursite.com/forgotPass.php?a=recover&email=" . $key . "&u=" . urlencode(base64_encode($userID)) . "</a>"; |
22 |
$message = "Dear $uname,\r\n"; |
23 |
$message .= "Please visit the following link to reset your password:\r\n"; |
24 |
$message .= "-----------------------\r\n"; |
25 |
$message .= "$passwordLink\r\n"; |
26 |
$message .= "-----------------------\r\n"; |
27 |
$message .= "Please be sure to copy the entire link into your browser. The link will expire after 3 days for security reasons.\r\n\r\n"; |
28 |
$message .= "If you did not request this forgotten password email, no action is needed, your password will not be reset as long as the link above is not visited. However, you may want to log into your account and change your security password and answer, as someone may have guessed it.\r\n\r\n"; |
29 |
$message .= "Thanks,\r\n"; |
30 |
$message .= "-- Our site team"; |
31 |
$headers .= "From: Our Site <webmaster@oursite.com> \n"; |
32 |
$headers .= "To-Sender: \n"; |
33 |
$headers .= "X-Mailer: PHP\n"; // mailer |
34 |
$headers .= "Reply-To: webmaster@oursite.com\n"; // Reply address |
35 |
$headers .= "Return-Path: webmaster@oursite.com\n"; //Return Path for errors |
36 |
$headers .= "Content-Type: text/html; charset=iso-8859-1"; //Enc-type |
37 |
$subject = "Your Lost Password"; |
38 |
@mail($email,$subject,$message,$headers); |
39 |
return str_replace("\r\n","<br/ >",$message); |
40 |
}
|
41 |
}
|
42 |
}
|
Esta función se encarga de enviar el correo electrónico para restablecer una contraseña. Comenzamos haciendo una consulta SQL para obtener el nombre de usuario y la dirección de correo electrónico del usuario aprobado. Después de vincular los parámetros y el resultado, cerramos la consulta. Por razones de seguridad, solo queremos que el enlace que generemos sea válido durante 3 días. Para ello, creamos una nueva fecha que será de 3 días en el futuro. A través de varios de los valores que se pasan, la fecha, un número aleatorio y nuestra sal, generamos un hash MD5 que será nuestra clave de seguridad. Debido a cada cambio de fecha futura y número aleatorio, debemos asegurarnos una clave completamente aleatoria cada vez. Luego construimos una consulta SQL para insertarla en la base de datos. Después de ejecutar la consulta, hacemos el enlace que será enviado en el correo electrónico. Queremos incluir 'a = recover' y nuestra clave, y el ID de usuario que base64_encode () y urlencode () para que no sea legible. Una vez que se genera el enlace, hacemos el resto de nuestro correo electrónico y lo enviamos.
NOTA: Si está utilizando el ejemplo sin cifrar, desea cambiar el texto de su mensaje para imprimir la variable $ pword en lugar de un enlace para restablecer la contraseña.
NOTA: Si está desarrollando esto en un servidor local, es poco probable que tenga instalado smtp, por lo que no podrá enviar correo. Es por esto que estamos usando el comando @mail () (para evitar mensajes de error). También por esa razón, esta función devuelve la cadena del mensaje para que pueda imprimirse en la pantalla. Ahora para las últimas 3 funciones:
1 |
|
2 |
function checkEmailKey($key,$userID) |
3 |
{
|
4 |
global $mySQL; |
5 |
$curDate = date("Y-m-d H:i:s"); |
6 |
if ($SQL = $mySQL->prepare("SELECT `UserID` FROM `recoveryemails_enc` WHERE `Key` = ? AND `UserID` = ? AND `expDate` >= ?")) |
7 |
{
|
8 |
$SQL->bind_param('sis',$key,$userID,$curDate); |
9 |
$SQL->execute(); |
10 |
$SQL->execute(); |
11 |
$SQL->store_result(); |
12 |
$numRows = $SQL->num_rows(); |
13 |
$SQL->bind_result($userID); |
14 |
$SQL->fetch(); |
15 |
$SQL->close(); |
16 |
if ($numRows > 0 && $userID != '') |
17 |
{
|
18 |
return array('status'=>true,'userID'=>$userID); |
19 |
}
|
20 |
}
|
21 |
return false; |
22 |
}
|
23 |
|
24 |
function updateUserPassword($userID,$password,$key) |
25 |
{
|
26 |
global $mySQL; |
27 |
if (checkEmailKey($key,$userID) === false) return false; |
28 |
if ($SQL = $mySQL->prepare("UPDATE `users_enc` SET `Password` = ? WHERE `ID` = ?")) |
29 |
{
|
30 |
$password = md5(trim($password) . PW_SALT); |
31 |
$SQL->bind_param('si',$password,$userID); |
32 |
$SQL->execute(); |
33 |
$SQL->close(); |
34 |
$SQL = $mySQL->prepare("DELETE FROM `recoveryemails_enc` WHERE `Key` = ?"); |
35 |
$SQL->bind_param('s',$key); |
36 |
$SQL->execute(); |
37 |
}
|
38 |
}
|
39 |
|
40 |
function getUserName($userID) |
41 |
{
|
42 |
global $mySQL; |
43 |
if ($SQL = $mySQL->prepare("SELECT `Username` FROM `users_enc` WHERE `ID` = ?")) |
44 |
{
|
45 |
$SQL->bind_param('i',$userID); |
46 |
$SQL->execute(); |
47 |
$SQL->store_result(); |
48 |
$SQL->bind_result($uname); |
49 |
$SQL->fetch(); |
50 |
$SQL->close(); |
51 |
}
|
52 |
return $uname; |
53 |
}
|
La primera función comprueba la clave de correo electrónico que se pasó. Hacemos una cadena de fecha para representar la fecha de hoy (para saber si la clave está vencida). Luego, nuestro SQL comprueba si existe algún registro para una clave que coincida con el usuario aprobado, la clave de seguridad, y que la fecha de caducidad sea posterior a la actual. Si existe una clave de este tipo, devolvemos una matriz con el ID de usuario en ella, o falso en caso contrario.
La función updateUserPassword () se encarga de cambiar la contraseña real en la base de datos. Primero verificamos la clave de correo electrónico contra la información de usuario y clave pasada para asegurarnos de que las cosas estén seguras. Generamos una nueva contraseña combinando la contraseña pasada y el salt definido previamente, y luego ejecutando md5 () en esa cadena. Luego realizamos nuestra actualización. Una vez que se completa, eliminamos el registro de la clave de recuperación de la base de datos para que no se pueda usar nuevamente.
NOTA: La función updateUserPassword () no es necesaria en el ejemplo sin cifrar, ya que no estamos cambiando su contraseña.
getUserName () es simplemente una función de utilidad para obtener el nombre de usuario del ID de usuario validado. Utiliza una sentencia SQL básica como las demás.
Paso 5: Completa la página de contraseña olvidada
Ahora que hemos creado las funciones que necesitamos, agregaremos algunas más a forgotPass.php. Agregue este código dentro del div con la id "page":
1 |
<?php switch($show) { |
2 |
case 'emailForm': ?> |
3 |
<h2>Password Recovery</h2> |
4 |
<p>You can use this form to recover your password if you have forgotten it. Because your password is securely encrypted in our database, it is impossible actually recover your password, but we will email you a link that will enable you to reset it securely. Enter either your username or your email address below to get started.</p> |
5 |
<?php if ($error == true) { ?><span class="error">You must enter either a username or password to continue.</span><?php } ?> |
6 |
<form action="<?= $_SERVER['PHP_SELF']; ?>" method="post"> |
7 |
<div class="fieldGroup"><label for="uname">Username</label><div class="field"><input type="text" name="uname" id="uname" value="" maxlength="20"></div></div> |
8 |
<div class="fieldGroup"><label>- OR -</label></div> |
9 |
<div class="fieldGroup"><label for="email">Email</label><div class="field"><input type="text" name="email" id="email" value="" maxlength="255"></div></div> |
10 |
<input type="hidden" name="subStep" value="1" /> |
11 |
<div class="fieldGroup"><input type="submit" value="Submit" style="margin-left: 150px;" /></div> |
12 |
<div class="clear"></div> |
13 |
</form>
|
14 |
<?php break; case 'securityForm': ?> |
15 |
<h2>Password Recovery</h2> |
16 |
<p>Please answer the security question below:</p> |
17 |
<?php if ($error == true) { ?><span class="error">You must answer the security question correctly to receive your lost password.</span><?php } ?> |
18 |
<form action="<?= $_SERVER['PHP_SELF']; ?>" method="post"> |
19 |
<div class="fieldGroup"><label>Question</label><div class="field"><?= getSecurityQuestion($securityUser); ?></div></div> |
20 |
<div class="fieldGroup"><label for="answer">Answer</label><div class="field"><input type="text" name="answer" id="answer" value="" maxlength="255"></div></div> |
21 |
<input type="hidden" name="subStep" value="2" /> |
22 |
<input type="hidden" name="userID" value="<?= $securityUser; ?>" /> |
23 |
<div class="fieldGroup"><input type="submit" value="Submit" style="margin-left: 150px;" /></div> |
24 |
<div class="clear"></div> |
25 |
</form>
|
26 |
|
27 |
<?php break; case 'userNotFound': ?><br> <h2>Password Recovery</h2><br> <p>The username or email you entered was not found in our database.<br /><br /><a href="?">Click here</a> to try again.</p><br> <?php break; case 'successPage': ?><br> <h2>Password Recovery</h2><br> <p>An email has been sent to you with instructions on how to reset your password. <strong>(Mail will not send unless you have an smtp server running locally.)</strong><br /><br /><a href="login.php">Return</a> to the login page. </p><br> <p>This is the message that would appear in the email:</p><br> <div class="message"><?= $passwordMessage;?></div><br> <?php break; |
Este código tratará de mostrar realmente todos los elementos de la interfaz. Comenzamos examinando la variable $ show de anterior con switch (). Como recordará anteriormente, hay muchos valores diferentes que $ show puede tener, por lo que debemos crear un caso para cada uno. El primer caso, "emailForm", es donde el usuario ingresará un nombre de usuario o correo electrónico para el cual desea recuperar la contraseña. Básicamente, necesitamos dos campos de texto y una entrada oculta para saber qué paso hemos enviado. También hay un bloque if ($ error == true) que mostrará un mensaje de error si el indicador de $ error es verdadero.


El segundo caso es "securityForm". Aquí es donde se mostrará la pregunta de seguridad. Puede ver que también tenemos un mensaje de error y usamos getSecurityQuestion () para imprimir la pregunta de seguridad. Luego tenemos una entrada para la respuesta, así como el paso y el ID de usuario.


El mensaje para "userNotFound" es simplemente un mensaje de texto que le dice al usuario que no se encontró el registro.
"SuccessPage" se muestra cuando la pregunta de seguridad se ha respondido correctamente y el correo electrónico se ha enviado. Tenga en cuenta que imprimimos el mensaje de correo electrónico en la parte inferior de este mensaje, no lo haría en un entorno de producción, porque entonces le da a la persona acceso instantáneo al código de seguridad destinado solo para el usuario registrado.


Continuemos con este código:
1 |
|
2 |
case 'recoverForm': ?> |
3 |
<h2>Password Recovery</h2> |
4 |
<p>Welcome back, <?= getUserName($securityUser=='' ? $_POST['userID'] : $securityUser); ?>.</p> |
5 |
<p>In the fields below, enter your new password.</p> |
6 |
<?php if ($error == true) { ?><span class="error">The new passwords must match and must not be empty.</span><?php } ?> |
7 |
<form action="<?= $_SERVER['PHP_SELF']; ?>" method="post"> |
8 |
<div class="fieldGroup"><label for="pw0">New Password</label><div class="field"><input type="password" class="input" name="pw0" id="pw0" value="" maxlength="20"></div></div> |
9 |
<div class="fieldGroup"><label for="pw1">Confirm Password</label><div class="field"><input type="password" class="input" name="pw1" id="pw1" value="" maxlength="20"></div></div> |
10 |
<input type="hidden" name="subStep" value="3" /> |
11 |
<input type="hidden" name="userID" value="<?= $securityUser=='' ? $_POST['userID'] : $securityUser; ?>" /> |
12 |
<input type="hidden" name="key" value="<?= $_GET['email']=='' ? $_POST['key'] : $_GET['email']; ?>" /> |
13 |
<div class="fieldGroup"><input type="submit" value="Submit" style="margin-left: 150px;" /></div> |
14 |
<div class="clear"></div> |
15 |
</form>
|
16 |
<?php break; case 'invalidKey': ?> |
17 |
<h2>Invalid Key</h2> |
18 |
<p>The key that you entered was invalid. Either you did not copy the entire key from the email, you are trying to use the key after it has expired (3 days after request), or you have already used the key in which case it is deactivated.<br /><br /><a href="login.php">Return</a> to the login page. </p> |
19 |
<?php break; case 'recoverSuccess': ?> |
20 |
<h2>Password Reset</h2> |
21 |
<p>Congratulations! your password has been reset successfully.</p><br /><br /><a href="login.php">Return</a> to the login page. </p> |
22 |
<?php break; case 'speedLimit': ?> |
23 |
<h2>Warning</h2> |
24 |
<p>You have answered the security question wrong too many times. You will be locked out for 15 minutes, after which you can try again.</p><br /><br /><a href="login.php">Return</a> to the login page. </p> |
25 |
<?php break; } |
26 |
ob_flush(); |
27 |
$mySQL->close(); |
28 |
?>
|
Continuando con la instrucción de cambio, tenemos "recoverForm", que muestra el formulario para cambiar la contraseña. Imprimimos el nombre del usuario y un mensaje de error si es necesario. Luego tenemos los campos de entrada para una contraseña y una contraseña de confirmación, el paso de envío, el ID de usuario y la clave de seguridad del correo electrónico. Para la clave y el ID de usuario, estamos utilizando la sintaxis especial if () para asegurarnos de que obtengamos la variable del lugar correcto. Si la variable de la cadena de consulta es nula, buscaremos en las variables de publicación para asegurarnos de que siempre tenemos un valor aquí.


Luego, "invalidKey" significa que la clave proporcionada no existía o estaba caducada. El mensaje "speedLimit" se muestra si el usuario ha sido bloqueado debido a demasiadas respuestas incorrectas. Finalmente, llamamos a ob_flush () para enviar la página al navegador, y $ mySQL-> close () para desconectarse de la base de datos.
NOTA: los mensajes "recoverForm" y "invalidKey" de arriba no son necesarios para las contraseñas no cifradas.
Conclusión
En este artículo, hemos analizado la adición de una característica extremadamente útil para cualquier sitio de membresía. Ya sea que almacene sus contraseñas como texto simple o cifrado, este tutorial debería haberle ayudado a agregar una función de recuperación de contraseña. Analizamos la lógica detrás del cifrado de contraseñas, el envío de correos electrónicos para recuperar las contraseñas y algunos problemas de seguridad al hacerlo.
Además, analizamos el uso de mysqli para mejorar nuestra interacción con la base de datos. Si bien este tutorial no fue tan profundo con mysqli, debería dejarte un poco mejor de lo que estabas antes.
Espero que pueda usar las técnicas que se describen aquí para agregar esta característica extremadamente útil a su próximo sitio de membresía de usuario.
Nota sobre la descarga: En los archivos de descarga, he implementado versiones cifradas y no cifradas en dos carpetas diferentes.
- Síganos en Twitter o suscríbase al Feed RSS de NETTUTS para obtener más artículos y artículos de desarrollo web diarios.




