altas(ajax)控件(十八):拒绝机器人自动提交程序的控件NoBot
一、 简介
为什么各大网站的注册系统都有图片验证的要求?
因为如果没有图片验证系统,那么机器人爬虫将自动登录网站,如果网站给每个登录
用户分配一点内存,那无数的机器人自动登录网站将使网站的负载大大增加,甚至网站将就此瘫
痪。但是图片验证系统编写太复杂,atlas给了一个图片验证系统的替代者-NoBot。但是需要说明
的是,最佳方案还是图片验证,看了NoBot的原理你就明白为什么NoBot无法取代图片验证。
人类提交的速度不可能很快,而机器人的提交速度在微秒级别,NoBot正是验证提交的间隔时
间是否长和客户端是否为真正的浏览器,来判断提交是不是人类的正常提交。这样就有一个问题,
如果机器人模拟人类的操作,那么NoBot就无效了。
二、 属性说明
NoBot控件在页面中是完全不可见的,这和我们前面介绍的atlas系统不太一样。不过从增强用户体验的角度来看,NoBot却的确是一大进步,它也正符合了Ajax的最根本设计目标——提高用户体验。
声明NoBot控件的语法将类似如下所示:
<ajaxToolkit:NoBot
ID="noBot"
runat="server"
ResponseMinimumDelaySeconds="2"
CutoffWindowSeconds="60"
CutoffMaximumInstances="5"
OnGenerateChallengeAndResponse="noBot_GenerateChallengeAndResponse" />
- ResponseMinimumDelaySeconds: 一个合理的客户端从开始接受页面到提交表单的时间间隔,单位为秒。在该时间段之内的提交将被认为是机器人所为。
- CutoffWindowSeconds:指定一个统计同一客户端提交次数的窗口时间段,单位为秒。在该时间段之内的提交次数超过CutoffMaximumInstances所指定的值将被认为是机器人所为。
- CutoffMaximumInstances:指定在窗口时间段内同一客户端最多的提交次数。在CutoffWindowSeconds所指定的时间段之内的提交次数超过该值将被认为是机器人所为。
- OnGenerateChallengeAndResponse:指定GenerateChallengeAndResponse事件的处理函数。在该事件处理函数中我们可以设定强制浏览器执行的一段JavaScript以及其预期的执行结果。若浏览器的执行结果和预期结果不符,则本次提交将被认为是机器人所为。
三、 实例
(转自Dflying Chen的博文)
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(秒)。
对于CutoffMaximumInstances属性,这里我们设置为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枚举的可选值有如下几种:
- Valid:表示验证通过。
- InvalidBadResponse:表示前面自定义的JavaScript脚本(即e.ChallengeScript)的运行结果和预期结果(即e.RequiredResponse)不符,验证失败。
- InvalidResponseTooSoon:表示客户端完成表单的时间小于ResponseMinimumDelaySeconds所指定的时间,验证失败。
- InvalidAddressTooActive:表示客户端在CutoffWindowSeconds指定的窗口时间内的请求超过了CutoffMaximumInstances所指定的数量,验证失败。
- InvalidBadSession:表示会话状态验证失败,可能是客户端并没有保存会话状态,验证失败。
- 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);
}
}
}