A basic, self-hosted CAPTCHA generates a random string, renders it as a distorted image using PHP's GD library, stores the expected answer in the session, and validates the user's typed answer against it — no third-party service required, though a service like reCAPTCHA (covered separately elsewhere on this site) is generally the more robust choice against real automated bots.
Generating the CAPTCHA image
Route::get('/captcha', function () {
$code = strtoupper(substr(str_shuffle('ABCDEFGHJKLMNPQRSTUVWXYZ23456789'), 0, 6));
session(['captcha_code' => $code]);
$image = imagecreate(150, 50);
imagecolorallocate($image, 255, 255, 255);
$textColor = imagecolorallocate($image, 0, 0, 0);
imagestring($image, 5, 30, 15, $code, $textColor);
// add some noise lines to make it harder for basic OCR to read
for ($i = 0; $i < 5; $i++) {
imageline($image, rand(0, 150), rand(0, 50), rand(0, 150), rand(0, 50), imagecolorallocate($image, rand(100, 200), rand(100, 200), rand(100, 200)));
}
ob_start();
imagepng($image);
$imageData = ob_get_clean();
imagedestroy($image);
return response($imageData)->header('Content-Type', 'image/png');
});
Excluding easily-confused characters (like 0/O and 1/I) from the random character pool, as the example above does, reduces genuine user frustration from a CAPTCHA that's ambiguous even to a real person.
The form with the CAPTCHA image and input
Refreshing the CAPTCHA without a full page reload
function refreshCaptcha() {
document.getElementById('captcha-image').src = '/captcha?t=' + Date.now();
}
Appending a changing query parameter (like the current timestamp) is necessary specifically to defeat the browser's own image caching — without it, the browser may simply reuse the previously cached image instead of actually requesting a fresh one from the server.
Validating the answer on form submission
public function store(Request $request)
{
$request->validate([
'captcha_answer' => 'required',
]);
if (strtoupper($request->captcha_answer) !== session('captcha_code')) {
return back()->withErrors(['captcha_answer' => 'Incorrect CAPTCHA code.']);
}
session()->forget('captcha_code');
// proceed with the actual form processing
}
Clearing captcha_code from the session after a successful check prevents the same code from being reused for a second submission attempt.
Why a service-based CAPTCHA (like reCAPTCHA) is generally the more robust choice
A self-hosted image CAPTCHA like this one is genuinely defeatable by modern OCR and automated solving services at any real scale — it's a reasonable, dependency-free option for a low-stakes form, but for anything facing meaningful spam or bot traffic, Google reCAPTCHA's behavioral analysis (covered elsewhere on this site) offers considerably stronger protection.