5.7 共通功能——由Zend_Auth实现的用户认证功能
从本节开始将介绍本系统所使用的插件(plugin)。插件有如下特点,首先是用于完成了网站的共通功能,其次是在每个页面显示前,会按照注册的顺序执行。
本节首先介绍用户认证的插件以及登录(login)功能,用户登录后,将应用程序执行时所需的用户信息以session的形式保存起来。
要点
这里利用Zend Framework中的Zend_Auth组件来实现用户认证功能,Zend_Auth组件是一个只拥有些基本的功能,检查输入的用户名与密码是否与数据库中的用户名与密码一致,即进行以下的处理:
● 检查用户是否已经认证。
● 登录成功时,显示对应网页(本系统由于采用了特殊的别名处理,登录成功时将转到首页)。
详细的处理会在后面的代码中一一介绍,这里就采用插件方式实现第1项功能这一点做些说明。插件就是在前台控制台处理过程中,所触发的事件中得到执行的代码。可能对这种表述有点难理解,表5-7列举了所有的可用于定义插件的事件,应该能够有助于你的理解。
表5-7 插件中能对应的事件
在4.17节中介绍过,在前台控制台中进行请求的分配处理的各种处理。插件是能够中途插入到这些处理的结构,因此能用于给系统追加共通功能。插件能通过registerPlugin/unregisterPlugin方法进行注册与解除,你可以根据需要进行注册或解除操作。
本节中,使用插件方式实现判断终端用户是否被认证,并取出保存在Cookie中信息。
目录结构
数据库的表结构
本节的插件以及用户注册页面中要用到的表有:用户表(user)与保存用户登录时刻Cookie信息的辅助表、自动登录记录表(autologin)、用户配置信息,分别如表5-8到表5-10所示。
表5-8 用户表(user)
表5-9 自动登录记录表(autologin)
表5-10 用户配置信息(config)
代码
在定义插件(plugin)时,必须继承Zend_Controller_Plugin_Abstract类。AuthPlugin是在前台控制台index.php中事先进行注册的类。
取得已经设置为自动登录的用户ID(uid)。
以Cookie键和当前的时间为条件检索用户ID(uid)。
定义在dispatch循环开始时刻运行的处理。dispatchLoopStartup方法的参数是请求(request)对象。
在Zend Framework中管理session信息的任务是由Zend_Session组件来完成。Zend Framework中由于安全性的考虑,不推荐使用标准的session处理函数。请务必使用Zend_Session组件。
而在Zend_Session组件中是通过Zend_Session_Namespace类来完成session信息管理的。在Zend_Session_Namespace构造函数的参数中指定session信息分组(group)的组名。本系统使用myApp这个名称。下面所有session信息都在myApp这个名称空间下管理着,因此就避免了session信息多了出现冲突的情况。
以“对象名->session名”的形式取得session值。这里判断session信息isLogined值是否为真(即是否通过认证),未通过认证时,进行30到46行的处理。
如果设置了Cookie信息autoLoginKey,并且在其值非空时,进行31到37行的设置session信息的处理。
取得Cookie信息autoLoginKey的值,并将其作为参数传入到getUserid方法中取得用户ID。
成功取得用户ID后,将保存到session变量中,并调用setConfigSession方法设置其他session变量。
将session变量isLogined设为TRUE;
当URI不是登录关联页面(因为只有default/auth/index与default/auth/process是登录关联的action,这里只判断controller是否为auth)时,将module/controller/action等保存在相关session信息中,以便其他程序使用。
动作(action)default/auth/index是表示登录页面的动作。
取得请求对象。
setHeaderInfo是设置本系统所有网页头的共通方法,包括右上角的导航菜单以及所有的分类菜单。
在下面的default/auth/process动作中,当发生登录错误时,也会转到登录网页,并显示错误信息,getBody方法就是在Body对的开头插入错误信息。
default/auth/process是在登录页面的“登录”按钮按下时,被调用的动作,并进行登录处理。
使用Zend_Auth_Adapter_DbTable类来利用数据库中保存的用户信息进行认证确认。Zend_Auth_Adapter_DbTable类的构造函数的参数依次为,连接数据库时使用的连接类、保存用户信息的表名、保存用户ID的列名、保存密码的列名、加密方式(这里采用md5加密方式,将输入的密码用MD5函数处理后再进行认证)。
使用setIdentity/setCredential方法将输入的用户ID与密码设置进去。
authenticate方法进行实际认证了,并将结果返回保存在名为$result的Zend_Auth_Result对象中。
Zend_Auth_Result::isValid方法确认认证是否成功。同时,authUser方法取得用户的有效标示后,判断该用户是否有效(1:有效)。
getResultRowObject方法取得用户表的结果,getResultRowObject方法带两个参数:第一参数是想取得值的列名,如果要取得所有的列值,将其设为NULL;第二个参数是想排除的列名,此处设为“passwd”,即取出除了passwd外的所有列的值。
调用setAutologin方法设置自动登录的Cookie信息(详细请参照Register.class.php)。
设置session变量isLogined,uid,roles。setExpirationSeconds方法是用于设置session的有效时间的,1800表示有效时间为1800秒(即30分钟)。
登录成功后跳转到首先。
登录失败时,在登录页面上显示错误信息。
_forward方法做转向处理,与上面的重定向处理的_redirect方法的内部机制有微妙的差别的,具体请参照本节最后的补充资料。
退出登录状态的动作。
delAutologin方法删除用于自动登录的Cookie信息,更新autologin表,详细请参照后面的Register.class.php。
Zend_Session::destroy方法删除所有有效的Session信息。
退出登录状态后,也将页面跳转到首页。
单击“登录”按钮后,调用default/auth/process动作进行登录处理。
此行以下的链接因为是著作权的关系,都删除了(现设置为链接向首页),请见谅。
类Register原来是容纳用户注册相关函数的类,为了简化,本书删除了用户注册这一部分内容,只保留了与用户登录相关的函数。用户注册是一个很简单的功能,有兴趣的读者可以尝试自行完成这一部分。
构造函数中取得数据库操作对象。
破坏函数中加入关闭数据库操作对象的处理。
本方法是设置自动登录相关的Cookie信息,以及在数据库中保存用户的登录状态。关于自动登录的详细说明请参照本节的补充资料。
判断登录页面的自动登录选项是否选择,1为选择。选中后,进行21行到44行的处理。
设置Cookie的有效周期,此处设置为一年。
为判断autologin表中是否已经有当前用户的自动登录设置,以登录用户ID为条件取得记录数目(不会超过1)。
取得与当前时间相关的乱码,将设置到Cookie信息中。
$expire为Cookie的有效时间,在当前时间的基础上往后推一年(3600*24*365秒)。
设置Cookie值,setcookie方法中的参数依次为:Cookie关键字(autoLoginKey)、Cookie值、有效时间、路径(此处设为根“/”)、主机名(domain)。主机名非常关键,必须正确设为系统所在的主机,否则Cookie值将不能被设置。如你的domain为www.examples.com,那么必须将此项修改为www.examples.com。
表autologin中存在自动登录的信息时,更新有效时间与Cookie值。否则插入对应用户的Cookie值。
退出登录状态方法。
如果存在Cookie值,先删除autologin表中的相关信息,再删除Cookie值。
Cookie值不能直接删除,有重新设置Cookie(设为空)与修改有限时间两种方法,此处将有效时间改为过去的时间(即失效)。为了保证删除Cookie值,这里将两种删除方法都用上了。
设置其他与书签显示有关的session值,这些值可以在系统的设置网页中进行修改。
检索config表,检索出排序标示(按日期或人气的顺序),一页中表示的件数,并将其保存在session中。
补充
向数据库追加新用户时的注意事项
Zend_Auth组件中使用的用户密码必须事先以MD5码的形式登录在数据库中。这样万一其他第三者看到数据库中的用户密码信息,也不至于泄露密码。
但是,需要注意的是,这些只是在保存/比较时的加密措施,不是通信过程中的加密措施。要保证通信过程中的安全性,必须利用SSL(Secure Socket Layer)等措施。
这样当要向数据库中追加新用户或修改密码时,直接操作数据库是不行的。必须事先生成经过加密的字符串,在将这个字符串保存在表的passwd列中。可以采用下述的PHP语句来生成MD5哈希字符串。
实际应用时请替换“password”部分。另外,生成的MD5哈希字符串是32字节的,注意事先将数据库表中保存密码的列设置为32字节。
另外,在向系统追加新用户时,因为还有其他两个表(设置管理表config与个人收藏夹目录管理表folder)与个人用户ID相关,必须向上述两个表中追加管理数据,否则页面将不能正确显示,请务必注意。其中在个人收藏夹目录管理表folder必须追加用户的根目录(parent设为0)。
重定向(redirect)与转送(forward)
重定向(_redirect方法)与转送(_forward方法)都是提供能将现在的页面转移到指定的其他页面,表面上很相似的功能。
但是,从内部处理来说,重定向与转送是两个完全不同的过程。对应用程序开发者来说,认识这两者的区别后,再去使用它是非常重要的。图5-4显示了重定向与转送的内部数据交换过程。
图5-4 重定向(Redirect)与转向(Forward)的比较
在重定向的情况下,页面A的处理结束后,服务器先将应答(response)给客户端,然后再从客户端重新发行对B页面的请求。而转送只是在服务器内进行页面A到页面B的转向。
也就是说,重定向时,服务器与客户端间要进行两次信息交换,而转送只用进行一次。这不仅仅意味着要发生两倍的交互。而且重定向处理中的从客户端发出的第二次请求,将被放在服务器端的请求队列的最末尾,当服务器的访问用户很多时,由于等待的原因会出现延迟的可能性。而对于转送处理,因为只有1次请求的原因,就完全没有上述的担心。
另外,转送处理就如两个action只有一个请求样的处理方式,一系列的请求(request)/应答(response)信息都可以共享的。但是,重定向处理来说,两个页处理是不同请求,这些信息是不能共享的。
但是,今后进行此类的编程时,并不能说一律推荐使用转送(forward)方式。理由如下。
首先,转送处理从性质上说只是限制在同一应用程序内部的页面转移。当要移动到外部网站的情况时,还必须使用重定向处理。
其次,转送处理中,能在目的页面中利用上一页面的请求信息。这是多个action能够相互联动的一个重要原因,但是有时会出现由于继承了上一页面的请求信息,使处理变得复杂的情况(不需要的信息也继承过来了)。只是纯粹的页面移动的情况下(不共享请求/应答信息),原则上,使用重定向处理也没有关系。
自动登录
自动登录是很多网站有的功能,本节展示了一种解决方案。实现自动登录有如下三个步骤。
(1)初次登录时,在客户端的Cookie中留下用户已经登录过的信息。
自动登录一般都必须在用户的客户端浏览器上以Cookie的形式保存登录相关信息,为了安全,通常是时间相关的间接信息,因此前提是必须使浏览器的Cookie设置有效。本节在Cookie中保存的是如下信息。
sha1是PHP提供的加密函数,你也可以采用其他的如MD5等加密函数。另外,uniqid与mt_rand函数是取得随机数的相关函数,可以参照本书最后函数索引,了解其用法。
time是取得当前时间的函数,返回的是现在时刻(相对于1970年1月1日00:00:00)折合为秒的数值,本系统很多地方都要用到。这样将三个函数的返回值连接成一个字符串,从而保证字符串值的唯一性。
最后,使用setcookie函数将上述的信息保存在Cookie中。
autoLoginKey是在Cookie中保存的变量名(实际相当于联想数组的键名),后面必须用这个名字来取得设定的Cookie值。expire是保存的Cookie值得有效时间,当然超过了有效时间Cookie值将被客户端自动删除。当有效时期设置为一年时,则
“/”为提供Cookie的路径(此处设为根目录)。需要特别的提醒是,最后的“localhost”是Cookie得以使用的主机,这里设为是本地,如果你的网站的URL为www.examples.com是,那么必须将此项设为www.examples.com。
(2)在数据库中记录下用户的登录信息。
数据库中至少要保存三个信息,就如本节使用的autologin表一样——用户登录ID、Cookie中保存的值、有效时间。其中Cookie值与有效时间一般在用户每次访问网站时,得到更新,除非用户退出登录状态。
(3)初次以后访问网站时,通过Cookie信息判断是否已经登录过。
首先从Cookie取出登录关键字,并以其为条件检查数据库中是否存在相应的有效记录。如果数据库中存在有效时间内的记录,则将用户置于已经登录的状态。本系统中即将session变量isLogined置为TRUE。
否则,保持用户的未登录的状态或强制用户必须重新登录。
(4)退出登录状态时,删除相关信息。
首先要删除的Cookie中的设定值,遗憾的是,PHP并没有提供删除Cookie值的相关函数,本节使用了如下的两种方式进行删除Cookie值的操作,实际是将Cookie值设为空,以及将其有效时间设置为过去的时刻。为保证万无一失,因此将两种方法都用上了。