Ovlßdacφ prvok CAPTCHA v ASP.NET
SystΘmy CAPTCHA zis¥uj·, s k²m aplikßcia komunikuje, respektφve zabezpeΦuj·, ₧e aplikßcia nekomunikuje so "strojom", ale so skutoΦn²m Φlovekom. Vo webov²ch aplikßcißch sa CAPTCHA najΦastejÜie vyu₧φva pri prihlasovanφ sa do aplikßcie Φφm sa zaruΦuje, ₧e do aplikßcie sa prihlasuje skutoΦn² Φlovek a nie automat, robot. V tomto Φlßnku si priblφ₧ime princφpy CAPTCHA a vytvorφme si ASP.NET ovlßdacφ prvok, ktor² sa dß bez problΘmov pou₧i¥ v ╛ubovo╛nej webovej aplikßcii.
CAPTCHA (Completely Automated Public Turing test to tell Computers and Humans Apart) by sa dalo vo╛ne prelo₧i¥ ako "automatick² sp⌠sob ako rozlφÜi¥ stroj od Φloveka". Viac informßciφ nßjdete naprφklad na strßnkach The CAPTCHA Project.
Ako funguje CAPTCHA
Preto₧e poΦφtaΦe nedokß₧u (aspo≥ nie dostatoΦne spo╛ahlivo) "rozumie¥" obrßzkom, je systΘm CAPTCHA zalo₧en² na obrßzkoch najrozÜφrenejÜφm z t²chto systΘmov. Princφp je jednoduch² - aplikßcia vygeneruje obrßzok, na ktorom je k≤d (text, Φφslo, grafick² objekt, fotografia), ktor² je ╛ahko porozumite╛n² pre Φloveka, ale ¥a₧ko pre poΦφtaΦ. U₧φvate╛ musφ k≤d z obrßzka zada¥ do textovΘho po╛a. Aplikßcia si k≤d na vstupe porovnß s t²m, ktor² vygenerovala do obrßzka. Ak s· k≤dy zhodnΘ, tak je vysoko pravdepodobnΘ, ₧e s aplikßciou komunikuje Φlovek.
Pou₧itie systΘmu CAPTCHA pri prihlasovanφ sa do aplikßcie mß aj ∩alÜφ ve╛mi d⌠le₧it² "ved╛ajÜφ efekt" - zabra≥uje hßdaniu hesiel hrubou silou. ┌tok normßlne funguje tak, ₧e nejak² automat (program, robot) sk·Üa za sebou vÜetky kombinßcie znakov ako heslo a sk⌠r Φi nesk⌠r heslo do systΘmu musφ zφska¥. Pri zabezpeΦenφ prihlasovania CAPTCHA systΘmom ale aplikßcia vygeneruje pri ka₧dom pokuse o prihlßsenie nov² obrßzok s k≤dom. Najprv sa skontroluje sprßvnos¥ CAPTCHA k≤du, ak je nesprßvny, pokus o prihlßsenie sa konΦφ ne·spechom a teda eÜte sk⌠r, ako sa v⌠bec kontroluje sprßvnos¥ zadanΘho hesla. Z bezpeΦnostnΘho hladiska musφ v₧dy plati¥, ₧e po ka₧dom (sprßvnom aj nesprßvnom) pokuse o prihlßsenie sa musφ vygenerova¥ nov² CAPTCHA k≤d.
Aj Gmail sa proti hßdaniu hesiel hrubou silou brßni systΘmom CAPTCHA
V jednom z predoÜl²ch Φlßnkov som vßm ukßzal, ako Φo najefektφvnejÜie generova¥ obrßzky na strane servra. Prφklad vÜak nemal ve╛kΘ vyu₧itie v praxi. Teraz si tento prφklad rozÜφrime a vytvorφme ovlßdacφ prvok CAPTCHA, ktor² nßjde vyu₧itie prakticky v ka₧dej webovej aplikßcii.
Http handler na generovanie obrßzka
Zdrojov² k≤d http handlera, ktor² bude generova¥ obrßzok s CAPTCHA k≤dom, je prakticky rovnak² ako v Φlßnku DynamickΘ obrßzky v ASP.NET cez HTTP handler. V met≤de ProcessRequest(HttpContext context)
vÜak upravφme Φas¥, v ktorej vykres╛ujeme samotn² text. Ten zobrazφme nßhodne zdeformovan², aby bol horÜie Φitate╛n²m pre poΦφtaΦ:
// Nastavime format textu
StringFormat format = new StringFormat();
format.Alignment = StringAlignment.Center;
format.LineAlignment = StringAlignment.Center;
// Vytvorime cestu a nahodne ju 'pokrutime'
GraphicsPath path = new GraphicsPath();
path.AddString(textToDisplay, this.UsedFont.FontFamily, (int) this.UsedFont.Style, this.UsedFont.Size, this.UsedRectangle, format);
float v = 4F;
PointF[] points =
{
new PointF(this.random.Next(this.UsedRectangle.Width) / v, this.random.Next(this.UsedRectangle.Height) / v),
new PointF(this.UsedRectangle.Width - this.random.Next(this.UsedRectangle.Width) / v, this.random.Next(this.UsedRectangle.Height) / v),
new PointF(this.random.Next(this.UsedRectangle.Width) / v, this.UsedRectangle.Height - this.random.Next(this.UsedRectangle.Height) / v),
new PointF(this.UsedRectangle.Width - this.random.Next(this.UsedRectangle.Width) / v, this.UsedRectangle.Height - this.random.Next(this.UsedRectangle.Height) / v)
};
Matrix matrix = new Matrix();
path.Warp(points, this.UsedRectangle, matrix, WarpMode.Perspective, 0F);
// Vykreslime text
Brush hatchBrush = new HatchBrush(HatchStyle.LargeConfetti, Color.LightGray, Color.DarkGray);
g.FillPath(hatchBrush, path);
...
V met≤de si najprv vytvorφme cestu (trieda GraphicsPath
) s CAPTCHA k≤dom. Cestu nßhodne deformujeme met≤dou Warp(...)
. A nakoniec vyplnφme pomocou FillPath(...)
. NßÜ CAPTCHA obrßzok bude vyzera¥ naprφklad takto:
NßÜ CAPTCHA obrßzok
Zßsadou je - Φφm viac ruÜiv²ch elementov sa v obrßzku nachßdza (Üum, r⌠zne Φiary a podobne), t²m je pre prφpadnΘho ·toΦnφka ¥a₧Üie tento systΘm prekona¥ a vytvori¥ automat, ktor² by vedel k≤d spo╛ahlivo Φφta¥.
Ovlßdacφ prvok CAPTCHA v ASP.NET
Ovlßdacφ prvok bude obsahova¥ obrßzok so samotn²m CAPTCHA k≤dom a textovΘ pole, kam bude u₧φvate╛ tento k≤d vklada¥. Ovlßdacφ prvok bude tie₧ validßtorom, Φi₧e bude aplikßcii oznamova¥, ₧e u₧φvate╛ zadal nesprßvny k≤d.
Ovlßdacφ prvok musφ pri ka₧dom svojom "vykres╛ovanφ" vygenerova¥ nov² CAPTCHA k≤d. Tento ulo₧φ do sessiony, odkia╛ si ju vyzdvihne http handler a vykreslφ ho. Pri post-backu formulßra sa k≤d zadan² u₧φvate╛om porovnß s t²m, ktor² je v session. Ak nie s· k≤dy rovnakΘ, validßtor oznßmi aplikßcii chybu.
Vytvorφme si teda triedu pre nßÜ ovlßdacφ prvok - ten bude dedi¥ z triedy System.Web.UI.Control
:
{
private HtmlInputText inputText;
private Random random = new Random();
public string Url = "~/captchaimage.axd";
public string TagKey = "div";
public string CssClass = "captcha";
protected override void CreateChildControls()
{
// vytvorime input control
this.inputText = new System.Web.UI.HtmlControls.HtmlInputText("password");
this.inputText.ID = "input";
this.inputText.MaxLength = 3;
// vlozime ho medzi svoje detske ovl. prvky
this.Controls.Add( this.inputText );
}
private string GenerateRandomCode(int length)
{
string s = String.Empty;
System.Text.StringBuilder sb = new System.Text.StringBuilder(length);
for (int i = 0; i < length; i++)
{
sb.Append( this.random.Next(10).ToString() );
}
return sb.ToString();
}
// dalÜφ k≤d p⌠jde sem
}
Trieda bude ma¥ nieko╛ko verejne prφstupn²ch premenn²ch. Adresa nßÜho http handlera, ktor² generuje samotn² obrßzok s k≤dom, sa bude nastavova¥ pomocou Url. (Nezabudnite, ₧e handler musφ by¥ sprßvne zaregistrovan².) Otvßracφ HTML tag nßÜho prvku je v premennej TagKey. A CSS trieda je v CssClass.
VÜetky kompozitnΘ ovlßdacie prvky by mali vytvßra¥ svojich "potomkov" v met≤de CreateChildControls()
. My v tejto met≤de vytvorφme textovΘ pole <input type="password" ...> a pridßme ho do kolekcie Controls
.
Na generovanie re¥azca nßhodn²ch Φφsiel sl·₧i met≤da GenerateRandomCode(int length)
. Ako parameter staΦφ met≤de zada¥ dσ₧ku re¥azca. My budeme pou₧φva¥ trojcifernΘ Φφsla.
"Vykres╛ovanie" ovlßdacie prvku prebieha v met≤de Render
:
{
// vyrenderujeme id
if (this.ID != null)
{
writer.AddAttribute(HtmlTextWriterAttribute.Id, this.ClientID);
}
// vyrenderujeme CSS triedu
if (this.CssClass != null)
{
writer.AddAttribute(HtmlTextWriterAttribute.Class, this.CssClass);
}
// zaciatocna znacka
writer.RenderBeginTag(this.TagKey);
// obrazok - odkaz na http handler
writer.WriteBeginTag( "img" );
writer.WriteAttribute("src", this.ResolveUrl(this.Url));
writer.WriteAttribute("alt", String.Empty);
writer.Write( HtmlTextWriter.TagRightChar );
// zaciatok tela
writer.AddAttribute(HtmlTextWriterAttribute.Class, "body");
writer.RenderBeginTag("div");
// obsah
writer.AddAttribute(HtmlTextWriterAttribute.For, this.inputText.ClientID);
writer.RenderBeginTag("label");
writer.Write( "Zadaj k≤d ktor² vidφÜ na obrßzku:" );
writer.RenderEndTag();
this.RenderChildren( writer );
writer.RenderEndTag();
writer.RenderEndTag();
}
V met≤de najprv "vykreslφme" <img ...> element, ktor² zobrazuje CAPTCHA k≤d, potom oznam pre u₧φvate╛a a napokon textovΘ pole pre zadßvanie tohto k≤du. NßÜ ovlßdacφ prvok bude generova¥ tak²to HTML k≤d:
<img src="/captcha/captchaimage.axd" alt="">
<div class="body">
<label for="CaptchaControl1_input">
Zadaj k≤d ktor² vidφÜ na obrßzku:
</label>
<input name="CaptchaControl1:input" id="CaptchaControl1_input" type="password" maxlength="3" />
</div>
</div>
Ako som u₧ spomφnal, ovlßdacφ prvok musφ pri ka₧dom svojom "vykres╛ovanφ" vygenerova¥ nov² k≤d. Preto pridßme do naÜej triedy met≤du, ktorß nßm toto spo╛ahlivo zabezpeΦφ - vygeneruje trojcifernΘ Φφslo a ulo₧φ ho do sessiony:
{
base.OnPreRender (e);
if (this.IsVisible)
{
string captchaKey = this.GenerateRandomCode(3);
this.Context.Session[ CaptchaHttpHandler.SESSION_CaptchaKey ] = captchaKey;
}
}
NßÜmu ovlßdaciemu prvku ch²ba u₧ len poslednß vec - kontrola sprßvnosti u₧φvate╛om zadanΘho k≤du. Ovlßdacφ prvok je preto zßrove≥ aj validßtorom a implementuje rozhranie IValidator
(Viac o validßtoroch sa doΦφtate v Φlßnku ASP.NET - schvalovacφ prvky). V met≤de Validate()
najsk⌠r zistφme, Φi nßÜ ovlßdacφ prvok je vidie¥. Ak nie, tak vlastnos¥ IsValid
vrßti true, Φi₧e ₧iadna chyba sa nevyskytla. Ak prvok je vidie¥, tak zavolßme met≤du EvaluateIsValid
, ktorß naΦφta zo sessiony aktußlny k≤d a porovnß ho s k≤dom, ktor² zadal u₧φvate╛. Ak s· rovnakΘ, vrßti true, inßΦ vrßti false. No a eÜte do triedy pridßme vlastnos¥ ErrorMessage, ktorß obsahuje chybov· hlßÜku. Implementßcia nßÜho validßtora vyzerß nasledovne:
{
if (!this.IsVisible)
{
this.isValid = true;
return;
}
this.isValid = this.EvaluateIsValid();
}
protected virtual bool EvaluateIsValid()
{
// ak uzivatel nic nezadal tak konec
if (this.inputText.Value.Length == 0)
{
return false;
}
object o = Context.Session[ CaptchaHttpHandler.SESSION_CaptchaKey ];
if ( !(o is string) )
{
return false;
}
string generated = (string)o;
if (generated.Equals(this.inputText.Value))
{
return true;
}
return false;
}
private bool isValid = true;
public bool IsValid
{
get
{
return this.isValid;
}
set
{
this.isValid = value;
}
}
private string errorMessage;
public string ErrorMessage
{
get
{
return this.errorMessage;
}
set
{
this.errorMessage = value;
}
}
Pre sprßvnu funkΦnos¥ musφme eÜte nßÜ prvok pri inicializßcii zaregistrova¥ medzi ostatnΘ validßtory na strßnke:
{
base.OnInit (e);
// pridame ov. prvok medzi ostatne validatory:
this.Page.Validators.Add(this);
this.EnsureChildControls();
}
Ovlßdacφ prvok v praxi
NßÜ ovlßdacφ prvok je hotov² a m⌠₧me ho zaΦa¥ pou₧φva¥ pri prihlasovanφ u₧φvate╛ov, alebo naprφklad aj pri overovanφ, Φi u₧φvate╛ naozaj chce vykona¥ nejak· deÜtruktφvnu operßciu. ("Chcete naozaj stornova¥ fakt·ru? Zadajte k≤d, ktor² vidφte na obrßzku...")
Ja som pripravil jednoduch· ASPX strßnku, na ktorej si m⌠₧eme overi¥ funkΦnos¥ ovlßdacieho prvku. Cel² zdrojov² k≤d nßjdete v prilo₧enom s·bore. Nemß v²znam uvßdza¥ cel² k≤d do Φlßnku - ukß₧eme si iba ako v obsluhe tlaΦidla zistφme, Φi sa na strßnke vyskytla chyba a teda Φi u₧φvate╛ zadal sprßvny k≤d. Je to jednoduchΘ:
{
if (!this.IsValid)
{
// na stranke je chyba
return;
}
// na stranke nie je chyba - uzivatel zadal spravny kod
}
NaÜa testovacia strßnka v prehliadaΦi:
Testovacia strßnka - chybne zadan² k≤d
Testovacia strßnka - sprßvne zadan² k≤d
Obmedzenia popisovanΘho prvku
NßÜ ovlßdacφ prvok nie je dokonal² a ani sa nesna₧φ by¥. Mß nieko╛ko obmedzenφ a mo₧nΘ bezpeΦnostnΘ slabiny. Obmedzenφm je naprφklad jeho vzh╛ad (respektφve generovan² HTML k≤d), ktor² je pevne dan² a meni¥ sa dß len pomocou CSS. Nov² CAPTCHA k≤d sa generuje pri ka₧dej po₧iadavke (request) na server - Φo je samozrejme sprßvne. Ale aj obrßzok s k≤dom sa generuje pri ka₧dej po₧iadavke (requeste na nßÜ http handler). To m⌠₧e by¥ bezpeΦnostnß slabina. Ak poÜleme na server naprφklad 10 po₧iadaviek, zaka₧d²m server vygeneruje in² obrßzok (preto₧e ho nßhodne deformujeme), na ka₧dom vÜak bude ten ist² k≤d. Toto m⌠₧e vyu₧i¥ sofistikovan² robot, ktor² bude posiela¥ po₧iadavky na CAPTCHA obrßzok, k²m si nebude ist², ₧e k≤d na ≥om rozpoznal. Potom tento k≤d vlo₧φ do textovΘho po╛a. S· aplikßcie (naprφklad u₧ spomφnanΘ stornovanie fakt·ry v intranetovej aplikßcii), kde tßto slabina nemusφ prekß₧a¥. V ostatn²ch vÜak treba zabezpeΦi¥, aby pri ka₧dej po₧iadavke server vrßtil rovnak² obrßzok, a₧ dok²m sa nevygeneruje nov² CAPTCHA k≤d...