Django and captchas

Today at work, I wrote a simple captcha system for the web application we are building. Danny mentioned that he would be interested in seeing how that stuff is done. So I made a quick demo app, and I’ll post the code here.

A few files are needed to make this work:

This is what the result will look like:

First, here’s my views.py file. I put everything in there, but if your application grows, you will probably want to move your manipulator code into another file (e.g.: forms.py) and maybe the captcha() function too (this one could go in utils.py). The code is commented, so I don’t think I need to go over everything again, do I?

import Image, ImageDraw, ImageFont
import sha
from random import randrange

from django.shortcuts import render_to_response
from django.conf import settings
from django import oldforms as forms
from django.core import validators

class CaptchaManipulator(forms.Manipulator):
    # We create a new form with a single
    # field to input our captcha.  This
    # field is required, and it will be
    # validated with our own custom method.
    def __init__(self):
        self.fields = (
            forms.TextField(field_name='captcha',
                            is_required=True,
                            validator_list=[self.captcha_check]),
        )

    # Check that the captcha field when hashed
    # is the same as in the hidden field.  If
    # not, raise a validation exception.
    def captcha_check(self, field, all_fields):
        image_hash = sha.new(field + settings.SECRET_KEY).hexdigest()
        if image_hash != all_fields['hash']:
            raise validators.ValidationError('Incorrect captcha!')

def captcha():
    # Pick a random number between 10,000 and 99,999
    n = str(randrange(10000, 100000))

    # Get a SHA hash of the random number we picked
    # and our Django project's secret key.  The idea
    # here is that a potential bot could have the SHA
    # hashings for all the possible numbers and could
    # use that to fool our system, so that's why we add
    # secret key.  It would also be really stupid to
    # put the captcha answer in plain text in the HTML.
    image_hash = sha.new(n + settings.SECRET_KEY).hexdigest()

    # Open our background image.  You may want to use
    # os.path.join and MEDIA_ROOT to avoid path problems.
    image = Image.open('media/bg.jpg')

    # We're going to draw on that image
    draw  = ImageDraw.Draw(image)

    # Using this truetype font.  It is recommended to use
    # a weird font that people have no problem reading, but
    # that is still irregular to fool potential bots.  The
    # second parameter is the size of the font. To avoid
    # path problems, use os.path.join and MEDIA_ROOT.
    font  = ImageFont.truetype('media/Nervous0.ttf', 52)

    # Write our number starting at point 0@10 using our
    # font.  fill is the color we want to use
    draw.text((0, 10), n, font=font, fill=(0, 0, 0))

    # Save the image in the media directory.  Using the
    # same name over and over may not work for large
    # sites.  Same advice as above, use os.path.join.
    image.save('media/captcha.jpg', 'JPEG')

    return image_hash

def index(request):
    manip = CaptchaManipulator()

    if request.method == 'POST':
        data = request.POST.copy()
        errors = manip.get_validation_errors(data)
    else:
        data = errors = {}

    form = forms.FormWrapper(manip, data, errors)

    # We pass two variables to our template: the
    # hash we calculated and the form.
    return render_to_response('index.html',
                              {'hash': captcha(),
                               'form': form})

And here’s what index.html looks like:

{% if form.captcha.errors %}
    {{ form.captcha.errors|join:" " }}
{% else %}
    You got it!
{% endif %}

<img src="/media/captcha.jpg" />

<form method="post" action=".">
  {{ form.captcha }}
  <input name="hash" type="hidden" value="{{ hash }}" />
  <input type="submit" />
</form>

This blog entry was really helpful in getting me in the right direction. In fact, if you look closely, you can see that I just removed some stuff from his code.

I hope this is helpful!

Comments are closed.