To make this work within Grails, I had to make several tweaks. The following files are required:
- JQuery 1.2+ (I am using version - 1.4.2)
- JQuery UI (I am using version - 1.8.2)
- jquery.simpleCaptcha-0.2.js
- Captcha Images placed in images/captchaImages
- BCrypt.java by Damien Miller
- CaptchaController.groovy (below)
Create a new controller called Captcha. This can really be named anything, but if you do rename it, it will have to be updated in the jquery.simpleCaptcha-0.2.js file or passed in as an option via the javascript.
package com.berry | |
import com.berry.BCrypt | |
import grails.converters.JSON | |
class CaptchaController { | |
def index = { | |
// Generate the SALT to be used for encryption and place in session | |
def captchaSalt = session.captchaSalt ?: BCrypt.gensalt() | |
session.selectedCaptchaText = null | |
session.captchaSalt = captchaSalt | |
// Modify below for custom images | |
def images = [ | |
'house': 'images/captchaImages/01.png', | |
'key': 'images/captchaImages/04.png', | |
'flag': 'images/captchaImages/06.png', | |
'clock': 'images/captchaImages/15.png', | |
'bug': 'images/captchaImages/16.png', | |
'pen': 'images/captchaImages/19.png', | |
'light bulb': 'images/captchaImages/21.png', | |
'musical note': 'images/captchaImages/40.png', | |
'heart': 'images/captchaImages/43.png', | |
'world': 'images/captchaImages/99.png' | |
] | |
// Create the image array to be returned in JSON format | |
def size = images.size() | |
def num = Math.min(params.numImages ? params.int('numImages') : 5, size) | |
def keys = images.keySet().toList() | |
def used = [] | |
def random = new Random() | |
(1..num).each { i -> | |
def idx = random.nextInt(keys.size()) | |
def item = keys.get(idx) | |
keys.remove(idx) | |
used << item | |
} | |
// Select the 'chosen' text to be used for authentication and place in session | |
def selectedText = used[random.nextInt(used.size())] | |
def hashedSelectedText = BCrypt.hashpw(selectedText, captchaSalt); | |
session.selectedCaptchaText = hashedSelectedText | |
// println "SELECTED: ${hashedSelectedText}" | |
// println "USED: ${used.inspect()}" | |
// Generate object to be returned | |
def ret = [ | |
text: selectedText, | |
images: [] | |
] | |
used.each { u -> | |
ret['images'] << [hash: BCrypt.hashpw(u, captchaSalt), file: images[u]] | |
} | |
render ret as JSON | |
} | |
} |
What this controller does is return a JSON object with the data needed to generate the captcha. The JSON appears like so:
{ | |
"text":"heart", | |
"images":[ | |
{ | |
"hash":"$2a$10$GTcG7U1rt7XFBi4JVImT2Oo.E3D8FCzha2772XuXm7v28Kx2LNL5S", | |
"file":"images/captchaImages/99.png" | |
}, | |
{ | |
"hash":"$2a$10$GTcG7U1rt7XFBi4JVImT2Oa5Y/I/cXOUj30kffPqyX0qxTnAACX6O", | |
"file":"images/captchaImages/43.png" | |
}, | |
{ | |
"hash":"$2a$10$GTcG7U1rt7XFBi4JVImT2O8zeOa4.ed1s8pZS9AgkalcSSQm9pmbi", | |
"file":"images/captchaImages/15.png" | |
}, | |
{ | |
"hash":"$2a$10$GTcG7U1rt7XFBi4JVImT2OSNYwC4RPwhNpuPYBbeNB0j4ozoItwDK", | |
"file":"images/captchaImages/06.png" | |
}, | |
{ | |
"hash":"$2a$10$GTcG7U1rt7XFBi4JVImT2OLv6DzHHDX0aB2AwS1YEVZMp9cEpo2sq", | |
"file":"images/captchaImages/01.png" | |
} | |
] | |
} |
Now we just need to implement this in our GSP file and controller. Suppose we have a page like shown above with a pickup code and the last 4 digits of the persons phone number. With adding our captcha div and required javascript, our GSP would look like this:
<!-- PLACE IN HEADER --> | |
<script type="text/javascript" src="${resource(dir:'js',file:'jquery.simpleCaptcha-0.2.js')}"></script> | |
<style type="text/css"> | |
img.simpleCaptcha { | |
margin: 2px !important; | |
cursor: pointer; | |
} | |
img.simpleCaptchaSelected { | |
margin-bottom: 0px; | |
border-bottom: 2px solid red; | |
} | |
</style> | |
<!-- BODY CONTENTS --> | |
<g:form action="pickup"> | |
<div class="stylized myform" style="width:542px;"> | |
<h2>Your pickup code will be given to you by your loan consultant</h2> | |
<g:if test="${flash.message}"> | |
<div class="error"> | |
${flash.message} | |
</div> | |
</g:if> | |
<div class="clearfix formField"> | |
<label class="label_only">Pickup Code</label> | |
<g:textField name="pickupCode" value="${pickupCode}" autocomplete="no" class="text" /> | |
</div> | |
<div class="clearfix formField"> | |
<label class="label_only">Last 4 Digits of Phone</label> | |
<span class="after_checkbox" style="padding-right: 0px;">(XXX) XXX-</span> | |
<g:textField name="lastFourDigits" value="${lastFourDigits}" autocomplete="no" class="text" maxLength="4" /> | |
</div> | |
<div class="clearfix formField"> | |
<label class="label_only">Are You Human?</label> | |
<div style="float: left; margin-left: 10px;"> | |
<!-- Begin Captcha --> | |
<div id="captcha"></div> | |
<!-- End Captcha --> | |
</div> | |
</div> | |
<div class="clearfix" style="margin-top: 15px;"> | |
<label class="label_only"> </label> | |
<g:submitButton name="submit" value="Show Me My Offer" class="button" /> | |
</div> | |
</div> | |
</g:form> | |
<script type="text/javascript"> | |
$(document).ready(function() { | |
$('#captcha').simpleCaptcha({ | |
numImages: 5, | |
introText: '<p>Are you human? Then pick out the <strong class="captchaText"></strong></p>' | |
}); | |
}); | |
</script> |
Finally, we need to perform the validation on the controller side. The modified authentication action would look like the following:
def pickup = { | |
// Determine if the captcha is picked correctly | |
if (params.captchaSelection != session.selectedCaptchaText) { | |
// They selected the correct Captcha image. Continue with Authentication | |
} else { | |
flash.message = "You did not click the correct image below. Please Try Again." | |
} | |
} |
So there ya go. It's actually pretty easy and customers seem to like choosing an image much more than typing a word that is difficult to read.
Hi.
ReplyDeletereally great idea! Definetly worth remembering!
Hello there, my name is Jordan, author of simpleCaptcha. If you would like me to include your Grails implementation in the release, please send over all files to jquery aht jordankasper d com. Of course, you don't have to, but I'm sure some others will appreciate it.
ReplyDeleteGlad to see it worked out for you!
I actually went a step further and used the generated hash as a "key" for the image in a custom rendering action in my controller, so that an association can't be made between an image URL and the name listed for selection.
ReplyDeleteThe action translates the hash into the "actual" URL of the CAPTCHA image and renders the image. With this, instead of passing the URL of the image in to simpleCaptcha, I can pass my custom URL with the hash as a parameter.
Respect and that i have a keen proposal: What Home Renovation Expenses Are Tax Deductible home renovation companies near me
ReplyDelete