使用ASP.NET AJAX Control Toolkit中的NoBot控件拒绝垃圾发布程序

本文来自我即将出版的《ASP.NET AJAX程序设计 第I卷 服务器端ASP.NET AJAX Extensions与ASP.NET AJAX Control Toolkit(暂定名)》第10章第1节。请各位朋友不吝给出建议和意见。

 

10.1 NoBot:拒绝机器人程序

NoBot 控件可以为页面中的表单提供类似CAPTCHA[注释1]而却无需任何用户操作的验证,以阻止机器人程序自动提交垃圾信息。

 

10.1.1 应用场景

网络上的垃圾信息似乎无处不在,从前是垃圾邮件、广告等。而现在,这些无孔不入的垃圾信息发布者又盯上了互联网上的各大网站。各种机器人程序(Bot)应运而生,它们可以自动在网络上爬行并寻找带有评论或留言功能的页面,随即自动填写表单并提交,其提交垃圾信息的数量和质量更是让传统的手工发布者自愧不如。管理者往往一夜之间发现自己的网站下已经多了成千上万条广告,不但让真正有用的信息淹没于其中,更是让网站在性能上不堪重负。

由此,很多解决方法同样应运而生,其中最著名的当属各种验证图片了。这种验证图片中的文字由计算机随机生成,并尽其所能地对其进行扭曲、变形、修饰、模糊,最终要达到的目的是只有聪明的人类才能够分析出其中的内容(如图10-1所示),而当前水平的计算机却只能够望“图”兴叹。然后将用户对这幅图片的识别文本随表单一起发送至服务器。这样,服务器即可通过检查客户端输入的识别文字的正确与否来判断这是否是人类所为,也就达到了区分机器人程序和人类的目的。

图10-1 极其复杂的验证图片

目前为止,这种做法非常有效,因为计算机图像处理能力的发展还不足以完全正确地识别出如此复杂的图片中的文字。不过这样做的缺点也很明显——麻烦!每次提交一次表单都需要用户仔细睁大双眼分辨验证图象中的内容,并且输入一串毫无意义、没有任何连贯性的字符,真的是一种痛苦!而且,对于视力不便的残障人士来说,这种验证图片更是将其拒之门外。

于是,某些网站提供了另外一种可选项,即对于视力不便的残障人士,可以选择听一段朗读,并输入其中读过的字母,通过声音的方式来分辨人类和计算机,例如Hotmail的注册页面,如图10-2所示。

图10-2 以识别声音的方式来分辨人类和计算机

这样似乎考虑得很周全了……不过,如果某个用户不懂得英文,那岂不还是不能使用么?世界上有这么多种语言,难道要提供每种语言的版本?而且,即使提供了所有语言的版本,对于麻烦这个致命的问题,也仍旧是无法解决啊!

有没有一种无需用户操作的,对用户完全透明的验证机器人程序的解决方法呢?ASP.NET AJAX Control Toolkit中提供的NoBot控件即提供了这样一种相对来说比较折衷的解决方案。

 

10.1.2 声明语法以及常用属性

NoBot控件可以通过如下四种方式较为准确地判断出进行当前操作的是否为人类:

  1. 让客户端浏览器执行一段JavaScript,并判断其执行结果。机器人程序一般只是取得HTTP流的内容,对其分析并填写其中表单之后即提交,这个过程中并不包含对浏览器功能的使用,也就更不会解析并运行页面中的JavaScript得到正确的运行结果。且这段JavaScript既可以是一段简单的纯数学运算,例如123*4455=?,也可以是一些非常复杂的DOM操作,例如动态创建一个<div>,并返回它的位置等。这样即强迫该程序只能够在浏览器中使用,大多数机器人程序显然对此等计算无力回天。
  2. 判断客户端是否保存了本次会话状态。一般来讲,只有浏览器才会对会话状态进行关注并保存,而简单的机器人程序则会完全忽略会话状态信息。
  3. 判断客户端从开始接受页面到提交表单的时间间隔。机器人程序都比较讲究“效率”,加上计算机的强大运算能力,几乎可以在接收到页面之后的瞬间就完成表单的填写并提交回服务器。而对于人类,显然不可能在如此短暂的几秒钟时间之内就完成这样复杂的一张表单。
  4. 判断某段时间之内某个客户端的提交次数。同样,对于人类来说,没有能力也没有意义在比如一分钟之内填写同样的表单100次,而对于机器人程序,则这很有可能是它们的一贯作风。

以上的四种方法,虽不能百分之百地完全阻止机器人程序,然而在大多数情况下还是非常有效且相当精确的。且使用这种方法最大的优势就在于它省去了对用户交互的需要,让程序更加友好易用。

NoBot控件在页面中是完全不可见的,这似乎和我们潜意识中Ajax程序的那些眩目的界面效果没什么关系。不过从增强用户体验的角度来看,NoBot却的确是一大进步,它也正符合了Ajax的最根本设计目标——提高用户体验。

声明NoBot控件的语法将类似如下所示:

<ajaxToolkit:NoBot
    ID="noBot"
    runat="server"
    ResponseMinimumDelaySeconds="2"
    CutoffWindowSeconds="60"
    CutoffMaximumInstances="5"
    OnGenerateChallengeAndResponse="noBot_GenerateChallengeAndResponse" />

NoBot控件继承于System.Web.UI.WebControls.CompositeControl,并间接继承于System.Web.UI.WebControls.WebControl,也就拥有了这些控件的所有属性/方法/事件,声明NoBot控件时所常用的属性标签如表10-1所示。

表10-1 声明NoBot控件时的常用属性标签[注释2]

  1. ResponseMinimumDelaySeconds: 一个合理的客户端从开始接受页面到提交表单的时间间隔,单位为秒。在该时间段之内的提交将被认为是机器人所为。
  2. CutoffWindowSeconds:指定一个统计同一客户端提交次数的窗口时间段,单位为秒。在该时间段之内的提交次数超过CutoffMaximumInstances所指定的值将被认为是机器人所为。
  3. CutoffMaximumInstances:指定在窗口时间段内同一客户端最多的提交次数。在CutoffWindowSeconds所指定的时间段之内的提交次数超过该值将被认为是机器人所为。
  4. OnGenerateChallengeAndResponse:指定GenerateChallengeAndResponse事件的处理函数。在该事件处理函数中我们可以设定强制浏览器执行的一段JavaScript以及其预期的执行结果。若浏览器的执行结果和预期结果不符,则本次提交将被认为是机器人所为。

 

10.1.3 示例程序:阻止机器人程序的提交

NoBot控件虽然设置起来比较简单,但对其进行合理的配置却并不容易,检查标准太高或太低均难以达到我们的预期目标。接下来让我们通过一个示例程序来分析讨论该控件的使用方法。

首先在新建的页面中添加ScriptManager控件,然后添加一个TextBox和一个Button,用来模拟出一个最简单的输入表单,再加入一个Label,用来显示机器人程序检测是否通过的信息:

<asp:TextBox ID="tbSomething" runat="server"></asp:TextBox> 
<asp:Button ID="btnSubmit" runat="server" Text="Submit" /> 
<asp:Label ID="lbResult" runat="server"></asp:Label> 

接下来是NoBot控件的声明:

<ajaxToolkit:NoBot ID="noBot" CutoffWindowSeconds="10" CutoffMaximumInstances="2" 
  ResponseMinimumDelaySeconds="2" 
  OnGenerateChallengeAndResponse="noBot_GenerateChallengeAndResponse" 
  runat="server" />

我们先来看ResponseMinimumDelaySeconds属性,在该示例程序中,因为表单非常简单,只有一个域,我们将其设置为较短的2(秒)。在实际开发中,我们应该根据表单的复杂程度估计用户填写需要的时间,并相应地配置该属性。例如,对于如Hotmail中复杂的用户注册表单来讲,将该属性值设置为100秒都不足为过——在100秒之内能够将该注册页面填写完成的,除了机器人程序也只有天才了。

对于CutoffWindowSeconds属性,这里我们设置为10(秒)。该属性值越大,则统计的时间段也就越长,判断结果也就愈加有信服力,但同时这样也会造成服务器端更大的开销。一般情况下,将该属性设置为10-100是一个比较合理的选择。

对于CutoffMaximumInstances属性,这里我们设置为2(次),配合CutoffWindowSeconds属性,其含义即为在10秒钟之内同一个客户端最多可以提交2次,超过该次数的提交均被认为是机器人程序。在考虑设置该属性时,我们也要考虑表单的复杂程度并相应地估计用户填写所需要的时间。

而对于OnGenerateChallengeAndResponse属性,即GenerateChallengeAndResponse事件的处理函数,我们将在其中设定强制浏览器执行的JavaScript以及预期的结果,这里将其指定为noBot_GenerateChallengeAndResponse(),函数的名称无关紧要,让开发人员能够理解就好。该事件处理函数的签名如下:

protected void noBot_GenerateChallengeAndResponse(object sender, NoBotEventArgs e) 

注意其中的类型为NoBotEventArgs的参数e,将在稍后用到。在该函数中,我们将动态生成一个随机大小的<div>并添加到页面的DOM树中,且将该<div>的长宽乘积作为预期值保存起来。同时将向页面中写入一段JavaScript,该JavaScript用来在客户端运行时找到该<div>并取得其长宽的乘积。这样在页面回送之后,NoBot控件就可以通过比较预期值与从客户端得到的实际值是否相等来判断客户端是否为真正的浏览器,进而判断客户端是否为机器人。

在noBot_GenerateChallengeAndResponse()方法中,首先新建一个ASP.NET Panel,选择Panel是因为该控件将被呈现为HTML <div>元素,方便得到其长宽属性:

Panel noBotPanel = new Panel(); 

接下来,生成两个随机数,将分别设置到该Panel的长和宽属性上:

Random rand = new Random(); 
int width = rand.Next(80); 
int height = rand.Next(120); 

随后,为这个Panel指定一个随机的ID,指定ID是为了让之后的JavaScript中可以在客户端取到其生成的<div>,而选用随机的ID是为了让程序更加具有不确定性,进一步迷惑机器人程序:

noBotPanel.ID = string.Format("noBotPanel{0}", rand.Next(1000)); 

然后将上面生成的长、宽应用到该Panel上:

noBotPanel.Width = width; 
noBotPanel.Height = height; 

为了不干扰页面的现有布局,我们还要设置一下该Panel的样式,将其隐藏起来:

noBotPanel.Style.Add(HtmlTextWriterStyle.Visibility, "hidden"); 
noBotPanel.Style.Add(HtmlTextWriterStyle.Position, "absolute"); 

注意第一句实际上是设置了visibility: hidden;,而并没有选择我们常用的display: none;。因为若选用后者,则浏览器将认为其大小为0。

然后将该Panel添加为NoBot的子控件,同样是为了避免可能出现的对页面结构的影响:

(sender as NoBot).Controls.Add(noBotPanel); 

然后设置将在浏览器中执行的这一段检验的JavaScript:

e.ChallengeScript = string.Format("var noBotPanel = document.getElementById('{0}'); noBotPanel.offsetWidth * noBotPanel.offsetHeight;", noBotPanel.ClientID); 

注意到这段JavaScript在运行时将首先通过该Panel的客户端ID得到其实际<div>元素的引用,然后使用offsetWidth和offsetHeight得到其实际大小,并将其乘积返回。这段JavaScript赋值给了e.ChallengeScript,即NoBotEventArgs类型对象的ChallengeScript属性上。

最后设置上面这段JavaScript的预期运行结果,非常简单:

e.RequiredResponse = (width * height).ToString(); 

需要注意的是要将预期运行结果赋值给e.RequiredResponse,即NoBotEventArgs类型对象的RequiredResponse属性。

这样,若客户端为真正的浏览器的话,则设置于e.ChallengeScript中的这段JavaScript将正常执行,并如我们所料地返回和e.RequiredResponse中完全一样的预期结果。若是二者不匹配,则即可认为该客户端为忽略了JavaScript的机器人程序。

完整的noBot_GenerateChallengeAndResponse()代码如下:

protected void noBot_GenerateChallengeAndResponse(object sender, NoBotEventArgs e)
{
    Panel noBotPanel = new Panel();
 
    Random rand = new Random();
 
    int width = rand.Next(80);
    int height = rand.Next(120);
 
    noBotPanel.ID = string.Format("noBotPanel{0}", rand.Next(1000));
    noBotPanel.Width = width;
    noBotPanel.Height = height;
    noBotPanel.Style.Add(HtmlTextWriterStyle.Visibility, "hidden");
    noBotPanel.Style.Add(HtmlTextWriterStyle.Position, "absolute");
 
    (sender as NoBot).Controls.Add(noBotPanel);
 
    e.ChallengeScript = string.Format("var noBotPanel = document.getElementById('{0}'); noBotPanel.offsetWidth * noBotPanel.offsetHeight;", noBotPanel.ClientID);
 
    e.RequiredResponse = (width * height).ToString();
}

接下来同样需要编写的还有Page_Load()函数,其中我们将使用NoBot控件进行验证。因为只有在回送时才有验证的必要,所以我们忽略掉页面第一次加载的情况:

protected void Page_Load(object sender, EventArgs e)
{
    if (IsPostBack)
    {
        ……
    }
}

我们将在上述代码中满足IsPostBack的条件下编写我们的验证代码。在其中新建一个NoBotState类型的枚举,NoBot控件的验证结果就将存放于该枚举中:

NoBotState state; 

NoBotState枚举的可选值有如下几种:

  1. Valid:表示验证通过。
  2. InvalidBadResponse:表示前面自定义的JavaScript脚本(即e.ChallengeScript)的运行结果和预期结果(即e.RequiredResponse)不符,验证失败。
  3. InvalidResponseTooSoon:表示客户端完成表单的时间小于ResponseMinimumDelaySeconds所指定的时间,验证失败。
  4. InvalidAddressTooActive:表示客户端在CutoffWindowSeconds指定的窗口时间内的请求超过了CutoffMaximumInstances所指定的数量,验证失败。
  5. InvalidBadSession:表示会话状态验证失败,可能是客户端并没有保存会话状态,验证失败。
  6. InvalidUnknown:未知错误,验证失败。

然后将该NoBotState枚举的引用传递到NoBot控件的IsValid()方法中,该方法将返回一个布尔值,代表验证是否成功。同时,传递进入的NoBotState也将被设定为相应的枚举值。这样,我们即可通过分辨IsValid()方法的返回值判断验证是否成功,并作以相应操作:

if (noBot.IsValid(out state))
{
    ……
}
else
{
    ……
}

在验证通过时,我们将给出一个示例性的提示:

lbResult.Text = "您的信息已经被提交!"; 

若验证失败,则将同样给出详细的错误提示:

string errorMessage = string.Empty;
switch (state)
{
    case NoBotState.InvalidAddressTooActive :
        errorMessage = "该IP地址在短时间内提交了过多的请求。";
        break;
    case NoBotState.InvalidBadResponse :
        errorMessage = "浏览器中检测脚本未被运行或运行结果不正确。";
        break;
    case NoBotState.InvalidBadSession :
        errorMessage = "ASP.NET会话状态不可用。";
        break;
    case NoBotState.InvalidResponseTooSoon :
        errorMessage = "两次回送时间间隔过短。";
        break;
    case NoBotState.InvalidUnknown :
        errorMessage = "未知错误。";
        break;
}
lbResult.Text = string.Format("请求被拒绝,原因:{0}", errorMessage);

出于演示的目的,上述代码才如此耐心地对原因一一解释。在实际的应用程序中,我们完全没有必要如此的“友善”,简单地提示“怀疑为机器人程序”即可,或是更加干脆地用Response.End()结束本次HTTP会话,给机器人程序以颜色,也免得让其了解我们程序中更多的NoBot实现细节。

作为参考,下面列出Page_Load()函数的完整代码:

protected void Page_Load(object sender, EventArgs e)
{
    if (IsPostBack)
    {
        NoBotState state;
        if (noBot.IsValid(out state))
        {
            lbResult.Text = "您的信息已经被提交!";
        }
        else
        {
            string errorMessage = string.Empty;
            switch (state)
            {
                case NoBotState.InvalidAddressTooActive :
                    errorMessage = "该IP地址在短时间内提交了过多的请求。";
                    break;
                case NoBotState.InvalidBadResponse :
                    errorMessage = "浏览器中检测脚本未被运行或运行结果不正确。";
                    break;
                case NoBotState.InvalidBadSession :
                    errorMessage = "ASP.NET会话状态不可用。";
                    break;
                case NoBotState.InvalidResponseTooSoon :
                    errorMessage = "两次回送时间间隔过短。";
                    break;
                case NoBotState.InvalidUnknown :
                    errorMessage = "未知错误。";
                    break;
            }
            lbResult.Text = string.Format("请求被拒绝,原因:{0}", errorMessage);
        }
    }
}

这样即完成了本示例程序,编译并在浏览器中查看该页面,将如图10-3所示。

图10-3 初始化的表单

在文本框中输入一些文字,确保等待了2秒钟之后再提交页面,将看到“您的信息已经被提交!”验证通过信息,如图10-4所示。

图10-4 验证通过

迅速再点一下提交(2秒钟之内),将看到如图10-5所示的“请求被拒绝,原因:两次回送时间间隔过短。”验证失败信息。

图10-5 两次回送时间间隔过短,验证失败

若是在10秒钟之内提交次数超过两次,将看到如图10-6所示的“请求被拒绝,原因:该IP地址在短时间内提交了过多的请求。”验证失败信息。

图10-6 同一IP地址在短时间内提交了过多的请求,验证失败

若是在浏览器中禁用了的JavaScript,则将看到如图10-7所示的“请求被拒绝,原因:浏览器中检测脚本未被运行或运行结果不正确。”验证失败信息。

图10-7 浏览器中检测脚本未被运行或运行结果不正确,验证失败

 

10.1.4 常见问题以及使用技巧

NoBot可以完全代替传统的验证图片么?

不可以。按照当前的计算机技术来看,验证图片将始终是最为精确的、无可替代的辨别机器人程序和真正用户的最佳方法。对于NoBot控件所采用的判断规则,机器人程序均能够通过某种方式进行模拟并巧妙地绕开。且由于NoBot控件需要统计过多的信息,例如某个时间段内每个IP的提交次数、每个页面的提交时间间隔等,也会在某种程度上影响服务器端的执行效率。同时,若NoBot控件配置不当,或是用户使用某些不支持JavaScript的浏览器(例如移动设备中的浏览器),则极易导致较高的误判断率乃至根本无法通过验证,反而影响了用户体验。

而若是配置得当且服务器端资源充沛,则NoBot控件的优势也非常明显。所以,在选择合适的验证方法时,上述问题均要结合实际应用场景考虑周全,并做出恰当的决定。

如何选择强制浏览器执行的JavaScript,即ChallengeScript?

由于这段JavaScript难以调试,所以应该尽可能的简单。同时,为了避免机器人程序的成功预测,其中也要包含相当的不确定性。由此,前面示例程序中演示的创建<div>并检测其高度和宽度的乘积的方法非常适合:既足够简单,也有着相当的随机性,足够让机器人程序难以捉摸。


[1]CAPTCHA即Completely Automated Public Turing Test to Tell Computers and Humans Apart(全自动的公开图灵测试),其目的是让计算机生成区分计算机和人类的程序算法,这种程序必须能够生成并评价出人类能很容易通过但计算机却难以通过的测试。目前常见的验证图片等都属于CAPTCHA。若想了解更多,请访问“The CAPTCHA Project”网站:http://www.captcha.net/。

[2] ID属性起到控件标志符的作用,我们都很熟悉,限于篇幅这里不赘。下同。

posted on 2007-03-16 10:19  Dflying Chen  阅读(8297)  评论(28编辑  收藏  举报