(转贴)PHP注册及登录验证模块设计(登陆模块大全)
所谓"人过留名,雁过留声",几乎每个动态交互式的网站都有一个注册模块用来保存用户信息,并提供一个登录模块以供注册用户登录。本章将建立一个注册程序,以实现用户的登录和注册。 通过本章的学习,读者将了解: — 如何建立HTML表单; — PHP如何获取用户填写的信息; — 如何建立PHP与MySQL数据库的连接; — 如何使用PHP往数据库添加记录及如何在数据库中查找记录; — 如何使用JavaScript脚本语言在客户端编程; — 如何使用正则表达式进行数据验证; — PHP如何使用Session来记住用户的登录信息; — 数据库处理错误的调试方法。 通过本章的学习,读者将对PHP网络编程有一个总体性的认识,为以后章节的学习打下良好的基础。 2.1 建立用户信息表
网站的开发是一个以数据为中心的开发过程,所以数据库的设计非常重要,在进行编程之前一定要做好需求分析和数据库设计。 本例将在MySQL安装时自动建立的test数据库中建立一张名为t_user的用户信息表。 t_user的表结构如表2-1所示。 表2-1 表t_user的结构 列 名
| 数 据 类 型
| 长 度
| 允 许 空
| 默 认 值
| 字 段 说 明
| f_username
| char
| 50
| 否
| 无
| 用户名,主键
| f_password
| char
| 50
| 否
| 无
| 用户密码
| f_name
| char
| 50
| 否
| 无
| 用户姓名
| f_email
| char
| 50
| 否
| 无
| 用户E-mail地址
| f_logintimes
| int
| 4
| 否
| 0
| 登录次数
| f_lasttime
| datetime
| 8
| 是
|
| 最后登录时间
| f_loginip
| char
| 19
| 是
|
| 最后登录IP
|
在编程开发中,程序员的代码应当是自注释的,也就是代码能够向阅读者传达出自身作用的信息,额外的说明语句需要但不宜太多,否则会降低代码的可读性。在编 程开发中,为每一个对象选择一个合适的名称是非常重要的,在进行数据库设计时为每张表及每个字段合适地命名也很重要。给表名和字段名提供一个合适的前缀可 以显著提高代码的可读性,笔者就喜欢给表名加上前缀"t_",为字段名加上前缀"f_"。
|
很多开发者可能会为用户信息表添加一个int型自动增量字段(如f_uid)作为主键,但笔者认为这样做是弊大于利、得不偿失的。一来造成空间的浪费,二 来时间效率上也有所降低。因为在实际开发过程中用户名是使用得最为频繁的查询条件,而众所周知在主键上进行的查询,其速度是最快的;使用自动增量字段为主 键的话,在用户名作为条件的查询上则要先根据用户名查找到f_uid,再根据f_uid去查找所要的信息。无疑,这是一个吃力不讨好的选择。 另外,很多熟悉其他类型数据库的开发者转而使用MySQL时仍喜欢使用Varchar类型的字段。其实如果空间不是非常紧张的话,在MySQL中一 般情况下Char类型是更好的选择。一是Char型字段时间效率高,二是两者长度范围都在255个字符以内,空间上损失不会太大,再者在取出Char型字 段数据时,数据库会自动丢弃多余的空格,因此使用上两者一样方便。
2.2 为注册建立HTML表单
能够用于网页设计的工具有很多,从简单的Windows自带的记事本、写字板到号称网页三剑客之一的Macromedia公司出品的 Dreamweaver都可以使用,这完全取决于开发人员的爱好。不过如果读者正在使用所见即所得的网页设计工具,而又有志成为一名专业的网络编程人员的 话,笔者还是建议读者放弃这些工具,因为这些工具有一个统一的毛病就是把HTML代码排列得乱七八糟,极大地降低了代码的可读性;另外也会使开发人员对工 具产生依赖性而不去记基本的HTML标签的使用。其实,HTML是极其简单的一门语言,标签数也不多,用心的话很快就能掌握。笔者一向比较喜欢 UltraEdit,有兴趣的读者可以试用。 注册页面的代码如下,输入这些内容并将其保存为register.php。 <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=gb2312"> <title>Registering form</title> </head> <body> <form name="frmRegister" method="post" action="register.php"> <table width="330" border="0" align="center" cellpadding="5" bgcolor= "#eeeeee"> <tr> <td width="40%">用户名:</td> <td><input name="username" type="text" id="username"> </td> </tr> <tr> <td>密码:</td> <td><input name="pwd" type="password" id="pwd"></td> </tr> <tr> <td>重复密码:</td> <td><input name="repeat_pwd" type="password" id="repeat_pwd"></td> </tr> <tr> 姓名: <td><input name="name" type="text" id="name"></td> </tr> <tr> <td>Email:</td> <td><input name="email" type="text" id="email"></td> </tr> <tr> <td colspan="2" align="center"> <input type="submit" name="Submit" value="提交"> <input type="reset" name="reset" value="重置"></td> </tr> </table> </form> </body> </html> 注意以上这段代码中以粗体标出的部分,其中"charset=gb2312"告诉浏览器本页使用gb2312编码(即简体中文)来显示文本内容,而使用
标签括起来的这一部分就是HTML表单的内容。标签的name属性用以指出表单的名称,以便客户端编程时引用表单内容;method属性指出表单内的数据 将以何种方式提交到服务器端,常用的提交方式有两种,一种是get方式,另一种就是本例中使用的post方式,默认的是get方式,这两种方式的区别后面 会给出详细讲述;action属性指出表单内的数据将提交给谁来处理,本例中的action="register. php"即指出表单内的数据将提交给register.php文件,也就是说表单所在的文件本身将处理这些数据。 用户注册界面如图2-1所示。 列 名
| 数 据 类 型
| 长 度
| 允 许 空
| 默 认 值
| 字 段 说 明
| f_username
| char
| 50
| 否
| 无
| 用户名,主键
| f_password
| char
| 50
| 否
| 无
| 用户密码
| f_name
| char
| 50
| 否
| 无
| 用户姓名
| f_email
| char
| 50
| 否
| 无
| 用户E-mail地址
| f_logintimes
| int
| 4
| 否
| 0
| 登录次数
| f_lasttime
| datetime
| 8
| 是
|
| 最后登录时间
| f_loginip
| char
| 19
| 是
|
| 最后登录IP
|
在网页设计中,标签往往不像其字面意思那样用来制作一张表格,而是作为定位的工具,这时就要将其border属性设为0,也就是让表格不显示边框。 标签的align属性也很有用,其作用是指明排列方式,有3个值可取:left,center和right,它们的意思很显然,勿庸赘述。另外,标签也具 有这个属性,意义一样,而且更为常用。
2.3 处理注册数据
本节将详细介绍PHP如何获取用户填写的数据信息,以及PHP如何连接数据库并将这些数据信息保存到数据库中。 2.3.1 获取用户填写的信息
PHP有几个预定义的自动全局变量,这些变量各有用途,其中可以用来获取客户端提交的数据的变量有$_REQUEST,$_GET和$_POST。$ _GET和$_POST分别用以获取客户端以get方式和以post方式提交的数据;而不管以什么方式提交上来的数据,$_REQUEST都可以取到。 PHP中的全局变量在使用之前必须以global关键字进行声明,很多开发人员在使用全局变量时经常会因为忘了将其声明为global而出错。然而PHP的自动全局变量却是个例外,所谓自动,就是说在使用之前无须声明为global,直接就可以使用。 还有一种其他PHP书籍介绍的获取客户端提交的数据的方式,即自动获取。该方式必须打开php.ini中的register_globals选项方能使用,而且更重要的是,在安全性上会给黑客以可乘之机,所以不推荐使用这种方式。 现在开始书写获取用户信息数据的代码,打开上一节建立的register.php文件。在文件的开始输入以下代码(输入时注意文本的对齐,这是优良代码风格的体现): <?php $username = $_POST['username']; $pwd = $_POST['pwd']; $repeat_pwd = $_POST['repeat_pwd']; $name = $_POST['name']; $email = $_POST['email']; ?> 接下来,在<body>标签之后,<form>标签之前输入以下代码: <?php if (!empty($username) ) { // 有内容才输出 echo "您填写的信息是:<br>\n"; echo "用户名: $username <br>\n"; echo "密码: $pwd <br>\n"; echo "重复密码: $repeat_pwd <br>\n"; echo "姓名: $name <br>\n"; echo "Email: $email <br>\n"; } ?> 这就是获取客户端提交的用户信息的全部代码。运行后,在表单中输入各项信息并单击"提交"按钮,最后页面内容如图2-2所示。 图2-2 获取用户信息后的页面 PHP中的字符串有两种定界符,即单引号"'"和双引号"""。单引号是强引用定界符,两个单引号间的一切字符都被解释为相应的字符;双引号是弱引 用定界符,两个双引号间的变量会以相应的变量值替换,一些特殊的字符也会进行相应的转义,如"\n"会在输出时产生一个新行。另外,读者注意"\n"只是 使PHP运行后产生的HTML源代码的相应位置启动一个新行,而标签<br>将使得浏览器显示的页面的相应位置启动一个新行。初学网页编程的 开发者,尤其是C语言开发者,易混淆二者。 2.3.2 建立PHP与MySQL数据库的连接
获取到用户输入的信息后,就该把这些信息记录到数据库中。不过在此之前,需要先建立PHP与MySQL数据库的连接。 在PHP中要建立与MySQL数据库的连接,需要使用mysqli构造函数,其调用原型是: class mysqli { __construct ( [string host [, string username [, string passwd [, string dbname [, int port [, string socket]]]]]] ) } 参数host指出MySQL数据库服务器,一般用IP地址(也可以是主机名,如localhost)指出服务器所在的机器。参数username和 password分别指定连接时使用的用户名和密码。参数dbname指定当前连接要使用的数据库。默认情况下,PHP将连接到服务器的3306端口,开 发人员可以通过参数port指定使用其他的端口号。参数socket较为少用,它可以进一步指定所用的套接字或命名管道。 在调用mysqli构造函数之后,应当调用mysqli_connect_errno()函数检查数据库连接是否建立成功,该函数原型是: int mysqli_connect_errno ( void ) 该函数返回的是最后一次建立连接时产生的错误代码,如果成功则返回0,返回其他值说明产生了某种类型的错误。调用mysqli_connect_error()函数可以获得具体的错误信息。 另外,如果在建立数据库连接时没有指定dbname参数,则还必须调用类mysqli的成员函数select_db(),以指定使用数据库服务器的哪一个数据库。该函数的原型是: bool select_db ( string database_name) 调用如果成功返回TRUE,失败返回FALSE。参数database_name指定要使用的数据库名称。 当操作完数据库后,还要调用类mysqli的成员函数close()来关闭之前打开的数据库连接,从而释放连接资源。虽然在PHP脚本执行完毕时,PHP会自动关闭数据库连接,但是及时关闭数据库连接是一个更好的编程习惯。 本例中,连接并使用数据库的代码如下: // 调用mysqli的构造函数建立连接,同时选择使用数据库'test' $db = @new mysqli("127.0.0.1", "developer", "123456", "test"); // 检查数据库连接 if (mysqli_connect_errno()) { echo "数据库连接失败!<br>\n"; echo mysqli_connect_error(); exit; // 退出程序,后面的所有语句将不再执行 } printf("Host information: %s <br/><br/>\n", $db->host_info); //...... 一些操作数据库数据的语句 // 关闭数据库连接 $db->close(); 到此为止,就已经建立好了一个操作数据库的PHP程序框架。 应当说明的是,PHP5的MySQL库提供了两套操作数据库的方法,一种是面向对象方式的,另一种是面向过程方式的。面向对象的方法是较先进的也是本书主要使用的方法。 这两种方式使用时的区别在于,面向对象方式调用的是类mysqli的成员函数,要使用类变量来调用,而不需要另外指定数据库连接资源;面向过程方式 调用的是全局函数,要多指定一个数据库连接资源的参数。另外这两者除了连接数据库的方法外,其他功能的函数名是对应的,差别在于面向对象方式的成员函数没 有"mysqli_"前缀。如面向对象方法使用$db->select_db(string dbname)来指定数据库,而面向过程方法使用mysqli_select_db (mysqli link, string dbname)来指定数据库(注意,粗体部分标出了二者的区别)。面向过程方法的函数中的参数mysqli link通过mysqli_connect()函数调用来获得,该函数的参数与mysqli的构造函数的参数完全相同;不同的是, mysqli_connect()成功时返回连接资源,失败时返回布尔值FALSE。 使用面向过程的方法连接数据库的例子参见第1章的1.3.3节。注意:PHP5之前的版本只支持面向过程的数据库操作方法。 如果用同样的参数第二次调用 mysqli_connect(),将不会建立新连接,而是返回已经打开的连接标识。 2.3.3 将用户信息记录到数据库
上一节已经建立了一个操作数据库的程序框架,本节具体介绍如何把数据添加到数据库中。这涉及PHP中MySQL数据库编程最重要也是使用最频繁的一个函数:query()。其原型是: mixed query ( string query [, int resultmode] ) 该函数向当前活动数据库发送一条查询语句。参数query指定要执行的查询语句。参数resultmode有两个可能值: MYSQLI_USE_RESULT和MYSQLI_STORE_RESULT,默认为MYSQLI_STORE_RESULT。如果指定了 MYSQLI_USE_RESULT,则在执行下一条查询前必须调用mysqli_result类的free()或close()函数释放查询结果对象, 否则查询将失败。一般在执行的会返回大量数据的查询语句时指定MYSQLI_USE_RESULT,其他情况都应使用 MYSQLI_STORE_RESULT。 如果执行的是SELECT,SHOW,DESCRIBE或EXPLAIN语句,query()函数执行成功将返回一个结果对象;如果执行的是其他查 询语句,如INSERT,DELETE,UPDATE等,query()函数执行成功则返回TRUE。无论执行什么查询语句,query()函数失败时都 将返回布尔值false。 下面就使用query()函数把前面用自动全局变量$_POST获取到的用户信息添加到数据库中去,代码如下: $sql = "INSERT INTO t_user (f_username, f_password, f_name, f_email) VALUES"; $sql .= "('$username', '$pwd', '$name', '$email')"; echo $sql; $rs = $db->query($sql); var_dump($rs); if (!$rs) { $db->close(); // 关闭数据库连接 echo '数据记录插入失败!'; exit; } 到此为止,已经制作好HTML注册表单,也能够获取到提交给PHP的用户信息,而且建立好了数据库连接并将这些用户信息记录到数据库。现在来整理一下代码,register.php文件的内容应当如下所示: <?php $username = $_POST['username']; $pwd = $_POST['pwd']; $repeat_pwd = $_POST['repeat_pwd']; $name = $_POST['name']; $email = $_POST['email']; if (!empty($username)) { // 用户填写了数据才执行数据库操作 // 调用mysqli的构造函数建立连接,同时选择使用数据库'test' $db = @new mysqli("127.0.0.1", "developer", "123456", "test"); // 检查数据库连接 if (mysqli_connect_errno()) { echo "数据库连接失败!<br>\n"; echo mysqli_connect_error(); exit; // 退出程序,后面的所有语句将不再执行 }
// 将用户信息插入数据库的t_user表 $sql = "INSERT INTO t_user (f_username, f_password, f_name, f_email) VALUES"; $sql .= "('$username', '$pwd', '$name', '$email')"; $rs = $db->query($sql); if (!$rs) { $db->close(); // 关闭数据库连接 echo '数据记录插入失败!'; exit; } echo "<font color='red' size='5'>恭喜您注册成功!</font><br>\n"; // 关闭数据库连接 $db->close(); } ?> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=gb2312"> <title>Registering form</title> </head> <body> <?php if (!empty($username) ) { echo "您填写的信息是:<br>\n"; echo "用户名: $username <br>\n"; echo "密码: $pwd <br>\n"; echo "重复密码: $repeat_pwd <br>\n"; echo "姓名: $name <br>\n"; echo "Email: $email <br>\n"; } ?> <form name="frmRegister" method="post" action="register.php"> <table width="330" border="0" align="center" cellpadding="5" bgcolor= "#eeeeee"> <tr> <td width="40%">用户名:</td> <td><input name="username" type="text" id="username"> </td> </tr> <tr> <td>密码:</td> <td><input name="pwd" type="password" id="pwd"></td> </tr> <tr> <td>重复密码:</td> <td><input name="repeat_pwd" type="password" id="repeat_pwd"></td> </tr> <tr> <td>姓名:</td> <td><input name="name" type="text" id="name"></td> </tr> <tr> <td>Email:</td> <td><input name="email" type="text" id="email"></td> </tr> <tr> <td colspan="2" align="center"> <input type="submit" name="Submit" value="提交"> <input type="reset" name="reset" value="重置"></td> </tr> </table> </form> </body> </html> 需要说明的是,在上面的代码中,使用了if (!empty($username))来确保当有数据提交上来时才执行数据库操作。这样做的原因是,在进行PHP网络编程时,HTML表单内填写的数据 往往是提交给表单所在的PHP文件本身来处理的。而第一次运行该PHP文件时服务器上是获取不到任何数据的,只有当用户填写完表单并提交之后第二次运行该 PHP文件时,才能获取到用户填写的数据。 最后,运行程序。在浏览器中打开register.php,然后在表单中输入适当的数据,如图2-3所示。单击"提交"按钮,结果如图2-4所示。
图2-3 填写好的注册表单 图2-4 注册成功的页面 如上述程序清单所示,整个程序是比较短小整洁的。在此回想一下代码的编写步骤和程序运行流程,将会有更大收获。 细心的读者可能已经发现,HTML注册表单上"重复密码"输入框的值在以上代码中一直没派上用场,而且Email随便输入都可以注册,这显然是不合理的。下一节的数据有效性验证的内容就是为解决这个问题的。 为了使得整个PHP文件整洁、易读,所有服务器端的代码,除了输出语句(如echo语句)外,都应当放在所有的HTML代码的前面,即文件的开始 处。这样做还有另外一个原因,有些PHP语句必须在任何输出语句之前执行,否则就会出错,而每一条HTML代码都相当于一条PHP的输出语句.
2.4 客户端的数据有效性验证
数据有效性验证既可以放在客户端进行也可以放在服务器端进行,或者在两边同时进行。一般来说,为了系统的安全,服务器端的有效性验证是必须的,而客户端的 验证则可有可无。但是,在客户端进行有效性验证有很大好处,因为它在将数据提交给服务器之前就可以排除很多的输入错误,从而减少网络流量,减轻了服务器的 负担,也加快了验证的速度,提升了系统的性能。 2.4.1 指点迷津——JavaScript编程
在客户端进行数据有效性验证需要使用客户端的脚本语言进行编程,通常有VBScript,JavaScript和JScript可以选用,在功能上 JavaScript基本是JScript的一个子集,选择何者视开发人员的习惯。但需要注意的是,因为VBScript和JScript只有IE浏览器 支持,如果希望其他浏览器也能支持客户端脚本,最好选用JavaScript。注意:JavaScript是区分大、小写字母的。 回顾一下,在第1章的"第一个PHP程序中"有一段JavaScript脚本,在此单独摘录,如下代码中粗体部分所示。 …… <head> <META HTTP-EQUIV="Content-Type" CONTENT="text/html; charset=gb2312" /> <title>第一个PHP程序</title> <!—-以下是一段客户端运行的JavaScript脚本代码--> <script language='javascript'> <!-- alert('<? echo $str;?>'); --> </script> </head> …… 客户端的脚本块由<script>标签包含,并由该标签的language属性指明所使用的脚本语言,如本例指定使用 JavaScript语言。具体的JavaScript代码一般会用<!--和->括起来,这是为了使不能支持JavaScript的浏览器 忽略这段内容,由于现在的绝大多数浏览器都支持JavaScript,所以这并非十分必要。一般而言,客户端的脚本块最好放在<head>与 </head>之间,尤其是类定义或者函数定义等不立即执行的代码。 本例中的alert()函数是JavaScript中使用最频繁的函数之一,其作用是弹出一个提示对话框,在实际编程中经常用它来提示用户的输入或操作错误。 客户端的所有内容都可以通过PHP动态生成,当然也包括JavaScript代码的一部分甚至是全部。本例中,alert()函数的参数就是通过 PHP生成的。但是动态生成客户端的JavaScript代码时很容易出错,因为必须保证生成代码的语法和逻辑的正确性。检查生成的代码是否正确的一个简 单方法就是查看客户端的网页源文件,具体查看方法本书第1章已经介绍得很详细,在此就不再赘述。 限于篇幅,对于JavaScript的基本语法在此不讨论,下面仅介绍与本书后面内容紧密相关的,也是较深入的JavaScript类的定义和使用。因为JavaScript的类使用函数来定义,在此先了解一下函数的定义和使用。请看下面代码: <html> <head> <meta http-equiv="content-type" content="text/html; charset=gb2312"> <title>javascript测试</title> <script language='javascript'> <!-- // 截去串头部的空格 function ltrim(str) { var i = 0; while(str.charAt(i) == ' '){++i;} return str.substring(i, str.length); } // 截去串尾部的空格 function rtrim(str) { var i = str.length - 1; while(str.charAt(i) == ' '){--i}; return str.substring(0, i+1); } // 截去串头尾的空格 function trim(str) { return ltrim(rtrim(str)); } var strTest = trim(' 测试截去头尾空格的函数! '); strTest = '|' + strTest + '|'; --> </script> </head> <body> javascript测试 </body> </html> 将这段代码保存成js_func.php并在浏览器中打开,弹出如图2-5所示对话框。可见原来的串"测试截去头尾空格的函数!"头尾的空格已经被截去。 下面介绍函数的基本组成。一个JavaScript函数由关键字"function"进行声明,关键字"function"后面接函数名,函数名后 面是由圆括号括起来的函数的参数声明;一个函数可以有0到多个参数,但是不管有没有参数,一对圆括号是必须的。在参数声明之后是用花括号括起来的函数体, 函数体内可以写0到多句任何合法的JavaScript语句。函数既可以使用关键字"return"返回一个值,也可以不返回任何值。函数使用时,可以出 现在任何合法的表达式可以出现的位置。函数调用的形式是函数名后接圆括号,圆括号内指明要传入的实参(没有实参时,圆括号也应存在)。 JavaScript中string对象的charAt()成员函数(或称"方法")用以取出指定位置的字符,其参数指明所要取得的字符的位置下标 (以0开始)。string对象的substring()成员函数是一个使用非常频繁的函数,其作用是取字符串的子串,带两个参数。第一个参数指明从子串 哪个位置开始,第二个参数指明子串到哪个位置结束(注意结束位置的字符不被取出)。 <body>标签的onload属性用于声明页面加载事件被触发时进行响应的代码。本例中的onload="javascript: alert(strTest);"即声明当本页面被加载时使用JavaScript语言调用alert()函数。页面运行后,显示如图2-5所示的对话 框。 有了JavaScript函数定义和使用的基础知识,再来看一下JavaScript中如何定义和使用类。请看下面的代码: <html> <head> <meta http-equiv="content-type" content="text/html; charset=gb2312"> <title>javascript测试</title> <script language='javascript'> <!-- function MyString(str) { this.str = str; this.getString = function () { return this.str; }
// 截去串头部的空格 function ltrim(str) { var i = 0; while(str.charAt(i) == ' '){++i;} return str.substring(i, str.length); } // 截去串尾部的空格 function rtrim(str) { var i = str.length - 1; while(str.charAt(i) == ' '){--i}; return str.substring(0, i+1); } // 截去串头尾的空格 function trim(str) { return ltrim(rtrim(str)); }
// 调用成员函数 this.str = trim(this.str); } var objTest = new MyString(' 测试javascript的类! '); var strTest = '|' + objTest.getString() + '|'; --> </script> </head> <body> javascript测试 </body> </html> 将这段代码保存成js_class.php并在浏览器中打开,弹出如图2-6所示对话框,可见达到了和上例相同的效果。 图2-6 测试JavaScript类弹出的对话框 本例代码中的粗体部分为与上例的不同之处。可以看出类的定义与函数的定义基本相同,关键的区别在于,用于定义类的函数体中使用了关键字 "this"。"this"关键字用以指示类的实例本身。本例中this.str = str;即向类实例添加一个名为str的成员变量并把从参数传回的值赋给这个成员变量;this.getString = function()则向类实例添加一个名为getString的成员函数,该函数简单地返回成员变量str的值。注意,类的成员变量和成员函数都是公有 的,即在类外也可以访问。然而在类的定义中可以定义嵌套的函数,如本例中的ltrim(),rtrim()和trim(),这些函数却是私有的,只能在类 的定义函数(相当于类的构造函数)中访问。 使用时,通过关键字"new"来生成类的实例(即对象),并使用"."号来引用对象的成员变量或函数。 JavaScript对象的属性和方法既可以在类的构造函数中添加,也可以在对象生成以后动态添加。如本例在语句var objTest = new MyString(…)之后,可以为objTest添加一个成员函数getString2,语法为objTest.getString2 = function () {return this.str + ' ';},这样调用objTest.getString2()将返回一个以空格结尾的串。 最后,使用JavaScript进行客户端编程时,很多代码往往是通用的,如果在每个页面都嵌入相同的脚本代码,不仅使人厌烦,而且极不利于维护。 因此,自然会想到让通用的代码独立出来,这在JavaScript中可以通过将通用代码放到js文件中来办到。现在把上面的js_class.php的代 码改写一下,分成两个文件:op.js和js_class2.php。 op.js内容如下: function MyString(str) { this.str = str; this.getString = function () { return this.str; }
// 截去串头部的空格 function ltrim(str) { var i = 0; while(str.charAt(i) == ' '){++i;} return str.substring(i, str.length); } // 截去串尾部的空格 function rtrim(str) { var i = str.length - 1; while(str.charAt(i) == ' '){--i}; return str.substring(0, i+1); } // 截去串头尾的空格 function trim(str) { return ltrim(rtrim(str)); }
// 调用成员函数 this.str = trim(this.str); } js_class2.php内容如下: <html> <head> <meta http-equiv="content-type" content="text/html; charset=gb2312"> <title>javascript测试</title> <script language='javascript' src='op.js'></script> <script language='javascript'> <!-- var objTest = new MyString(' 测试javascript的类! '); var strTest = '|' + objTest.getString() + '|'; --> </script> </head> <body> javascript测试 </body> </html> 运行一下js_class2.php,可以看到和js_class.php完全一样的结果。 改写后的代码就是简单地将通用的JavaScript代码搬到一个js文件中,然后在需要使用这些通用代码的页面包含这个js文件。包含js文件的 语句也很简单,就是在<script>标签的src属性中指出要包含的js文件,而<script>内部不含任何其他内容。 2.4.2 表单数据的有效性验证
在介绍如何进行客户端的数据有效性验证之前,有必要简单了解一下DOM技术。DOM是Document Object Model的缩写,在HTML客户端编程时是通过DOM来操作页面上的各个元素的。 DOM具有树型的结构,称为文档树,浏览器内置的document对象是文档树的根结点。在操作DOM时,经常以"document.xxx" (xxx指某一属性名或方法名,如forms)的方式来引用和操作文档中的某个或某类结点。document对象的getElementById()函数 可以取得具有指定ID的结点,getElementsByTagName()函数则可以取得具有指定标签名的所有结点的列表。document对象有一个 非常重要的属性:forms变量。document.forms[]是文档中所有<form>标签(即HTML表单)对应的form对象组成 的一个数组变量。可以使用document.forms[0]来引用页面中的第一个表单,使用document.forms[1]来引用页面中的第二个表 单等;也可以使用表单的名称(即<form>的name属性)或ID来引用某个特定的表单,即document.forms.xxx或 document.forms["xxx"],这时xxx用表单的名称或ID替代。 如果页面只需要支持IE浏览器, 则内置的document对象有一个all属性可以起到和getElementById()函数一样的效果。document.all[]是文档中所有标 签组成的一个数组变量, 其用法是document.all.xxx或document.all["xxx"],这时xxx是指某个特定标签的ID。 再次打开前面的register.php文件,找到文件中内容为<form name="frmRegister" method="post" action="register.php">的行。在该行中添加如下以黑体显示的文本: <form name="frmRegister" method="post" action="register.php"> onsubmit事件在提交表单时被触发,添加的代码中,<form>标签的onsubmit属性指定了当onsubmit事件被触发 时要执行的客户端代码"return doCheck();",即执行doCheck()函数并返回该函数的返回值,当返回值为true时,表单被正式提交,而当返回值为false时,表单的 提交操作被终止(数据未提交到服务器端)。 接下来需要在register.php文件的</head>标签之前添加自定义的JavaScript函数doCheck(),该函数用于真正验证表单中输入数据的有效性。代码如下: <script language='javascript'> <!-- // 验证表单数据有效性的函数 // 当函数返回true时,说明验证成功,表单数据正常提交 // 当函数返回false时,说明验证失败,表单数据被终止提交 function doCheck() { var username = document.frmRegister.username.value; var pwd = document.frmRegister.pwd.value; var repeat_pwd = document.frmRegister.repeat_pwd.value; var name = document.frmRegister.name.value; var email = document.frmRegister.email.value;
if (username == '') { alert('请输入用户名!'); return false; } if (pwd == '') { alert('请输入密码!'); return false; } if (name == '') { alert('请输入姓名!'); return false; } if (email == '') { alert('请输入Email!'); return false; } if (repeat_pwd != pwd) { alert('重复密码与密码不一致!'); return false; } if (pwd.length < 6 || pwd.length > 30) { alert('密码必须在6到30个字符之间!'); return false; }
// 验证Email的格式,如果长度小于6或者不含'@'符号和'.'符则认为格式不正确 if (email.length <= 6 || email.indexOf('@') < 1 || email.indexOf('.') < 3) { alert('Email填写不正确!'); return false; }
return true; } --> </script> 以上代码中,首先取得<form>表单中各输入域的值,然后判断各域的值是否为空,如果为空则弹出提示对话框并返回false终止提 交;随后验证两次输入的密码是否一致,以及密码的长度是否为6~30个字符;最后,验证Email的格式是否正确。如果所有验证都通过,则在函数的最后返 回true,允许表单数据提交。 再次运行register.php,并在表单中输入如图2-7所示数据(其中密码输入分别为1abcdef和2abcdef)。 图2-7 客户端数据有效性验证——输入的密码不同 单击"提交"按钮后,显示如图2-8所示的提示信息并且页面未被提交。 图2-8 客户端数据有效性验证——密码不同时的提示 读者可以自行测试其他几种输入不合法而验证不通过的情况,最后测试输入数据全部合法的情况,检查客户端验证是否达到了预期的结果。 2.4.3 多学两招——使用正则表达式
上节对Email的有效性验证是不很严密的,本节将使用正则表达式(Regular Expression)来改进它。正则表达式是一种功能非常强大的字符串处理工具,现在很多编程语言都支持正则表达式,而且它们使用的正则表达式规则也是相同的。 在JavaScript中内置了RegExp对象来操作正则表达式。有两种创建RegExp对象实例的方式,一是使用两个"/"将正则表达式包含起来,二是使用RegExp对象的构造函数。这两种方式是等效的。如: var pattern1 = /http/; var pattern2 = new RegExp("http"); 以上代码中,pattern1和pattern2是完全相同的,都是匹配任何含有"http"的字符串。 RegExp对象的test()方法用来测试一个字符串是不是与其模式相匹配,其参数是一个待测试的字符串,当字符串与模式匹配时返回true,否则返回false。如: var pattern = /http/; if( pattern.test("http://www.google.com") ) { // test()会返回true alert("match"); // 这儿将被执行 } else { alert("doesn't match"); } RegExp对象的exec()方法在测试一个字符串是否与其模式相匹配的同时,还可以获得一些额外的信息。exec()方法的参数和test() 方法一样,但exec()的返回值是一个数组,该数组含有模式匹配结果的很多属性值。其中,length属性指示数组的长度,input属性指示原始的输 入串,index属性指示第一个匹配串的首字符在原字符串中的下标,lastIndex属性指示匹配串的下一个字符在原字符串中的下标。如果模式中含有子 表达式(即有圆括号),exec()方法返回值的数组将包含多个元素,其中第一个元素是整个匹配的串,第二个及之后的元素按模式中的顺序存放匹配的子串。 请看下面的示例: <html> <head> <meta http-equiv="content-type" content="text/html; charset=gb2312"> <title>正则表达式测试</title> </head> <body> <script language='javascript'> <!— // 创建一个正则表达式RegExp对象实例 var pattern = /(cat) (and) (dog) /; // 调用RegExp对象的exec()方法 var result = pattern.exec("My cat and dog are black."); // 输出匹配结果,result.length是结果数组中元素个数 // document.writeln()函数可以向网页中输出字符串 for(var i = 0; i < result.length; ++i) { document.writeln("result[" + i + "] = " + result + "<br />"); } // 显示结果数组的其他属性的值 document.writeln("result.index = " + result.index + "<br/>"); document.writeln("result.lastIndex = " + result.lastIndex + "<br/>"); document.writeln("result.input = " + result.input + "<br/>"); --> </script> </body> </html> 将以上代码保存为js_regexp.php文件并在浏览器中运行,结果如图2-9所示。 除了直接调用RegExp对象的成员函数(方法),如test()和exec()等,来使用正则表达式以外,JavaScript的String对 象的一些成员函数(方法),如search()、split()、replace()和match()等也可以使用正则表达式来执行一些方便的字符串操 作,本书将在用到这些函数的时候再做详细介绍。现在先来看如何使用正则表达式来进行Email的有效性验证。 图2-9 正则表达式测试例子的结果页面 重新打开register.php文件,添加粗体部分的代码: …… <script language='javascript'> <!-- // 验证表单数据有效性的函数 // 当函数返回true时,说明验证成功,表单数据正常提交 // 当函数返回false时,说明验证失败,表单数据被终止提交 function doCheck() { …… if (pwd.length < 6 || pwd.length > 30) { alert('密码必须在6到30个字符之间!'); return false; }
// 使用正则表达式验证Email的格式 var pattern = /^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/; if (! pattern.test(email) ) { alert('Email填写不正确!'); return false; }
return true; } --> </script> …… 用来匹配合法Email的正则表达式为:/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/。在表 达式的开始使用"^"并在表达式的末尾使用"$",使得被测试的字符串必须完全匹配该模式test()方法才返回true。"\w"匹配任何单词字符(包 括下划线),"[-+.]\w+"匹配以字符"-"、"+"及"."之一开始后跟一个以上的单词字符(包括下划线),"([-+.]\w+)*"表示匹配 "[-+.]\w+"这个模式的串可以出现一次或多次。 再次运行register.php,并在表单中输入如图2-10所示的数据,密码可以任意输入,但必须保证两次输入的一致且在6~30个字符之间,图中输入了错误的Email(在"@"之前多了".")。单击"提交"按钮之后,页面提示如图2-11所示。 读者可以自行输入其他不合法的或者合法的Email进行测试,并对照上面的正则表达式,看看该正则表达式是如何达到预期的验证效果的。
图2-10 使用正则表达式验证Email合法性的测试页面 图2-11 Email不合法的提示对话框 使用正则表达式的关键在于构造正则表达式。构造正则表达式时,可以采用由小到大的组合法,即先构造小的简单的表达式,然后用多种元字符与操作符将小 的表达式结合来创建更大的更复杂的表达式。正则表达式的组件可以是单个的字符、字符集合、字符范围、字符间的选择或者所有这些组件的任意组合。
2.5 服务器端的数据有效性验证
服务器端的数据有效性验证相对于客户端的验证来说,更多的是出于安全性的考虑。正因为安全方面的原因,使得服务器端的验证更为重要。黑客们有种种办法可以跳过客户端的数据验证而直接提交数据到服务器端,因此绝对不能因为进行了客户端的验证而忽略服务器端的验证。 2.5.1 指点迷津——PHP处理正则表达式
PHP中的正则表达式即一个模式字符串,模式字符串以"/"开始且以"/"结束(头尾的"/"称为模式定界符)。PHP中主要提供了几个以"preg_" 为前缀的正则表达式操作函数,其中较为常用的有preg_match(),preg_replace()和preg_split()。 preg_match()函数常用的调用原型是: int preg_match ( string pattern, string subject [, array matches] ) preg_match()函数在subject字符串中搜索与pattern给出的正则表达式相匹配的内容。如果提供了matches,则 matches被搜索的结果所填充。$matches[0]将包含与整个模式匹配的文本,$matches[1]将包含与第一个捕获的圆括号中的子模式所 匹配的文本,依次类推。preg_match()返回pattern所匹配的次数,0次(没有匹配)或者1次,因为preg_match()在第一次匹配 之后将停止搜索。如果出错,preg_match()返回false。 下面是一个使用preg_match()的例子: <?php // 从 URL 中取得主机名, 模式定界符后面的 "i" 表示不区分大小写字母的搜索 // 定义一个正则表达式的模式字符串 $pattern = "/^(http:\/\/)?([^\/]+)/i"; if(preg_match($pattern, "http://www.php.net/index.html", $matches)) { // $matches[2]中的内容对应的是模式中第二个圆括号中的子模式所匹配的内容 $host = $matches[2]; echo "URL中的主机名是: $host"; } else { echo "没有匹配的串"; } ?> preg_replace()函数用于执行正则表达式的搜索和替换,其调用原型是: mixed preg_replace ( mixed pattern, mixed replacement, mixed subject [, int limit] ) preg_replace()函数在subject中搜索pattern模式的匹配项并替换为replacement。如果指定了limit,则仅替换limit个匹配项,如果省略limit或者其值为-1,则所有的匹配项都会被替换。 replacement可以包含"\$n"形式的逆向引用。每个此种引用将被替换为与第n个被捕获的括号内的子模式所匹配的文本。n可以从取值范围 为0~99,其中$0指的是被整个模式所匹配的文本。对左圆括号从左到右计数(从1开始)以取得子模式的数目。当替换模式在一个逆向引用后面紧接着一个数 字时,应当使用花括号来化解歧义。如逆向引用第一个子串$1后接一个数字1的情况,如果使用$11则为引用第11个子串,所以应该用${1}1,这样就形 成一个隔离的$1逆向引用,而使另一个1只是单纯的文字。 preg_replace()如果搜索到匹配项,则会返回被替换后的subject,否则返回原来不变的subject。 请看下面的例子: <?php $string = "April 15, 2003"; $pattern = "/(\w+) (\d+), (\d+)/i"; // 用以替换的串中使用逆向引用第1的子模式(对应模式中的(\d+))的匹配结果 $replacement = "\${1}1,\$3"; // 打印替换后的结果 print preg_replace($pattern, $replacement, $string); ?> 本例的输出结果将是:April1,2003。 preg_replace()的每个参数(除了limit)都可以是一个数组。如果subject是一个数组,则会对subject中的每个项目执 行搜索和替换,并返回一个数组。pattern和replacement是数组的情况比较复杂也较为少用,有兴趣的读者可以自行研究,在此不详述。 preg_split()函数非常有用,其调用原型是: array preg_split ( string pattern, string subject [, int limit [, int flags]] ) preg_split()函数用正则表达式分割字符串,返回一个数组,数组中包含subject中沿着与pattern匹配的边界所分割的子串。 如果指定了limit,则最多返回limit个子串,如果limit是-1,则意味着没有限制,可以用来继续指定可选参数flags。flags可以是下列标记的任意组合(用按位或运算符 | 组合): — PREG_SPLIT_NO_EMPTY 如果设定了本标记,则preg_split()只返回非空的部分。 — PREG_SPLIT_DELIM_CAPTURE 如果设定了本标记,定界符模式中的括号表达式也会被捕获并返回。 — PREG_SPLIT_OFFSET_CAPTURE 如果设定了本标记,对每个出现的匹配结果也同时返回其附属的字符串偏移量。注意这改变了返回的数组的值,使其中的每个单元也是一个数组,其中第一项为匹配字符串,第二项为其在subject中的偏移量。 下面是一个使用preg_ split ()函数的例子: <?php // 将一个句子分割成多个单词。分界符是","及空白字符的组合 // 空白字符包括: " ", \r, \t, \n 和 \f $pattern = "/[\s,]+/"; $keywords = preg_split($pattern, "hypertext language, programming"); print_r($keywords); ?> 本例的输出结果将是:Array ( [0] => hypertext [1] => language [2] => programming )。 现在回到服务器端数据有效性验证上来。对于前面的注册表单中提交上来的数据,需要做一些简单的验证。打开register.php,添加以下以粗体标出的代码: <?php // trim()函数可以截去头尾的空白字符 $username = trim($_POST['username']); $pwd = $_POST['pwd']; $repeat_pwd = $_POST['repeat_pwd']; $name = trim($_POST['name']); $email = trim($_POST['email']); if (!empty($username)) { // 用户填写了数据才执行数据库操作 //--------------------------------------------------------- // 数据验证, empty()函数判断变量内容是否为空 if (empty($username) || empty($name) || empty($email) || empty($pwd) || $repeat_pwd != $pwd) { echo '数据输入不完整'; exit; } if (strlen($pwd) < 6 || strlen($pwd) > 30) { echo '密码必须在6到30个字符之间'; exit; } // 与客户端验证Email时相同的正则表达式 $pattern = "/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/"; if (!preg_match($pattern, $email)) { echo 'Email格式不合法!'; exit; } //--------------------------------------------------------- // 调用mysqli的构造函数建立连接,同时选择使用数据库'test' $db = @new mysqli("127.0.0.1", "developer", "123456", "test"); …… ?> 正则表达式的功能固然强大,但是如果采用普通的字符串操作函数就能完成的功能也采用正则表达式来操作的话,会降低代码的执行效率,因此正则表达式也 不宜滥用。如在使用preg_replace()函数之前可以考虑str_replace()函数,使用preg_split()函数之前可以考虑 explode()函数和str_split()函数。 2.5.2 在数据库中查询记录
在关系数据库中使用SELECT查询语句可以在数据库中查询符合用户所需条件的记录。PHP中为操作MySQL而提供的类mysqli的query()成 员函数可用于执行SELECT语句,query()成员函数执行SELECT语句后返回的是一个类mysqli_result(结果记录集类)的对象。然 后通过mysqli_result对象的fetch_array(),fetch_row(),fetch_assoc()等方法可以以数组的形式按行取 得结果记录集中的每条记录。 fetch_row()返回的是以数字为下标的数组,fetch_assoc()返回的是以字段名为下标的关联数组。fetch_array ([int resulttype])函数可以带一个参数resulttype,指明要返回的结果类型。当未指定参数或者参数resulttype的值为 mysqli_num时,其作用等效于fetch_row();当参数resulttype的值为mysqli_assoc时,其作用等效于 fetch_assoc()。 下面的代码用于获取用户信息表t_user中的所有记录: <?php // 调用mysqli的构造函数建立连接,同时选择使用数据库'test' $db = @new mysqli("127.0.0.1", "developer", "123456", "test"); // 检查数据库连接 if (mysqli_connect_errno()) { echo "数据库连接失败!<br>\n"; echo mysqli_connect_error(); exit; // 退出程序,后面的所有语句将不再执行 } // 构造SQL语句查询t_user表 $sql = "SELECT * FROM t_user"; $rs = $db->query($sql); if (!$rs) { $db->close(); // 关闭数据库连接 echo '查询失败!'; exit; } // 从结果记录集中获取记录并使用<table>显示记录 echo "<table border='1' bgcolor='lightblue'>"; echo "<tr><th>用户名</th><th>密码</th><th>姓名</th><th>Email</th></tr>"; // $rs->fetch_assoc()返回一个数组,数组中是记录集中的当前行的数据 // 并将记录集指针指向下一条记录 while($row = $rs->fetch_assoc()) { echo "<tr><td>{$row['f_username']}</td>"; echo "<td>{$row['f_password']}</td>"; echo "<td>{$row['f_name']}</td>"; echo "<td>{$row['f_email']}</td></tr>\n"; }// while echo "</table>"; // 关闭数据库连接 $db->close(); ?> 将以上代码保存为alluser.php文件并在浏览器中执行,结果如图2-12所示(实际内容因MySQL数据库中的数据的不同而不同)。 图2-12 从数据库获取到的用户列表 2.5.3 检查用户名是否已存在
一般来说,系统中的用户名应当是唯一的,也因此t_user表中的f_username字段被设计成了表格的关键字段。所以在使用用户信息之前,需要先判断用户名是否已经存在。 再次打开register.php文件,添加如下以粗体显示的代码: <?php …… // 调用mysqli的构造函数建立连接,同时选择使用数据库'test' $db = @new mysqli("127.0.0.1", "developer", "123456", "test"); // 检查数据库连接 if (mysqli_connect_errno()) { echo "数据库连接失败!<br>\n"; echo mysqli_connect_error(); exit; // 退出程序,后面的所有语句将不再执行 } // 查询数据库,看填写的用户名是否已经存在 $sql = "SELECT * FROM t_user WHERE f_username='$username'"; $rs = $db->query($sql); // $rs->num_rows判断上面的执行结果是否含有记录,有记录说明用户名已经存在 if ($rs && $rs->num_rows > 0) { echo "<font color='red' size='5'>该用户名已被注册,请换一个重试!</font><br>\n"; } else { // 将用户信息插入数据库的t_user表 $sql = "INSERT INTO t_user (f_username, f_password, f_name, f_email) VALUES"; $sql .= "('$username', '$pwd', '$name', '$email')"; $rs = $db->query($sql); if (!$rs) { $db->close(); // 关闭数据库连接 echo '数据记录插入失败!'; exit; } echo "<font color='red' size='5'>恭喜您注册成功!</font><br>\n"; } // 关闭数据库连接 $db->close(); …… ?> 在上面的代码中,使用了一个SELECT查询语句来检查数据库中是否存在同用户名的用户。从结果记录集类mysqli_result对象的 num_rows成员中可以获取记录集中记录的数目,通过判断$rs->num_rows是否大于0,可知用户名是否已经存在。如果存在则仅给出提 示,如果不存在则将用户信息记录到数据库中去。 现在再来看一下再次注册同一个用户的情况。在浏览器中运行register.php并在页面的表单中输入适当内容(除用户名为zhangsan外,其他的可任意输入),并单击"提交"按钮,结果如图2-13所示。 图2-13 因用户名已存在而注册失败的页面 2.5.4 脚下留心——小心SQL注入漏洞
前面对客户端提交的数据进行了截去头尾空格然后判断是否为空,判断密码长度是否在有效范围,判断Email的格式是否有效,以及判断用户名是否重复等数据的有效性验证,是否这就足够了呢?其实还有一个SQL注入漏洞的问题值得重视。 客户端提交上来的数据往往被用于构造SQL语句,使用这些构造出来的SQL语句访问数据库时会发生SQL注入攻击。所谓SQL注入,即客户端提交的数据用于构成了非法访问数据库的SQL语句。 为防止SQL注入漏洞,一是要遵循最小权限的原则,即赋予连接数据库的用户尽可能小(仅能执行预期的操作)的权限,严禁使用特权用户(如root);二是要尽可能地过滤由客户端提交上来的可疑的非法数据。 假设在服务器端将执行如下SQL语句: $sql = "DELETE FROM t_user WHERE f_username='$username'"; 这条SQL语句的本意是根据客户端的要求删除特定用户,但是如果一个不怀好意的访问者知道服务器端的数据库的用户信息表的名称,而且提交了一个用户 名为"1' OR 1 --"的数据,即$username = "1' OR 1 – –"。此时将上面这条SQL语句中的$username替换成提交上来的实际值后成为: $sql = "DELETE FROM t_user WHERE f_username='1' OR 1 –- '"; 由于删除条件中的"OR 1",结果表t_user中的记录将被全部删除。 要防范SQL注入漏洞,实际上只要屏蔽一些SQL命令及关键字即可。在进行服务器端的数据有效性验证之前,调用以下函数即可防止SQL注入漏洞: <?php function checkIllegalWord () { // 定义不允许提交的SQL命令及关键字 $words = array(); $words[] = " add "; $words[] = " count "; $words[] = " create "; $words[] = " delete "; $words[] = " drop "; $words[] = " from "; $words[] = " grant "; $words[] = " insert "; $words[] = " select "; $words[] = " truncate "; $words[] = " update "; $words[] = " use "; $words[] = "-- ";
// 判断提交的数据中是否存在以上关键字, $_REQUEST中含有所有提交数据 foreach($_REQUEST as $strGot) { $strGot = strtolower($strGot); // 转为小写 foreach($words as $word) { if (strstr($strGot, $word)) { echo "您输入的内容含有非法字符!"; exit; // 退出运行 } } }// foreach } checkIllegalWord(); // 在本文件被包含时即自动调用 ?> 输入以上代码并将其保存成common.php文件,在需要验证的页面文件中只需要简单地引用该文件即可。引用的语法如下: require_once('common.php'); 在PHP中可以用来引入其他文件的关键字有:require,require_once,include和include_once。 require和include以及require_once和include_once之间的区别仅在于:当要引用的文件不存在时,使用require 和require_once会导致一个致命错误,而使用include和include_once则仅产生一个警告,后续的代码仍会继续执行。另外, require_once和include_once能够确保仅对被包含的文件引用一次。
2.6 显示用户的注册信息 在用户注册成功之后,最好再次显示用户的注册信息以提示用户。 2.6.1 获取用户的注册信息 用户的注册信息从表单提交的数据中即可直接得到,下面介绍PHP的应用采用从数据库中读取的方法。 重新打开register.php文件,并做粗体部分所示的修改: <?php require_once('common.php'); // 引入公共文件,其中实现了SQL注入漏洞检查的代码 …… if (!empty($username)) { // 用户填写了数据才执行数据库操作 …… // $rs->num_rows判断上面的执行结果是否含有记录,有记录说明用户名已经存在 if ($rs && $rs->num_rows > 0) { echo "该用户名已被注册,请换一个重试!
\n"; } else { $pwd = md5($pwd); // 将明文密码使用md5算法加密 // 将用户信息插入数据库的t_user表 $sql = "INSERT INTO t_user (f_username, f_password, f_name, f_email) VALUES"; $sql .= "('$username', '$pwd', '$name', '$email')"; $rs = $db->query($sql); if (!$rs) { $db->close(); // 关闭数据库连接 echo '数据记录插入失败!'; exit; } // echo "<font color='red' size='5'>恭喜您注册成功!</font><br>\n"; // 将输出重定向到register_result.php文件 header("Location: register_result.php?uid=$username"); } // 关闭数据库连接 $db->close(); } ?> …… <body> <!--此处删除了显示用户填写的数据的PHP代码--> <form name="frmRegister" method="post" action="register.php"> …… 以上代码有两点主要的修改:一是对密码使用md5()函数进行加密,二是注册成功后实现了页面的跳转。 以明文形式在数据库中记录用户的密码是非常不安全的,因此这里调用PHP的md5()函数对明文形式的密码进行加密是很重要的;md5()函数返回一个由32个十六进制字符组成的哈希值串。另一个可选的加密函数是sha1(),其用法与md5()函数相同。 在PHP中可以使用header()函数进行页面的跳转(重定向),语法如下所示: header("Location: register_result.php?uid=$username"); 该语句的作用是,使得网页被重定向到另一个URL: register_result.php?uid=$username。并且该URL中含有一个参数uid,uid的值是客户端提交上来的用户名。这样做 的原因是,在一个专门的页面显示用户的注册信息,以使整个注册的流程更为清晰。 下面新建一个register_result.php文件,文件内容如下: <?php require_once('common.php'); // 引入公共文件,其中实现了SQL注入漏洞检查的代码 // 获取用户名 $username = trim($_GET['uid']); if(empty($username)) { echo 'URL参数错误!'; exit; } // 调用mysqli的构造函数建立连接,同时选择使用数据库'test' $db = @new mysqli("127.0.0.1", "developer", "123456", "test"); // 检查数据库连接 if (mysqli_connect_errno()) { echo "数据库连接失败!<br>\n"; echo mysqli_connect_error(); exit; // 退出程序,后面的所有语句将不再执行 } // 构造SQL语句查询t_user表,以获取用户信息 $sql = "SELECT * FROM t_user WHERE f_username='$username'"; $rs = $db->query($sql); if (!$rs) { $db->close(); // 关闭数据库连接 echo '查询失败!'; exit; } // 从结果记录集中获取记录放入$user数组 $user = $rs->fetch_assoc(); // 关闭数据库连接 $db->close(); ?> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=gb2312"> <title>Registered Result</title> </head> <body> <center> <?php if (!empty($user) ) { echo "<font color='red' size='5'>恭喜您注册成功!</font><br />\n"; echo "您的注册信息如下:<br />\n"; echo "用户名: {$user['f_username']} <br />\n"; echo "姓名: {$user['f_name']} <br />\n"; echo "Email: {$user['f_email']} <br />\n"; } ?> </center> </body> </html> 注意以上代码中以粗体显示的部分。首先, 通过PHP的自动全局变量$_GET获取从register.php重定向过来的URL中附加的用户名,此处使用$_GET而不是$_POST,因为直接 在URL中附加参数提交数据是以GET方式而不是POST方式。之后,判断用户名是否为空,如果为空则说明不是从register.php重定向而来的, 系统认为产生了错误并推迟网页的运行。 在数据有效性验证通过之后,代码中使用带WHERE查询条件的SELECT语句获取指定用户的信息,并将用户信息存放于$user数组,随后关闭打开的数据库连接(因为后面的操作与数据库无关,及时关闭以培养一个好的编程习惯)。 最后,在网页的<body>和</body>标签之间显示注册成功的提示信息,以及前面从数据库中取出并存放在$user数组中的用户信息。 再次运行register.php,在表单中填入适当数据并提交后,结果如图2-14所示。 图2-14 使用重定向技术后显示的注册信息 注意,此时浏览器的地址栏中显示的并不是初始的http: //localhost/register.php,而是register.php中的重定向语句中的URL:http: //localhost/register_result.php?uid=ceshiyuanjia。 在使用重定向功能的时候,在header()语句之前不能有任何的输出,包括显式的调用echo(),print(),print_r()等函数产生的输出和隐式的放在<?php ?>之外的任何字符输出(甚至只是一个空行)。如有任何输出都将造成运行错误。 2.6.2 使用CSS格式化页面 前面显示的用户注册信息看起来比较杂乱,使用HTML表格可以使得页面显示更为齐整。另外一个常用的更为简洁的可选方法是,使用CSS技术或者CSS与HTML相结合的技术来格式化页面的显示。 CSS是Cascading Style Sheet的缩写,可译为"层叠样式表"。CSS是用于控制(增强)网页样式并允许将样式信息与网页内容分离的一种标记性语言。 可以用3种方式将样式表加入您的网页,而最接近目标的样式其定义优先权越高。高优先权样式将继承低优先权样式的未重叠定义,但覆盖重叠的定义。 1.将样式表加入网页的方式 (1)链入外部样式表文件(Linking to a Style Sheet) 这种方式是先建立外部样式表文件(.css),然后使用HTML的link对象链入该文件。示例如下: <head> <title>title of article</title> <link rel=stylesheet href="style.css" type="text/css"> </head> 注意,<link>标签一般放在<head></head>标记内。 (2)定义内部样式块对象(Embedding a Style Block) 这种方式使用时是在目标HTML文档中(一般在<head></head>标记内)插入一个<style>...</style>块对象。示例如下: <head> <style type="text/css"> <!-- body {font: 10pt "Arial"} p {font: 10pt/12pt "Arial"; color: black} --> </style> </head> 请注意,这里将style对象的type属性设置为text/css,是允许不支持这类型的浏览器忽略样式表单。 (3)内联定义(Inline Styles) 内联定义即是在对象的标记内使用对象的style属性,定义适用该对象的样式表属性。示例如下: <p style="margin-left: 0.5in; margin-right:0.5in">这一行增加了左右的外补丁<p> 2.定义样式表的基本语法 基本语法为: Selector { property: value } 其中Selector是选择符,property:value是样式表属性的定义,属性和属性值之间用冒号( 隔开。定义之间用分号(;)隔开。 (1)选择符 常用的选择符主要有3种:类型选择符、类选择符和ID选择符。 类型选择符以文档语言对象类型作为选择符,示例如下: <style type="text/css"> <!— /* 所有td对象字体尺寸为14px,宽度为120px */ td { font-size:14px; width:120px; } /* 所有a对象都设为无文本装饰 */ a { text-decoration:none; } --> </style> 类选择符可以自定义类名,并以"."+类名组成,"."前可以指定一个类型选择符。示例如下: <style type="text/css"> <!-- /* 所有class属性值等于(包含)"note"的div对象字体尺寸为14px */ div.note { font-size:14px; } /* 所有class属性值等于(包含)"note"的对象字体尺寸为14px */ .dream { font-size:14px; } --> </style> ID选择符以文档目录树(DOM)中作为对象的唯一标识符的ID作为选择符,ID前以"#"引导。示例如下: <style type="text/css"> <!— /* 文档中ID为note的对象字全尺寸为14px */ #note { font-size:14px; } --> </style> 在实际应用中,不宜过多使用类型选择符,这和在编程中不宜过多使用全局变量一样。因为类型选择符会影响具有同一对象类型的所有元素,经常会带来负面效果。 (2)单位 长度单位常用的有:px = 像素,pt = 点,in = 英寸,cm = 厘米,mm = 毫米。 颜色单位有: #RRGGBB 由3个表示红、绿、蓝颜色值的两位十六进制正整数组成; rgb ( R,G,B ) 由3个红、绿、蓝颜色值的正整数或百分数数值组成。 颜色名称(如red)不同的浏览器会有不同的预定义颜色名称。 (3)属性 CSS中的属性有很多,在网上可以找到许多CSS属性介绍的资料,此处不再具体介绍。 根据以上介绍的CSS的基础知识,下面来讲解如何使用CSS格式化显示注册信息的页面。打开上面的register_result.php文件,更改如下,注意以粗体显示的部分。 <?php require_once('common.php'); // 引入公共文件,其中实现了SQL注入漏洞检查的代码 // 获取用户名 $username = trim($_GET['uid']); if(empty($username)) { echo 'URL参数错误!'; exit; } // 调用mysqli的构造函数建立连接,同时选择使用数据库'test' $db = @new mysqli("127.0.0.1", "developer", "123456", "test"); // 检查数据库连接 if (mysqli_connect_errno()) { echo "数据库连接失败!<br>\n"; echo mysqli_connect_error(); exit; // 退出程序,后面的所有语句将不再执行 } // 构造SQL语句查询t_user表 $sql = "SELECT * FROM t_user WHERE f_username='$username'"; $rs = $db->query($sql); if (!$rs) { $db->close(); // 关闭数据库连接 echo '查询失败!'; exit; } // 从结果记录集中获取记录放入$user数组 $user = $rs->fetch_assoc(); // 关闭数据库连接 $db->close(); ?> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=gb2312"> <title>Registered Result</title> <style type="text/css"> <!— /* 表边框为内凹型 */ table {border-color: #c0e0c0; border-style: inset; border-width:4px;} td {font-size:14pt} /* 居中 */ td.hint {color:red; font-size: 20pt;text-align:center} /* 背景为天蓝色 */ td.caption {background-color:skyblue;font-size: 16pt} /* 粗体 */ td.label {font-weight:bold;} --> </style> </head> <body> <center> <?php if (!empty($user) ) { ?> <table border='0' cellpadding='5' cellspacing='5'> <tr><td colspan='2' class='hint'>恭喜您注册成功! </td></tr> <tr><td colspan='2' class='caption'>您的注册信息如下: </td></tr> <tr><td class='label'>用户名:</td> <td><?echo $user['f_username']; ?></td></tr> <tr><td class='label'>姓名:</td> <td><?echo $user['f_name']; ?></td></tr> <tr><td class='label'>Email:</td> <td><?echo $user['f_email'];?></td></tr> </table> <?php } ?> </center> </body> </html> 以上代码中,首先是加入了 再次运行register.php并注册一个新的用户,注册成功后的页面如图2-15所示。 图2-15 使用CSS格式化后的用户注册信息页面
2.7 实现用户登录
本章前面已经完成了网站的用户注册功能,本节将继续完成网站的登录功能。 2.7.1 创建登录表单
登录表单的HTML代码如下: <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=gb2312"> <title>User Login</title> <style type="text/css"> <!— .alert {color: red} .textinput {width:160px} .btn {width:80px} table {border: 3px double;background-color:#eeeeee;} --> </style> <script language="javascript"> <!-- function doCheck(){ if(document.frmLogin.username.value==""){ alert('请输入你的用户名!'); return false; } if(document.frmLogin.password.value==""){ alert('请输入你的密码!'); return false; } } --> </script> </head> <body> <form name="frmLogin" method="post" action="login.php"> <table border="0" cellpadding="8" width="350" align="center"> <tr><td colspan="2" align="center" class="alert"></td></tr> 用户名: <td><input name="username" type="text" id="username" class="textinput" /></td> </tr> <tr><td>密码:</td> <td><input name="pwd" type="password" id="password" class="textinput" /></td> </tr> <tr><td colspan="2" align="center"> <input type="submit" class="btn" value="登录"> <input type="reset" class="btn" value="重置"> </td> </tr> </form> </body> </html> 将以上代码保存为login.php文件并在浏览器中运行,显示页面中的登录表单如图2-16所示。 图2-16 显示页面的登录表单 2.7.2 验证登录名和密码
用户在登录表单中输入登录信息之后,数据被提交回本页面login.php进行处理,下面在login.php的头部添加验证用户名和密码是否正确的代码。login.php代码更改如下: <?php require_once('common.php'); // 引入公共文件,其中实现了SQL注入漏洞检查的代码 $username = trim($_POST['username']); // 取得客户端提交的密码并用md5()函数时行加密转换以便后面的验证 $pwd = md5($_POST['pwd']); // 设置一个错误消息变量,以便判断是否有错误发生 // 以及在客户端显示错误消息。 其初值为空 $errmsg = ''; if (!empty($username)) { // 用户填写了数据才执行数据库操作 //--------------------------------------------------------- // 数据验证, empty()函数判断变量内容是否为空 if (empty($username)) { $errmsg = '数据输入不完整'; } //--------------------------------------------------------- if(empty($errmsg)) { // $errmsg为空说明前面的验证通过 // 调用mysqli的构造函数建立连接,同时选择使用数据库'test' $db = @new mysqli("127.0.0.1", "developer", "123456", "test"); // 检查数据库连接 if (mysqli_connect_errno()) { $errmsg = "数据库连接失败!
\n"; } else { // 查询数据库,看用户名及密码是否正确 $sql = "SELECT * FROM t_user WHERE f_username='$username' AND f_password='$pwd'"; $rs = $db->query($sql); // $rs->num_rows判断上面的执行结果是否含有记录,有记录说明登录成功 if ($rs && $rs->num_rows > 0) { // 在实际应用中可以使用前面提到的重定向功能转到主页 $errmsg = "登录成功!"; } else { $errmsg = "用户名或密码不正确,登录失败!"; }
// 关闭数据库连接 $db->close(); } } } ?> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=gb2312"> <title>User Login</title> <style type="text/css"> <!-- .alert {color: red} .textinput {width:160px} .btn {width:80px} table {border: 3px double;background-color:#eeeeee;} --> </style> <script language="javascript"> <!-- function doCheck(){ if(document.frmLogin.username.value==""){ alert('请输入你的用户名!'); return false; } if(document.frmLogin.password.value==""){ alert('请输入你的密码!'); return false; } } --> </script> </head> <body> <form name="frmLogin" method="post" action="login.php"> <table border="0" cellpadding="8" width="350" align="center"> <tr><td colspan="2" align="center" class="alert"><?echo $errmsg;?></td></tr> 用户名: <td><input name="username" type="text" id="username" class="textinput" value="<?echo $username;?>" /></td> </tr> <tr><td>密码:</td> <td><input name="pwd" type="password" id="password" class="textinput" /></td> </tr> <tr><td colspan="2" align="center"> <input type="submit" class="btn" value="登录"> <input type="reset" class="btn" value="重置"> </td> </tr> </form> </body> </html> 以粗体显示部分是新添的代码,大部分语句都已有详细注释。值得注意的是在HTML表单中添加的代码,其中在的第一行的单元格中加入PHP代码用以输 出服务器端处理过程可能发生的错误或提示,另外在用户名的输入框标记的value属性中添加了PHP代码用以输出上次提交的用户名,以便因为输入密码错误 而不能登录的用户在重试的时候只需要输入密码。 再次运行login.php并在表单中输入数据尝试登录,登录成功与失败时的页面分别如图2-17和2-18所示。
图2-17 登录成功页面 图2-18 登录失败页面 2.7.3 更新用户登录信息
用户信息表t_user中,f_logintimes字段用来记录用户的登录次数,f_lasttime字段用来记录用户最后一次登录的时间, f_login字段用来记录用户最后一次登录用的IP,数据库中记录这些信息主要是为管理提供统计用户登录次数和用户地域分布的方便。这些数据在用户每次 登录后都要更新,下面来添加更新这些数据的代码。 打开login.php文件,将其头部的PHP代码更改如下: <?php ...... if ($rs && $rs->num_rows > 0) { // 在实际应用中可以使用前面提到的重定向功能转到主页 $errmsg = "登录成功!";
// 更新用户登录信息 $ip = $_SERVER['REMOTE_ADDR']; // 获取客户端的IP $sql = "UPDATE t_user SET f_logintimes = f_logintimes + 1,"; $sql .= "f_lasttime=now(), f_loginip='$ip' "; $sql .= " WHERE f_username='$username'"; $db->query($sql); } else { $errmsg = "用户名或密码不正确,登录失败!"; } ...... ?> 以上粗体部分为新添的代码,代码中首先从自动全局变量$_SERVER中获得客户端的IP地址,然后构造SQL语句并执行该语句以更新用户登录信 息。值得注意的是该SQL语句中对f_lasttime的赋值是通过调用MySQL的内部函数now()来实现的,MySQL的now()函数返回的是服 务器上的当前时间。 2.7.4 用Session保存用户信息
HTTP协议是无状态的。它完成的事情只是简单地发送请求到服务器,以及从服务器获取数据;除此之外一无所知,即使两次请求同一个PHP文件,它也不会认为两次请求之间有任何联系。 由于HTTP协议的无状态,这就使得无法在两个不同的请求之间共享信息,如无法记录"当前访问者"的信息。虽然在登录过程已经验证了用户的用户名与 密码是正确的,但是当用户跳转到其他页面时,从登录页面获得的用户信息全部丢失,这是用户不希望发生的。同时,要求用户进入每一个页面时都要输入用户名与 密码进行验证又是不现实的,这就要求可以在不同页面之间共享信息。 一般来说,对于PHP以及其他的Web编程语言,可以使用Cookie或者是Session来解决这个问题。 Cookie是保存在客户端的一个小文件,可以将一些需要在页面间共享的资料存储在这个文件中。但Cookie有3个缺点:一是大小不可以超过 4KB(不同的浏览器可能限制不同),二是用户可以在浏览器设置中禁用Cookie,三是Cookie是在客户端记录资料安全性较差。 Session一般是通过Cookie来实现的,如果用户禁用了Cookie,Session也同样失效。不同于Cookie的是,Session 只是把一个信息的标识通过Cookie放在客户端而实际的信息却存放在服务器上,这样安全性能上有较大的提高。现在也有另外一种不通过Cookie而使用 Session的方法,即URL重写技术。这种方法是将Session的标识作为URL的参数与服务进行交互,其好处是不受客户端对Cookie禁用的限 制,缺点是使用起来较为麻烦。 在PHP中使用Session非常简单。PHP提供了一个自动全局变量$_SESSION用于处理Session。但是需要注意的是,如果在PHP 的配置文件中没有设置自动启动Session的话,在使用Session之前一定要调用session_start()函数启动Session。 再次打开login.php,添加以下以粗体显示的代码,以记录用户信息。 <? ...... if ($rs && $rs->num_rows > 0) { // 使用session保存当前用户 session_start(); $_SESSION['uid'] = $username;
// 在实际应用中可以使用前面提到的重定向功能转到主页 $errmsg = "登录成功!";
// 更新用户登录信息 $ip = $_SERVER['REMOTE_ADDR']; // 获取客户端的IP $sql = "UPDATE t_user SET f_logintimes = f_logintimes + 1,"; $sql .= "f_lasttime=now(), f_loginip='$ip' "; $sql .= " WHERE f_username='$username'"; $db->query($sql); } ...... ?> 不要滥用Session,Session最大的作用是在页面之间维持状态。许多初学者在掌握Session技术后,很容易将Session作为存储 数据的法宝,在Session里放置很多数据。由于这些数据直到Session过期才会被释放,因此会给服务器带来很大的负担。 2.7.5 判断用户是否已登录
既然上一小节已经完成了将用户名保存到Session中的工作,判断用户是否已经登录就很简单,代码如下: <?php session_start(); if (empty($_SESSION['uid'])) { echo "您还没有登录,不能访问当前页面!"; exit; } ?> 通过判断自动全局变$_SESSION中的uid是否为空,就可以判断用户是否已经登录。如果用户没有登录,就提示其无法访问当前页面,并终止程序的运行(或者使用一条重定向语句将页面导向登录页)。
2.8 多学两招——调试数据库处理错误
第1章中介绍了常见的数据库连接错误的类型及其表现和解决方法。在PHP数据库编程中,除了会碰见数据库连接错误外,更常遇见的是执行SQL查询时发生的 错误。解决SQL语句错误的方法有二:一是让PHP显示错误信息,二是把最终执行的SQL语句显示在网页中,然后复制到MySQL命令行程序中去执行,让 MySQL本身来提示错误信息。这两种方法本质都是一样的,因为PHP本身并不会执行SQL语句,它只是把SQL语句发送给MySQL让MySQL来执行 并获取执行结果,所以PHP显示的提示信息也是从MySQL数据库中得到的。 要让PHP显示SQL语句的执行错误,首先要打开PHP配置文件php.ini中的显示提示信息的选项,即设置"display_errors = On"。然后在程序中使用echo或print等输出语句打印出数据库的错误,示例如下: <?php $db = @new mysqli("127.0.0.1", "developer", "123456", "test"); $sql = "SELECT * FROM t_user WHERE username='zhangsan'"; $rs = $db->query($sql); if ($rs) { //...... } else { echo $db->error; } $db->close() ?> 以上在调用mysqli对象的query()函数执行SQL语句之后,判断执行结果是否正确,如果不正确则使用echo语句把执行错误显示出来,其中mysqli对象的error成员变量中保存了最近一次产生的数据库错误。 将上面的代码保存成debug_db.php并在浏览器中运行,结果页面会输出提示:Unknown column 'username' in 'where clause'。这样就知道了错误的产生是因为在SQL语句中把字段名"f_username"误写成了"username",把字段名改正过来即可解决 问题。 在MySQL命令行中执行SQL语句查看错误,可以得到相似的错误提示信息。将上面代码中echo $db->error;语句改成echo $sql; 重新执行以后页面中会输出:SELECT * FROM t_user WHERE username='zhangsan'。将该SQL语句复制下来,然后按第1章介绍的方法打开MySQL命令行程序,把复制的内容粘贴上去并执行(右击 后在弹出菜单中选择"粘贴")。结果如图2-19所示。 2.8 多学两招——调试数据库处理错误
第1章中介绍了常见的数据库连接错误的类型及其表现和解决方法。在PHP数据库编程中,除了会碰见数据库连接错误外,更常遇见的是执行SQL查询时发生的 错误。解决SQL语句错误的方法有二:一是让PHP显示错误信息,二是把最终执行的SQL语句显示在网页中,然后复制到MySQL命令行程序中去执行,让 MySQL本身来提示错误信息。这两种方法本质都是一样的,因为PHP本身并不会执行SQL语句,它只是把SQL语句发送给MySQL让MySQL来执行 并获取执行结果,所以PHP显示的提示信息也是从MySQL数据库中得到的。 要让PHP显示SQL语句的执行错误,首先要打开PHP配置文件php.ini中的显示提示信息的选项,即设置"display_errors = On"。然后在程序中使用echo或print等输出语句打印出数据库的错误,示例如下: <?php $db = @new mysqli("127.0.0.1", "developer", "123456", "test"); $sql = "SELECT * FROM t_user WHERE username='zhangsan'"; $rs = $db->query($sql); if ($rs) { //...... } else { echo $db->error; } $db->close() ?> 以上在调用mysqli对象的query()函数执行SQL语句之后,判断执行结果是否正确,如果不正确则使用echo语句把执行错误显示出来,其中mysqli对象的error成员变量中保存了最近一次产生的数据库错误。 将上面的代码保存成debug_db.php并在浏览器中运行,结果页面会输出提示:Unknown column 'username' in 'where clause'。这样就知道了错误的产生是因为在SQL语句中把字段名"f_username"误写成了"username",把字段名改正过来即可解决 问题。 在MySQL命令行中执行SQL语句查看错误,可以得到相似的错误提示信息。将上面代码中echo $db->error;语句改成echo $sql; 重新执行以后页面中会输出:SELECT * FROM t_user WHERE username='zhangsan'。将该SQL语句复制下来,然后按第1章介绍的方法打开MySQL命令行程序,把复制的内容粘贴上去并执行(右击 后在弹出菜单中选择"粘贴")。结果如图2-19所示。 图2-19 在MySQL命令行中调试执行SQL错误
|