27.5.5 重设遗忘的密码

除了修改密码,我们还需要解决用户忘记其密码的情形。请注意,在首页login.php中,我们专门为此情形提供了一个链接,"Forgotten your password?",该链接指向脚本forgot_form.php,该脚本调用输出函数来显示如图27-8所示的表单。

27.5.5 重设遗忘的密码 - 图1

图 27-8 forgot_form.php脚本提供了一个表单,在这里,用户可以请求重置他们的密码并发送给他们

该脚本非常简单,只是调用了一些输出函数,因此,我们将不再详细介绍它。当提交表单时,它将调用forgot_passwd.php脚本,这段代码更加有趣。其脚本如程序清单27-17所示。

程序清单27-17 forgot_passwd.php——该脚本将用户密码


<?php

require_once("bookmark_fns.php");

do_html_header("Resetting password");

//creating short variable name

$username=$_POST['username'];

try{

$password=reset_password($username);

notify_password($username,$password);

echo'Your new password has been emailed to you.<br/>';

}

catch(Exception$e){

echo'Your password could not be reset-please try again later.';

}

do_html_url('login.php','Login');

do_html_footer();

?>


重置为一个随机值并将新密码发送到用户的邮箱

可以看到,该脚本使用两个主要函数来实现此功能:reset_password()和notify_password()。我们将逐一介绍它们。

reset_password()函数将产生一个随机密码并将它保存到数据库。该函数的代码如程序清单27-18所示。

程序清单27-18 user_auth_fns.php文件中的reset_password()函数——该脚本将用户密码重置为随机值并将其发送到该用户邮箱


function reset_password($username){

//set password for username to a random value

//return the new password or false on failure

//get a random dictionary word b/w 6 and 13 chars in length

$new_password=get_random_word(6,13);

if($new_password==false){

throw new Exception('Could not generate new password.');

}

//add a number between 0 and 999 to it

//to make it a slightly better password

$rand_number=rand(0,999);

$new_password.=$rand_number;

//set user's password to this in database or return false

$conn=db_connect();

$result=$conn->query("update user

set passwd=sha1('".$new_password."')

where username='".$username."'");

if(!$result){

throw new Exception('Could not change password.');//not changed

}else{

return$new_password;//changed successfully

}

}


该函数通过从字典里获取随机单词来生成一个随机密码。调用get_random_word()函数并在得到的单词后面添加一个0~999之间的随机数作后缀。get_random_word()函数也包含在user_auth_fns.php库中。其脚本如程序清单27-19所示。

程序清单27-19 user_auth_fns.php文件中的get_random_word()函数——该函数从词典中获取一个随机单词,以生成新密码


function get_random_word($min_length,$max_length){

//grab a random word from dictionary between the two lengths

//and return it

//generate a random word

$word='';

//remember to change this path to suit your system

$dictionary='/usr/dict/words';//the ispell dictionary

$fp=@fopen($dictionary,'r');

if(!$fp){

return false;

}

$size=filesize($dictionary);

//go to a random location in dictionary

$rand_location=rand(0,$size);

fseek($fp,$rand_location);

//get the next whole word of the right length in the file

while((strlen($word)<$min_length)||(strlen($word)>$max_length)||

(strstr($word,"'"))){

if(feof($fp)){

fseek($fp,0);//if at end,go to start

}

$word=fgets($fp,80);//skip first word as it could be partial

$word=fgets($fp,80);//the potential password

}

$word=trim($word);//trim the trailing\n from fgets

return$word;

}


要使该函数能够正常工作,我们需要一个词典。如果使用的是UNIX系统,其内置的拼写检查器ispell就带有单词词典,通常它位于/usr/dict/words或/usr/share/dict/words目录下。如果在以上两个位置都没有找到,在大多数系统上,可以使用如下命令找到一个词典:


$locate dict/words


如果使用的是其他的系统而且不愿安装ispell,不用担心,可以下载ispell使用的单词列表,其下载地址如下所示:http://wordlist.sourceforge.net/。

该网站也有许多其他语言的词典,因此如果喜欢其他任意一种语言,例如,Norwegian或Esperanto的单词,也可下载这些词典。所有这些文件的格式都是每个单词一行,每行通过换行符分开。

要从该文件中获取一个随机单词,首先应选取一个介于0到文件长度之间的位置,并从此位置开始读文件。如果从该随机位置开始一行一行地读,获取的很可能是单词的一部分,因此,我们通过两次调用fgets()函数,跳过开始的随机行,而将下面的一个单词作为需要的单词。

该函数有两处设计很巧妙。第一,如果在查找单词的时候到了文件结尾,可以从头开始,如下代码所示:


if(feof($fp)){

fseek($fp,0);//if at end,go to start

}


第二,可以搜索特定长度的单词:我们搜索从词典中抽出的每个单词,如果它的长度没有介于$min_length和$max_length之间,就继续搜索。同时,我们还将过滤带有单引号的单词。当使用该词时,我们过滤这些字符,但是获得下一个单词会更容易一些。

回到reset_password()函数,在生成了一个新密码之后,需要更新数据库以体现密码已被修改,并将新密码返回到主脚本;然后又将它传到notify_password()这个函数,该函数将新密码发送到用户邮箱。下面,让我们来了解notify_password()函数,如程序清单27-20所示。

程序清单27-20 user_auth_fns.php文件中的notify_password()函数——该函数将新密码以电子邮件方式发送给用户


function notify_password($username,$password){

//notify the user that their password has been changed

$conn=db_connect();

$result=$conn->query("select email from user

where username='".$username."'");

if(!$result){

throw new Exception('Could not find email address.');

}else if($result->num_rows==0){

throw new Exception('Could not find email address.');

//username not in db

}else{

$row=$result->fetch_object();

$email=$row->email;

$from="From:support@phpbookmark\r\n";

$mesg="Your PHPBookmark password has been changed to".$password."\r\n"

."Please change it next time you log in.\r\n";

if(mail($email,'PHPBookmark login information',$mesg,$from)){

return true;

}else{

throw new Exception('Could not send email.');

}

}

}


在这个函数中,给定一个用户名和密码,我们只需要在数据库中查找该用户的邮箱地址,调用PHP的mail()函数将其发送给该用户。

给用户一个真正随机的密码是更保险的——该密码是任何小写字母、大写字母、数字和标点符号的组合——而不只是如上设计的随机单词和数字的组合。但是,像"zigzag487"这样的密码,用户更易阅读和输入,这比真正随机数好。因为用户通常容易混淆字符串中的0和O(数字0和大写O),以及1和1(数字1和小写1)。

在我们的系统中,词典文件包含了45 000个单词记录。如果一个黑客知道我们是如何创建密码的,而且知道用户名称,就可以在尝试22 500 000次获得一个用户的密码。看上去,这种安全级别对于这种类型的应用程序是足够的,即使我们的用户没有按照电子邮件中的建议再次修改密码。