Ovládací prvok CAPTCHA v ASP.NET

22. března 2005

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.

CAPTCHA na Gmaile
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
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:

public class CaptchaControl : System.Web.UI.Control, IValidator, INamingContainer
{
  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:

protected override void Render(HtmlTextWriter writer)
{
  // 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:

<div id=“CaptchaControl1″ class=“captcha“>
  <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:

protected override void OnPreRender(EventArgs e)
{
  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:

public void Validate()
{
  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:

protected override void OnInit(EventArgs e)
{
  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é:

private void bOK_Click(object sender, EventArgs e)
{
  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 – chybne zadaný kód

Testovacia stránka - správne 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…

Starší komentáře ke článku

Pokud máte zájem o starší komentáře k tomuto článku, naleznete je zde.

Štítky: Články

Mohlo by vás také zajímat

Nejnovější

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *