AJAX Control Toolkit –
NoBotExtender
Ez egy példaalkalmazáshoz tartozó leírás! Az alkalmazás erről a címről tölthető el:
http://devportal.hu/Portal/Detailed.aspx?NewsId=4bb3a972-9fbb-4cae-975e-9343ac120eb9 vagy
http://cid-8dcaf3b0da4fb828.skydrive.live.com/embedrowdetail.aspx/ACTsorozat/ACT|_XX|_NoBot.zip
Ma egy igazán érdekes és hasznos extender-rel fogunk megismerkedni, még pedig a NoBot-tal, melyet a spam robotok kiszűrésére találtak ki.
NoBot alapok
Manapság egyre gyakrabban találkozhatunk olyan fórumokkal, blogokkal vagy portálokkal, ahol üzenetküldés esetén meg kell adnunk egy képen látható szám-, vagy betűsorozatot. Erre azért van szükség, mert léteznek olyan intelligens szoftverek (ezeket hívjuk robotoknak vagy szimplán botoknak), melyek képesek regisztrálni egy adott oldalra és az ő levelezőrendszerüket felhasználva spamelnek. Ezért egy úgynevezett CAPTCHA (Completely Automated Public Turing test to tell Computers and Humans Apart) technikával szokás megbizonyosodni arról, hogy az üzenet küldője ténylegesen ember. Nagy általánosságban ezek a tesztek olyanok, hogy egy embernek nem jelent különösebb nehézséget megoldania, ellenben egy szoftvernek szinte lehetetlen feladat. A legelterjedtebb ilyen teszt, amikor egy semleges háttéren keresztbe-kasul futnak egyenesek, és ezeken helyezkednek el torzított betűk, illetve számok különböző színekben, méretekben.(Az ilyen dinamikusan generált képeket kacsáknak szokták hívni!)
Az elmúlt pár évben elég sok kutatás volt a CAPTCHA technikákkal kapcsolatban, melyeknek eredményeképp érdekesebbnél érdekesebb tesztek születtek. Az egyik kedvencem az, amikor egy képen látható több figura, melyek max 40%-ban fedik egymást, és néhány alaknak hiányzik egy-két testrésze. Azt kell ilyenkor megadnia a felhasználónak, hogy hány teljes alakzat látható a képen. Azért erre már durva lenne programot írni.
A NoBot fejlesztői viszont úgy gondolták, hogy olyan technikát kéne használniuk, mely nem igényel a felhasználótól semmilyen interakciót, emiatt szinte láthatatlan! Ennek az egyedüli hátránya az, hogy minden egyes postback-et „figyeli a nagy tesó”, emiatt a teljesítménye esik az oldalunknak, ezért inkább csak kisebb oldalhoz ajánlják. A NoBot az alábbi pár technika segítségével tudja kiszűrni a kérés feladói közül a botokat:
– Az oldalkérés és a postback között eltelt időt figyeli (egy ember egy űrlapot nem tud 2 másodperc alatt kitölteni).
– Egy adott IP címről érkező kéréseket számolja (1 percen belül egy ember átlagosan 3-5 oldalt kér le a szerverről).
– Java scripttel kiszámoltat valamit, amit után szerver oldalon ellenőriz (DOM hívás segítségével még azt is le lehet ellenőrizni, hogy böngésző-e az oldalt kérő szoftver).
Az utolsó technikánál saját java kód is megadható, míg az első kettőnél, csak egy-egy küszöbindex!
NoBotExtender használata
A mai példaalkalmazásunk rendkívül egyszerű: lesz egy gombunk, mellyel postback-et válthatunk ki, és lesz egy NoBot vezérlőnk, mellyel ezeket figyeljük. (Természetesen lesz egy „robotságméter”-ünk is két címke képében). Kezdjünk is hozzá a barkácsoláshoz!
Húzzunk a formunkra egy Button-t, egy Nobot-ot, illetve két Label-t, majd állítsuk be őket az alábbi módon:
<asp:Button ID="Button1" runat="server" Text="Kattanj rám!" /><br />
<hr style="width:100px; text-align:left;" />
<asp:Label ID="lbl_state" runat="server" Text=""></asp:Label><br />
<asp:Label ID="lbl_log" runat="server" Text=""></asp:Label><br />
<ajaxToolkit:NoBot ID="NoBot1" runat="server"
ResponseMinimumDelaySeconds="5"
CutoffMaximumInstances="5"
CutoffWindowSeconds="60"
OnGenerateChallengeAndResponse="CustomChallengeResponse" />
A ReponseMinimumDelaySeconds tulajdonság segítségével a kérés és a postback közötti minimális időt szabhatjuk meg másodpercekben. Jelen esetben, ha az oldal megérkezése után 5 másodpercen belül rákattintunk a gombra, akkor robot-nak fog minket minősíteni a rendszer (lentebb ezt majd bővebben kifejtem).
A CutoffMaximumInstances-en keresztül az azonos IP címről érkező kérések számát korlátozhatjuk egy adott időintervallumon belül.
A CutoffWindowsSeconds segítségével a postback-ek nyomon követésének az idejét szabályozhatjuk. Tehát az itt beállított idő alatt annyi postback érkezhet maximum, mint amennyit beállítottunk a CutoffMaximumInstances-nél. (Jelen esetben 1perc alatt max. 5)
Értelemszerűen érdemes ezeket az értékeket összehangolni. (pl.: ha az rmds 20 és cws 60, akkor nincs értelme annak, hogy a cmi értéke 3 vagy annál nagyobb legyen!)
Kódrészletünk végén pedig feliratkozunk a GenerateChallengeAndResponse eseményre, ahol egy saját számolgatós java kódot fogunk implementálni.
Még mielőtt ezen eseményhez megírnánk a kezelőfüggvényét, azelőtt készítsük el a „botméter”-ünket! Ezt a Page_Load-ban fogjuk elkövetni, valahogy így:
if (IsPostBack)
{
NoBotState state;
lbl_state.Text = NoBot1.IsValid(out state) ? "Valószínűleg ember" : "Valószínűleg bot";
lbl_state.Text += " – <i>" + state.ToString() + "</i><br />";
StringBuilder sb = new StringBuilder();
foreach ( KeyValuePair<DateTime, string> ip in NoBot.GetCopyOfUserAddressCache())
{
sb.AppendFormat("<b>{1}</b> – <i>{0}</i><br />",ip.Key.ToString(), ip.Value);
}
lbl_log.Text = sb.ToString();
}
Ahhoz, hogy ez a kódrészlet működjön mindenféleképpen szükségünk lesz az alábbi pár névtér using-olására: System.Collections.Generic; AjaxControlToolkit; System.Text.
Azért tesszük az egész kódunkat egy „IsPostBack-es blokkba”, mert első oldalkérésnél felesleges, hogy lefusson a botellenőrzés. A validálás különben úgy történik, hogy az IsValid függvény visszaad egy bool értéket attól függően, hogy bot (ilyenkor false-szal tér vissza), vagy ember (true) ül szerinte a hívó oldalon.
Mellesleg az lbl_state.Text = NoBot1.IsValid() ? "Valószínűleg ember" : "Valószínűleg bot"; értékadás egy rövidítése az alábbi kódnak:
if (NoBot1.IsValid()) lbl_state.Text = "Valószínűleg ember";
else lbl_state.Text = "Valószínűleg bot";
Van egy túlterhelése is az IsValid-nak, ahol megadható neki egy NoBotState enumerátor típusú változó, amely bővebb információval szolgál nekünk a bot „lebukásával ” kapcsolatban. Az enum az alábbi értéket tartalmazza:
– Valid: emberi viselkedésre vallanak a klienstől érkező kérések
– InvalidBadResponse: ha nem egyezik meg a javascript által visszaadott érték a szerver oldalon kiszámított értékkel. (ha rosszul írjuk meg a CustomChallengeResponse-t, akkor mindig ilyen hibát fog adni a NoBot!)
– InvalidResponseTooSoon: hamarabb történt postback, mint a ReponseMinimumDelaySeconds -nél beállított időegység.
– InvalidAddressTooActive: túllépte a kliens a CutoffMaximumInstances-nél megadott értéket
– InvalidBadSession: nem használható a session
– InvalidUnknown: egyéb ismeretlen hiba
Ezek után egy StringBuilder segítségével összefűzzük egy string-be a NoBot által logolt IP címeket és a kéréseik idejét. Azért használunk SB-t, mert a string-eknél használatos konkatenáció (+) művelethez képest ez sokkal hatékonyabb.
A naplót a NoBot GetCopyOfUserAddressCache metódusának segítségével tudjuk lekérni, mely egy SortedList<DateTime, string> objektummal tér vissza. Itt bármilyen olyan asszociatív tömb (lista) használható az iterációhoz, amelynek van generikus megfelelője (tehát ahol meg lehet szabni a kulcs, illetve az érték típusát)!
Fontos, hogy a naplóban eltárolt adatok megőrződnek mindaddig, amíg nem érkezik újabb valid kérés! Tehát, ha a fejlesztőgépünkön többször is elindítjuk az alkalmazást, és az első postback-et 5 másodpercen belül elkövetjük, akkor láthatjuk az előző körben eltárolt logot.
Amennyiben ezt nem szeretnénk, akkor az if (IsPostBack) else ágában hívjuk meg a NoBot EmptyUserAddressCache() metódusát!
CustomChallangeResponse
Végül már csak annyi dolgunk maradt, hogy megírjuk az egyedi java-tesztelő függvényünket. Egy kis számítási feladatról lesz szó, ahol az aktuális dátum hónapjához hozzá kell majd adni egy random értéket (0 és 12 között), és vissza kell adni az így kapott új dátum hónapjának a sorszámát. A véletlengenerált számot egy Label-ben fogjuk eltárolni, amelyet DOM-on keresztül érünk el, így is tesztelve azt, hogy a kliens egy böngésző, nem pedig egy bot.
A függvényünk az alábbi módon kerül implementálásra:
protected void CustomChallengeResponse(object sender, NoBotEventArgs e)
{
Label monthlabel = new Label();
monthlabel.ID = "lbl_date";
Random r = new Random();
monthlabel.Text = r.Next(12).ToString();
monthlabel.Style.Add(HtmlTextWriterStyle.Visibility, "hidden");
((NoBot)sender).Controls.Add(monthlabel);
e.ChallengeScript = string.Format("var p = document.getElementById(‘{0}’).innerText; var now = new Date(); now.getMonth() + parseInt(p);", monthlabel.ClientID);
int plussz;
int.TryParse(monthlabel.Text, out plussz);
e.RequiredResponse = DateTime.Now.AddMonths(plussz-1).Month.ToString();
}
Fontos, hogy NobotEventArgs-ot használjunk, ne sima EventArgs-ot!
A monthlabel-nél két dolgot érdemes megjegyeznünk. Az egyik, hogy ha direktbe állítjuk be a címke Visible tulajdonságát false-ra, akkor a java kódunk nem fog lefutni, mivel nem fogja tudni elérni a címkét. Hasonló dolog fog történni akkor is, ha a Visibility helyett a Display stílustulajdonságot állítjuk be None-ra. Ennek az az oka, hogy Display=None esetén a böngésző egyszerűen kihagyja az adott tag-et, míg Visibility-nél csak nem jeleníti meg, de feldolgozza, és a helyére pedig egy placeholder-t helyez el!
A másik dolog, hogy a címkét a Nobot vezérlőnk Controls collection-jéhez kell hozzáadnunk, nem pedig az oldalhoz! Ezt azért így oldjuk meg, mert a Page Controls gyűjteményéhez az oldal életciklusának valamely korábbi eseményénél tudunk csak új vezérlőt felvenni (pl.: Page_Init, Page_Load, Databind, stb.).
A NobotEventArgs objektumnak számunkra két fontos tulajdonsága van: a ChallengeScript, illetve a RequiredResponse. (Mindkettő string típusú!) Az elsőnél a kliens oldalon kiszámíttatni kívánt java kódot kell megadnunk. Fontos, hogy a getElementById-nak a ClientID-t passzoljuk át, ne a sima ID-t! A RequiredResponse-nál pedig a számítási feladat végeredményét kell megadnunk. Az itt beállított értékkel fogja majd összehasonlítani a Nobot a kliensoldali script által visszaadott értéket.
Nem tudom valakinek feltűnt-e, de a szerveroldalon kiszámított válasznál az aktuális dátumhoz nem random értéknyi hónapot adtunk hozzá, hanem ennél eggyel kevesebbet! Ennek az az oka, hogy java scriptben a hónapok nullától vannak számozva, nem pedig egytől, így például a decembernél a getMonth() függvény 11-et fog visszaadni. Emiatt kell kivonni egyet a véletlengenerált számunkból.
Elkészültünk az alkalmazásunkkal, tesztelgessük, majd használgassuk!!