MLDBM模块可以很容易地实现数据持久性。本文将给出一个基于Web的投票系统的真实的例子,重点介绍如何利用最小的外部模块、如何舍弃基于客户机的cookie以及如何利用CGI属性的优点。软件正日益变得更加复杂,这并不是什么秘密;我们也看到一些额外的层次被添加到系统中,以保持软件组件的模块化。最重要的结果是,这些系统现在更易于维护,而且可扩展性也更好;但是有时这些技术有过多的重复,会导致软件的过度设计。在另外一些情况下,开发开发人员宁愿选择一些过度复杂但却非常有名的技术,也不愿意集成一些简单但却不太熟悉的技术。
不管怎样,如果所有人都有一把锤子,那么每个问题看起来都不过像一颗钉子而已。
我最近被请求为一所大学的学生组织设计一个小程序,以统计选举票数。这是一个非常简单的项目,每周处理的学生请求不会超过500个;之后该程序会立即统计并发布结果。
由于这个项目对服务级别的要求很低,因此使用一个外部数据库来处理查询并没有什么好处。相反,使用脚本可以直接快速读写数据结构。不过,我仍然希望能够将一些经过良好设计的功能封装在一起,而不是采用一些像意大利面那样,将杂乱的代码拼装在一起。我希望可以采用一个经过仔细考虑的自成体系的设计,该设计将提供一些简化的部署。
Perl看来是这个项目的首选语言——在很多平台上似乎都受到支持,此外,在Perl的知识库CPAN中,还有很多方便的库可以使用。
对于底层架构来说,CGI(CommonGatewayInterface)是第一种广泛用来扩展Web服务器从而提供交互内容的方法。开发人员通常会鼓吹一些新的标准,例如JSP、.NET、mod_perl、PHP和ISAPI,这些技术也的确可以弥补CGI的一些不足。但是在这个项目中,我们只需要对几百个用户计算投票数,这样一个CGI脚本很难构成一个大型的应用程序,因为所有的投票信息都可以放到Web服务器的系统RAM中。在用户每次提交一个读写数据的请求时,这可以将要查询的整个表装入内存中。
还有,通过将逻辑数据分隔成3个不同的物理文件,可以实现填写选票、确认选票和统计结果的逻辑顺序;这样做可以最大限度地减少打开已加锁的文件。
如果一个事务在很偶然的情况下因为加锁的文件而失败了,那么这并不会产生实际的问题。不管一个事务是由于网络问题还是加锁文件而失败的,结果都是相同的:用户只需再等待一会儿即可,选票随后很有可能对其中的一次尝试进行统计。我们应该记住这种行为,然而,对于不同的应用程序来说,情况并非总是如此,因此可能无法处理并发事务。
对于这个项目来说,CGI提供了以下几个优点:
注意,在这个例子下,每个选票都不会被统计两次。在那些确定需要保密的情况中,可以使用一个简短的JavaScript函数来隐藏结果。诚然,有些人可能希望完全采用匿名投票,但是由于俱乐部的选举通常都是通过举手表决的,因此这很难实现安全的投票。
在考虑这种工作流程模式时,我意识到使用基于GET的验证链接以及使用非加密验证链接的必要性,这样可以进行一些实验,读取这些链接,并基于指定的电子邮件地址和一些已知的验证链接来构建一些错误的确认投票。为了防止这种事情的发生,同时为了仍然能够通过非加密链接进行简单的调试,我决定在验证步骤中添加一、两项内容:为每个预选票添加一个惟一的标识符。
这个标识符是基于操作系统中正在执行的脚本的集成标识符(PID)的。为了让预测验证预选票的URL更加困难,我们可以再使用一个随机数。我之所以关心这个问题,是因为会有一些恶意的用户可能会对非常直观的URL模式进行破解,从而试图构建一些虚假的验证选票。这是代码的一部分,它不会直接转换为一个mod_perl版本,因为它要依赖于正在运行的Perl的PID,以及另外一个随机数。如果这个表单是从一个重用的mod_perl实例中生成的,那么在两次调用之间,PID可能并不需要改变。
然后,我又意识到能使这个链接更具迷惑性的方法是使用一个MD5生成的哈希值,从而有效地隐藏所有投票者的信息。这具有双向受益的优点:既可以使它很难被伪造,同时还维护了基于mod_perl的脚本的可移植能力。缺点是代码有些难以调试,因为需要对客户机与服务器之间交换的信息进行监视。
安装过程要求Web服务器上有三种类型的目录:
$iduid=500(allan)gid=500(allan)groups=10(wheel),48(apache),500(allan)$sudomkdir/var/www/db/var/www/javascript//var/www/css/$sudochmod2775/var/www/db$sudochmod2755/var/www/javascript//var/www/css/$sudochownapache.apache/var/www/db/严格来说,只有cgi-bin(/var/www/cgi-bin)和DBM(/var/www/db)目录是绝对必需的,因为它们分别保存了脚本的可执行文件和投票数据。清单1中给出的文件布局是专用于Linux的,Web服务器进程的用户和组名可能有所不同,但实质上都需要在文件系统的适当地方放上几个Web服务器可以访问的组件。在将支持文件复制到各自的目录中之后,要确保对Web服务器的配置文件(例如httpd.conf)中的别名进行了正确更新。
在创建清单2中所给出的目录之后,将ZIP文件中展开的内容复制到您的系统的类似目录中。其中最重要的是,ballot、DraftBallot.pm、BallotBox.pm和CastBallot.pm文件都需要位于cgi-bin目录中。我们只需要使用3个非标准的Perl模块;安装过程如清单3所示(更详细的信息,请参阅模块的README文件)。
$sudoperl-MCPAN-e'installMLDBM'$sudoperl-MCPAN-e'installMLDBM::Sync'$sudoperl-MCPAN-e'installMIME::Lite'
虽然我可以用一个静态IP地址在拥有已分配的域的站点中建立这种服务,但是我觉得动态DNS应该可以提供一些安全上的好处。通常,如果一个服务器没有静态IP地址,那么来自Web上的访问流量就不可能太大,动态DNS让我们可以在另外一个顶级域名之上临时建立一个可解析的机器名。这样我们就可以在Internet上快速出现,并快速消失,将遭受黑客攻击的风险降至最低。最好的方法是,这种服务是免费的。
还需要指出的是,将服务器配置为监听一个非标准的大一些的端口(例如8000)是很明智的,因为很多ISP都阻塞了端口80上的连接请求。客户机(投票者)通常可以从一个知名的静态地址(例如学校提供的主页)上的链接重定向到投票服务器上。在投票完成之后,提供Web服务的服务器就可以从Web上完全消失了,无需关闭或重新配置这台服务器。其中并没有任何缺点可以影响到所引用的页面(这台服务器是由其他人进行管理的)。在一些对政策敏感的环境中,这种考虑尤其重要。(有关使用动态DNS的更详细内容,请参阅
浏览器可以使用GET和POST方法将数据传递到所引用的页面中,从而对状态进行维护;也可以通过传递给服务器上的消息头中包含的cookie信息对状态进行维护。为了确认一张选票是从一个真实的人(至少是从一个有效的电子邮件帐号中)那里发出的,应该先将预选票发送到一个电子邮件地址进行确认。此外,cc:或bcc:消息也可以在以后引用。正如我前面介绍的一样,实现这种功能的最直接的方法是向投票者发送一个HTTPGET结构化的链接。当然,有些作者会宣称用来更新记录的GET方法并不好用。但是在这种情况下,任何这之后单击某一个链接的用户都只会接收一条更新消息,并且可以从这条消息了解每个候选人的目前有效选票,因此,这是无害的。
在使用这个脚本时,还要考虑其他一些安全问题,我们也应该考虑这些问题。任何允许外部实体来输入数据的程序都容易受到恶意的攻击,例如缓冲区溢出和嵌入式控制字符。反之,使用专用的程序来读写本地DBM文件至少具有以下优点:在没有SQL后门的地方,是不可能存在SQL插入攻击的。
在您同意需要对到达的数据进行过滤之后,我要将变量$CGI::DISABLE_UPLOADS和$CGI::POST_MAX设置为非常严格的值。另外我建议采用如下设置:
