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

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/。

posted on 2007-06-09 22:54  小角色  阅读(746)  评论(0编辑  收藏  举报