As soon as your application has a form that's available to everyone, you WILL be spammed! So what can we do about it? Well, one option is to have all forms secured by authentication... OR we can use a captcha.

As always, there are a couple of plugins / gems out there that do the traditional image captcha (you know, the beautiful images with random text on it). The problem is though, some of them suck: Some look hideous (like recaptcha) and others require a lot of effort to set up (see simple captcha).

What are the options?

Basic Math! Asking a (really) simple mathematical question will prevent spam, because stupid bots are just to stupid to answer it :)

What do we need for a math captcha? Basically a generator to create a question and a two-way hashing algorithm to encode (and later decode) the question/answer. Here's the code (in a rails app, you want to place this class into the lib folder):


  require 'ezcrypto'

  class MathCaptcha
    CIPHER_KEY  = "secret key"
    CIPHER_SALT = "secret salt"

    attr_reader :a, :b, :operator

    def initialize
      @a        = (1..10).to_a.rand
      @b        = (1..10).to_a.rand
      @operator = [:+, :*].rand
    end

    def initialize_from(secret)
      yml = YAML.load(key.decrypt64(secret))
      @a, @b, @operator = yml[:a], yml[:b], yml[:operator]
    end

    def correct?(value)
      result == value.to_i
    end

    def encrypt
      key.encrypt64 to_yaml
    end

    def self.decrypt(secret)
      result = new
      result.initialize_from secret
      result
    end

    def question
      "#{@a} #{@operator.to_s} #{@b} = ?"
    end

  protected

    def to_yaml
      YAML::dump({
        :a        => @a,
        :b        => @b,
        :operator => @operator
      })
    end

  private

    def key
      EzCrypto::Key.with_password CIPHER_KEY, CIPHER_SALT
    end

    def result
      @a.send @operator, @b
    end
  end

The only interesting thing here is the code in the encrypt and decrypt methods. Basically, what we do is dump the relevant variables into a string using YAML and then encrypt/decrypt it using ezcrypto.

Integrating the captcha into the controller and view

Since our MathCaptcha already defines all the interface methods we need, the integration is straightforward. First the view (note how we use the encrypted question):


  / ... other form code
  %p
    = hidden_field_tag :captcha_secret, @captcha.encrypt
    = label_tag :captcha, @captcha.question
    = text_field_tag :captcha, ""
  %p
    = submit_tag "Send"

And the controller is easy as well. All we do is to intantiate the same captcha question with the use of the secret:


  require 'math_captcha'

  class ContactsController < ApplicationController

    def new
      @captcha = MathCaptcha.new
    end

    def create
      @captcha = MathCaptcha.decrypt(params[:captcha_secret])

      unless @captcha.correct?(params[:captcha])
        flash.now[:alert] = "Please make sure you answered the math question correct"
        render :new
      else
        ContactsMailer.question(contact).deliver

        flash[:notice] = "Thanks for your request. We will be in contact with you shortly"
        redirect_to root_path
      end
    end
  end