使用 CodeIgniter 框架快速开发 PHP 应用(六)
web编程 网络
原文:使用CodeIgniter框架快速开发PHP应用(六)简化使用Session和安全理论说得够多了!现在让我们开始写我们自己的应用。在这一章里,我们将会大致描述一下我们要建立的一个应用系统,而且我们分析一些会影响到网站系统的的基本问题也就是会话管理和安全。在这一章,我们将会见到:。如何使你的网页安全。如何使用CI的会话类开始用CI设计一个实际的网站我们已经看过CI安装时生成的welcome页…
原文:使用 CodeIgniter 框架快速开发 PHP 应用(六)

www.mysite.com/index.php/start/assessme"/>

我已经稍稍解释了 assessme函数,它接受一个用户/密码组合,我需要在一个数据库中查询它。为了要使代码更结构化, 我们要引入一个函数叫做checkme()。

所以,你会见到, assessme() 调用 checkme(),Checkme()在数据库里检查用户是否存在,密码是否有效,然后把'是' 或 '不' 返回给assessme()。如果返回是, assessme() 调用另外一个函数,mainpage(),返回一个视图。

注意代码模块好的好处。 每个函数扮演一个角色。 如果我需要改变系统检查密码的方式,我只需要改变 checkme() 函数。如果我需要修改页面,我只要修改 mainpage() 函数。

让我们看看代码结构以及各部分如何交互。 (注意为了要使例子简洁,我们不校验用户的输入。 当然,这不会造成问题, CI 的表格类自动为输入数据“消毒”。

/*receives the username and password fromthe POST array*/
   function assessme() {
       $username = $_POST['username'];
       $password = $_POST['password'];

       /*calls the checkme function to see if the inputs are OK*/
       if ($this->checkme($username,$password)=='yes') {
           /*if the inputs are OK, calls the mainpage function */
           $this->mainpage;()
       }
       /*if they are not OK, goes back to the index function, which re-presents the log-in screen */
       else
       {
           $this->index();
       }
   }
   /*called with a u/n and p/w, this checks them against some list. For the moment, there's just one option. Returns 'yes' or 'no' */

   function checkme($username='', $password='') {
      if ($username == 'fred' && $password =='12345') {
          return 'yes';
      } else {
          return('no';
      }

   }

在第 5 行-6 上, assessme() 接收来自表格的$_POST数组,包含以下一些数据:

[username] => fred 
[password] => 12345

assessme() 函数把这二个变量传递给另一个函数checkme()。该函数检测具有密码12345的用户fred是否是合法的用户, 如果他们是, 它返回 'yes'. 显然地,在一个真正的网站上,这会更复杂。 你或许会为有效的用户名/密码做数据库查询。 把它定义成一个单独的函数意味着我可以测试我的代码的其它部分, 并在稍后改进 checkme()函数。

如果使用者名称和密码是一个有效的组合, assessme()函数调用另一个函数 mainpage(),让用户进入网站。否则,它返回到index()函数-即,再次显示登录窗口。

下一个问题是如何处理状态。换句话说,当进入到另一页面时如何识别这是一个已登录的用户。

安全/会话: 使用另一个 CI 类

如果我想要建立一个会话机制确保未授权用户无法存取我的文件, 需要多少代码?

英特网通过一系列请求工作。 你的浏览器给我的服务器发请求,要求某一特定页。 我的浏览器把该页处理后返回你的服务器。服务器处理后向浏览器发出另一页面,你再按下一个超链接,对我的服务器提出新的页面请求。如此而已。

英特网是'没有状态的'-就是, 你的浏览器对我的服务器做的每个请求都被当做一个单独事件,HTTP协议没有办法把你的请求和任何其他的请求联系起来。

如果你想要你的网站页面与页面之间建立联系,你必须管理“状态”,要服务器记得哪些请求来自你的浏览器, 需要做一些特别的处理。

PHP 提供管理状态的二种办法: 使用cookie、或使用会话。PHP的session自动地检查你的浏览器是否接受cookie. 如果不接受,它会使用会话ID的方法,这个ID直接通过URL进行传递。

cookie是小块的数据由服务器发送到你的浏览器。 浏览器自动地保存它。 一旦保存了cookie,当浏览器尝试访问网站的时候,网站就会检查是否存在cookie。 如果它找到正确的cookie,它能读取它里面的数据适当地配置它本身。可能关闭特定页面,或显示你的个人数据。


因为一些人设定他们的浏览器不接受cookie, PHP 提供其它变通的方法。 每次当浏览器请求访问时,网站产生 '会话ID', 把它发送到浏览器端,当浏览器发送下个请求时把这个ID加入URL,所以网站能够识别浏览器。

CI 有一个会话类来处理会话相关工作。 事实上,它大大减少了编码量。我们在上一章学习到 CI 有各种各样的library,大大简化了PHP的编程。 这些就是框架的核心: 集成大量代码,为你提供各种强大的功能。你要做的只是使用它们,而不必关心他们是如何运作的。作为结果,你的应用使用了最专门的代码,而你却不需要自己去编写它们!

如果你想要在你的控制器内或模型中使用某个类内的函数,你一定记得首先要装载这个类,(有些类, 比如 config 总是自动地被装载,这是为什么我们不需要在代码中装载它的原因。) 

你可以这样很简单地装载library:

$this->load->library('newclass');

通常,把这一行放入你的控制器或模型的构造函数中。

如果你认为你会在每个控制器中使用某个library,你能象config类一样自动地装载它。找到/system/application/config/autoload文件, 增加你想自动装载的类名:

$autoload['libraries'] = array();

因此它看起来像这一样:

$autoload['libraries'] = array('newclass',' oldclass');

在这里,我们首先要用到session类,这帮助你维持状态, 而且可以识别用户。 做到这一点相当简单。下面列出增强的assessme()函数:

   function assessme() {

      $this->load->library('session');

      $username = $_POST['username'];
      $password = $_POST['password']; 
    
   if ($this->checkme($username, $password) == 'yes') {
       $this->mainpage;()
   } else {
       $this->index();
   }    
}

(你可以看到,在函数一开始就装载session library,但是通常,我们将会在控制器的构造函数中装载它,因此,在这一个类中的其它函数都可以使用这个类。) 

当这个类装入后,立即通过它本身强大的功能为你提供对会话的读取,创建,修改等功能。

好吧, 坦白地讲, 不仅仅就一行代码那么简单。 你必须首先修改 config 文件, 告诉会话类你想要它做的。

检查你的system/application/config/config.php 文件,你会找到像这样的一个部分:
--------------------------------------------------------------------------
| Session Variables
|--------------------------------------------------------------------------
|
| 'session_cookie_name' = the name you want for the cookie
| 'encrypt_sess_cookie' = TRUE/FALSE (boolean).  Whether to encrypt the cookie
| 'session_expiration'  = the number of SECONDS you want the session to last.
|  by default sessions last 7200 seconds (two hours).  Set to zero for no expiration.
|
*/
$config['sess_cookie_name']          = 'ci_session';
$config['sess_expiration']           = 7200;
$config['sess_encrypt_cookie']       = FALSE;
$config['sess_use_database']         = FALSE;
$config['sess_table_name']           = 'ci_sessions';
$config['sess_match_ip']             = FALSE;
$config['sess_match_useragent']      = FALSE;

现在,确定 [sess_use_database] 被设定成FALSE。

保存设置,现在,每次当你的客户登入网站,网站会在你的机器上保存一个cookie,包含下列的数据:

。CI生成的一个唯一的SESSION ID,这是CI为这个会话生成的任意组合的字符串。

。使用者的 IP 地址

。使用者的用户代理数据 (浏览器数据的最初 50个字符)

。最大有效时间

如果你设定 sess_encrypt_cookie 为FALSE, 你能在你的浏览器上看到cookie的内容:(它部分被编码了,但是你能辨认出来):

ip_address%22%3Bs%39%3%22127.0.0.1%22%3Bs%310%3A22

包括使用者的URL, 本例为127.0.0.1). 如果你设定 sess_encrypt_cookie 为TRUE, cookie被加密, 只是一行无意义的代码。 浏览器不能识别它,当然使用者也无法修改它。

当使用者发出另一个页面请求时, 网站会检查会话ID是否已经以cookie的形式保存在用户的机器里,如果有,这会作为一个已经存在的会话的一部分,如果没有,就会建立一个新的会话。记得在所有的其它控制器上加载会话类。CI将作同样的检查。我必须做的只是告诉每个控制器当没有cookie时该如何运作。

使会话更安全

这本来不会造成安全问题。任何访问网站的用户都会开始一个会话。代码只是判断他们是否是一个新用户。 避免未认证的用户访问某些页面的一个方法是如果他们已经登录,就在cookie中加入一些信息,因此我们可以对此进行验证。一旦用户输入正确的用户名和密码一次,就会在cookie中被记录,当发出新的请求時,会话机制会检查cookie,如果是已登录的用户,就返回用户请求的页面,如是不是,就返回登录画面。

把信息加入cookie很容易。 在我的 assessme() 控制器中,如果密码和使用者名称验证通过,加上如下代码:

if($this->checkme($username, $password)=='yes') {
   $newdata = array(
                    'status' => 'OK',
                    );
   $this->session->set_userdata($newdata);
   $this->mainpage();


这样会把$newdata数组中内容-上例中只有一个变量-加入到cookie中。现在,每当密码/用户名组合是可接受的, assessme() 将会把 'OK' 加入cookie,而且我们可以把这些代码加到每个控制器中:

   /*remember to load the library!*/
   $this->load->library('session');

   /*look for a 'status' variable in the contents of the session cookie*/
   $status = $this->session->userdata('status');

   /*if it's not there, make the user log in*/
   if(!isset($status) || $status != 'OK')
       { /*function to present a login page again…*/}

       /*otherwise, go ahead with the code*/

这样,你在你的网站周围构建起安全的地基。 你可以很容易地使它变得更精细。 举例来说,如果你的部分用户有更高级的权限, 你可以在cookie中保存了个存取级别码,而不是只放进一个“OK”,你能用这个进行更细致的权限调控。

在一个cookie中存放明码数据会比较糟糕,因为用户可以打开cookie并看到它的内容,当然也可以很容易地修改它。因此使用CI加密cookie会是一个很好的办法。还有,你可以在数据库中建立一个用户表,在用户登录以后修改用户表的登录状态,在用户表中维护一些权限属性,每次访问时,从数据库的用户表中读取权限信息,也是一个比较好的做法,如果结合缓存技术,就会解决系统的性能问题。

在你的数据库中保存会话数据非常简单。 首先,创建数据库表。 如果你正在使用 MySQL, 使用这一 SQL 语句:

CREATE TABLE IF NOT EXISTS  `ci_sessions` (
session_id varchar(40) DEFAULT '0' NOT NULL,
ip_address varchar(16) DEFAULT '0' NOT NULL,
user_agent varchar(50) NOT NULL,
last_activity int(10) unsigned DEFAULT 0 NOT NULL,
status varchar(5) DEFAULT 'no',
PRIMARY KEY (session_id)
);

然后,修改在system/application/config/database.php 文件中的连接叁数告诉 CI 数据库在哪里。详细介绍参见第 4 章

如果工作正常的话, 当你登录或退出时,在你的数据库表中增加了相关的数据。如果你在一张数据库表中储存会话, 当用户登录到你的网站,网站会查找cookie, 如果它找到了,就会读取会话ID,并用这个ID去匹配数据库表中保存的ID。

你现在拥有一个强健的会话机制。 而这一切只需要一行代码!

一点声明,PHP本身的会话类在用户关闭cookie功能时能够应付。(取代生成cookie,它把会话数据加入URL) CI 类不会这样做,如果使用CI,一旦用户禁止cookie,他就不能登录到你的网站。这方面需要增强功能。 希望Rich将会很快升级CI。

安全

注意到:会话类会自动地保存关于访问者的 IP 地址和使用者的资料。你可以使用一些增强的安全措施。

通过修改config文件的二处设置可增加安全性:

。 sess_match_ip: 如果你设定这个参数为TRUE, 当它读取会话数据时,CI 将会尝试相配使用者的 IP 地址。 这将预防使用者使用'抢'来的cookie信息。但是,有些服务器 (ISP和大公司服务器) 可能存在不同 IP 地址上的相同使用者登录的请求。 如果你设定这个参数为TRUE,可能会给他们造成麻烦。

。 sess_match_useragent: 如果你设定这为TRUE, 当读取会话数据的时候, CI将会试着匹配使用者代理。这意谓试着“抢夺”会话的人还需要匹配真正用户的user agent设置。 它使“抢夺”变得稍稍有些困难。

CI 也有一个 user_agent 类, 你可以这样装载:

$this->load->library('user_agent');

一旦装载, 你能要求它返回访问你网站的浏览器和操作系统的各种信息, 而不管它是一个浏览器,手机或机器人。 比如,如果你想列出叁观你的网站的机器人,你可以这样做:

$fred = $this->agent->is_robot();
if ($fred == TRUE)
    {$agent = $this->agent->agent_string();
/*add code here to store or analyse the name the user agent is returning*/
}

类通过装入后开始工作, 与用代理,浏览器,机器人和其它的类型的数组作比较,这个数组存放在system/application/config/user_agents。

如果你愿,你可以容易开发出使你的网站锁定特定机器人,特定浏览器类型的功能。 不过,记得一个攻击者很容易写出一个代理类型,并返回他想要扔给你的那种类型。因此,他们能容易地伪装成通常的浏览器。 许多机器人,包括象CI 的 user_agents 数组已列出的 Googlebot,是'品行端正的'。 这意味着如果你设定你的 robots.txt 文件排除他们,他们将不会强行攻击。没有容易的方法去阻击一个不知名的的机械手,除非你预先知道他们的名字!

在 CI框架中 ,会话机制保存那发出请求的 IP 地址,因此你可以使用这个功能维护一个网站的黑名单。可以用如下代码取回来自会话的 IP:

/*remember to load the library!*/
   $this->load->library('session');
/*look for an 'ip_address' variable in the contents of the session cookie*/
$ip = $this->session->userdata('ip_address');

因此你能针对黑名单作相应的安全处理,比如拒绝登录。

你也可以用CI 的会话机制限制来自重复请求的损害-像是一个机器人通过重复地请求网页来使你的网站超载。 你也可以使用这一个机制处理 '字典'攻击,即一个机器人不断重复尝试数百或者数以千计的密码/用户名组合直到它找到正确的一组。

因为 CI 的会话类保存每个会话的last_activity ,所以你能做到这一点。 每次,当网页被请求时, 你能检查多久以前这个IP地址的用户发出一请求,两次请求的间隔如果不正常,你可以终止会话,或放慢响应的速度。

摘要

我们已经概略介绍了我们想要建立的一个应用, 和几乎所有应用都需要解决的问题:会话管理和安全保证。

为了做到这一点,我们已经学习了CI会话类的一引起细节,而且见到它如何生成会话记录和在访客的浏览器中生成cookie。

当后续的请求发生时,你可以读取cookie对响应进行控制。