Navigace

Hlavnφ menu

 

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.

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...

Hlavat², DuÜan (22. 3. 2005)