Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Think Captcha Laravel Package

topthink/think-captcha

topthink/think-captcha provides easy CAPTCHA integration for ThinkPHP applications, generating customizable image-based verification codes and validating user input. It supports configuration, session-based checks, and simple controller/template usage, making form spam protection quick to add.

View on GitHub
Deep Wiki
Context7

Getting Started

First, the reality check: this is the official TopThink captcha library for ThinkPHP 6/8, not Laravel. composer require topthink/think-captcha in a Laravel app will pull in topthink/framework and its helpers, collide with Illuminate\* namespaces, and give you nothing usable. If you are in a Laravel codebase, the day-to-day equivalent is mews/captcha (same captcha_src() / captcha_img() / captcha() API shape and the same GD-based image generator lineage). Use this package only when you actually have a ThinkPHP app in your stack — a legacy admin, a side service, or a mixed portfolio.

Assuming a ThinkPHP 6/8 app:

composer require topthink/think-captcha   # 3.x
php think vendor:publish                  # optional: copy config/captcha.php into your app

Requirements that bite first: ext-gd and a working session store. Without a session the image renders fine and verification always fails, which looks like a code bug.

Where to look first, in vendor/topthink/think-captcha/src/:

File Why you care
Captcha.php The generator (create(), check()) — config keys, fonts, math mode
CaptchaController.php The route handler behind captcha/[:config]
helper.php captcha_src(), captcha_img(), captcha_check()
config/captcha.php Every knob you will actually tune
CaptchaService.php What gets auto-registered (route + helpers)

First use case — a login form. In the view:

<img src="{:captcha_src()}" id="captcha" onclick="this.src='{:captcha_src()}?'+Math.random()">
<input type="text" name="captcha" autocomplete="off">

In the controller:

if (!captcha_check($this->request->post('captcha'))) {
    return json(['code' => 1, 'msg' => 'captcha invalid']);
}

That is the whole loop. Everything else below is scale, multi-form, and API/SPA handling.

Implementation Patterns

Validation-rule style (the cleanest path). ThinkPHP's validator has a captcha rule shipped by the package, so the check disappears into your validate array:

protected $rule = [
    'username' => 'require',
    'password' => 'require',
    'captcha'  => 'require|captcha',
];

Prefer this over manual captcha_check() calls — it keeps the failure inside the standard validation-error flow, which in Laravel terms is your FormRequest + $errors. Manual checks scattered through controllers is the anti-pattern that shows up in every codebase that adopted this package early.

Custom keys for multiple forms. The default session key is derived from the request pathinfo, so two forms on two different URLs collide/reuse. Pass a config name through the route:

<img src="{:captcha_src('login')}">    <!-- hits captcha/login -->
<img src="{:captcha_src('resetpw')}">  <!-- hits captcha/resetpw -->

…and verify with the matching key. This mirrors named captcha "bags" in mews/captcha; think of it as the key that scopes the session slot.

Rendering into a JSON/API response. \think\facade\Captcha::create() returns a think\Response with the PNG body. For an SPA or a mobile client that would rather not juggle an <img> tag and a session-cookie dance:

$response = \think\facade\Captcha::create();
$b64 = base64_encode($response->getContent());
return json(['captcha' => 'data:image/png;base64,' . $b64]);

The session cookie is still the transport for the answer, so the client must send it back — see the SPA gotcha below.

Frontend refresh pattern. Every "refresh" is a fresh request that overwrites the session slot. Always cache-bust:

img.src = '/captcha?' + Date.now();

Test/QA bypass without forking. There is no built-in "disable" flag. The pattern that ages well is a thin wrapper service you call from controllers, gating on config/env:

class CaptchaGate
{
    public function verify(string $value, string $key = ''): bool
    {
        if (config('app.captcha_disabled')) return true;
        return captcha_check($value, $key);
    }
}

Then flip captcha_disabled in .env.testing. In Laravel terms you would reach for Captcha::fake(); here you own the seam, so build it before you have 200 tests calling captcha_check() directly.

Mixed-stack integration. If a Laravel front end talks to a ThinkPHP service that owns the captcha, treat it as an opaque endpoint pair: GET /captcha (returns image, sets session cookie) and the form POST. Do not attempt shared-session tricks across two frameworks unless you are already sharing a session driver and domain. Keep the captcha and the form POST on the same origin, and let Laravel proxy or iframe it rather than reimplementing.

Rate limiting belongs to you. The package has no throttle. Wrap the captcha route in ThinkPHP middleware (or Nginx limit_req) — every hit is a GD render and a bcrypt hash, so an unthrottled /captcha is a cheap CPU DoS on small instances.

Gotchas and Tips

The pathinfo-derived session key. This is the number-one source of "captcha always invalid" reports. The session slot defaults to a hash of the request pathinfo, so if the image was fetched at /captcha but the form posts to /login, verification looks in the wrong slot and fails silently. Fix: pass an explicit config name to captcha_src() and the same key to captcha_check(), or render the captcha from the same route family as the POST. In Laravel terms: this is not a token you carry around; it is a session slot with an implicit name.

Session is mandatory and non-obvious. Stateless API deploys (no SESSION_DRIVER, or session middleware excluded from the route group) produce a captcha that never validates. For SPAs, fetch needs credentials: 'include' and Axios needs withCredentials: true, plus correct SameSite/domain settings if the API is on a subdomain. If your front end and back end live on different hosts, use the base64/JSON pattern above on the same origin as the POST.

One-time consumption + expiry window. A successful captcha_check() clears the slot; a failed one does not. With the default expire of 1800s, an attacker gets an unlimited number of guesses against a 4-character code within that window. Tighten expire (60–120s for login), regenerate on failure, and rate limit per IP/session. Do not lower length below 4.

Config keys you will actually touch: length, codeSet (strip 0O1lI for human friendliness), expire, useNoise/useCurve, fontSize, bg, useZh (needs the Chinese font set — larger payload, slower render), math (arithmetic captcha — good UX on mobile keyboards, poor for accessibility readers). Publish the config (php think vendor:publish) rather than editing inside vendor/, or your tuning dies on the next composer update.

Config and route caching. After php think optimize:config / optimize:route, edits to config/captcha.php or route definitions have zero effect until you re-run the optimizer. In multi-app mode, check whether the config lives at config/captcha.php (global) or app/<name>/config/captcha.php (per-app) — the wrong one is a silent no-op.

Route collisions and middleware. The service registers captcha/[:config] automatically. If you already have a captcha route, or you attach auth/CSRF middleware to the group, the image endpoint 302s to login or 419s and the <img> renders broken. Define your own explicit route to \think\captcha\CaptchaController@index if you need it inside a specific middleware stack.

Fonts and GD. fontttf empty means "pick randomly from the shipped TTFs". If your container's gd build lacks FreeType (--with-freetype), rendering throws rather than degrading. Verify with php -r 'var_dump(function_exists("imagettftext"));' in the deployed image, not just locally — this is the classic "works on my Mac, 500s in Alpine" trap.

Testing. No fake/skip hook exists. Either build the gate wrapper above, or seed the session in your test bootstrap before the request. Do not assert on image bytes; assert on the captcha_check() outcome.

Accessibility and product fit. There is no audio captcha. If the form is public-facing and you have compliance obligations, this package alone will not clear an accessibility audit — pair it with an email/SMS fallback path or an alternative challenge.

License. The registry metadata reports NOASSERTION even though the repository ships Apache-2.0-style terms. That mismatch is enough for some legal reviews to block it; get it confirmed before shipping in a commercial product, and pin the version you vetted (2024-11-22 is the latest release, ~117 stars — small, single-maintainer, official-but-quiet). Pin exact versions and keep an eye on the ThinkPHP framework constraint when you upgrade the framework itself.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
novasa/laravel-language-center
opencage/geocode
adolfoholzer/parameters
livenetworks/ln-starter
shitoudev/permissionmanager
johannesclimacus/laravel-tui
hocvt/seotools
vitalybaev/sypexgeo
amphibee/themosis-ignition
gito-me/fast-excel
frankyso/laravel-ipaymu
geolize-studio/laravel-chile
boomerangui/chat
cgv/localized-resource
noith/settings
kda/laravel-package
kda/filament-dynamic-navigation
bypersoft/signbox
pairus/product-data
fayne/laravel-filament-cms-core