Web Login Captcha Specification: From Legacy Admin Audit to One-Time Redis Validation
This note records a full round of investigation and refactoring I recently did around the admin login captcha flow in a Web project. The starting point was simple: I wanted to confirm whether the legacy admin captcha on the old server was actually being validated, and what the right shape of a complete captcha security flow should look like in the newer web_3 system.
I mainly did three things:
- Audited the real captcha logic behind
yzdwx.yizhoudao.net/admin/ - Verified whether the legacy Web system was actually using Redis
- Rebuilt the admin captcha flow in local
web_3asbackend generation + Redis storage + one-time backend validation
The final conclusion was very clear: the legacy admin page still displayed a captcha, but the login flow did not actually validate it; the new web_3 flow has already been rebuilt into a proper Redis-backed captcha pipeline.
Why I Wrote This Note
Captcha mechanisms often fall into a fake-security state: the page clearly shows an input box and a captcha image, so visually everything looks fine, but what actually matters is whether the backend validates it and whether the challenge is consumed exactly once.
This audit made one thing much clearer to me: a login captcha must be designed as a full loop of generation, storage, submission, validation, and invalidation. If any step is missing, the feature is not truly in place.
The Real Captcha Logic on the Old Server
I first logged into the old server over SSH and performed a read-only investigation. I did not modify production code and I did not restart any online service.
I confirmed the following site layout:
- Domain
yzdwx.yizhoudao.netpoints to:/www/wwwroot/yzdwx5.weitaibei.com/public_html - Application entry point:
/www/wwwroot/yzdwx5.weitaibei.com/public_html/index.php - Actual business code directory:
/www/wwwroot/yzdwx5.weitaibei.com/system/application
Then I inspected the admin login template and confirmed that the page really did contain both a captcha input box and a captcha image. The page requests:
/captcha
Refreshing the image requests:
/captcha?random=...
This route ultimately comes from ThinkPHP’s captcha package. Tracing deeper, I confirmed that:
- The captcha image is indeed generated by the backend
- The captcha answer is written into
Session - It is not written to the frontend
- It is also not written to Redis
The more important part came when I traced the admin login controller. The legacy login method accepted a $verify parameter, but the actual logic only did the following:
- Check whether the username is empty
- Check whether the password is empty
- Call
rbaclogin(...)
I did not find any real captcha validation logic such as:
captcha_check($verify)validate(...captcha...)- Manual captcha comparison
So the real state of the old admin captcha was:
- The page shows a captcha
- The backend generates a captcha image
- The captcha answer is stored in
session - But the login business flow never actually validates it
That means the legacy admin captcha was very likely in a state of “the UI is still there, but validation has already failed to exist or was accidentally dropped.”
How I Confirmed It
This was not guesswork, and it was not just based on what the frontend page looked like. I followed a fairly reliable verification chain.
I first confirmed SSH access and the site directory:
- Connect to the server through my local SSH alias
- Inspect the Nginx config and confirm which directory
yzdwx.yizhoudao.netreally points to - Check PHP-FPM and Nginx runtime status
Then I traced the application layer:
- Find the admin login template
- Find where
/captchais routed - Find the captcha generation logic
- Find the captcha validation logic
Finally, I inspected the admin login controller:
- Check whether
login()actually calls captcha validation - The answer was no
The benefit of this order is that it avoids being fooled by the UI. If the login controller does not execute captcha validation, then even a perfectly rendered captcha image is only visually present, not security-effective.
Whether the Legacy Web System Really Used Redis
I checked this separately because many projects end up in a common state: the codebase contains a Redis wrapper, but production does not actually use Redis.
My conclusion this time was:
- The repository does contain a Redis utility class
- But I did not find real business code on the live legacy Web system that actually used it
- I also did not find a running Redis service on the server itself
The evidence I checked included:
- ThinkPHP session configuration
- PHP
session.save_handler - Whether
/tmpcontained manysess_*files - Whether
redis-serverexisted on the server - Whether
redis-cliexisted - Whether port
6379was listening
The final facts were:
- The default session driver was not Redis
- PHP’s current session handler was
files - There were indeed many
sess_*files under/tmp - No Redis process was found
- No
6379listener was found - No Redis binary was found either
So the most accurate statement is not “the old project never had Redis”, but rather:
The old project had Redis wrapper code, but the currently running legacy site did not actually enable Redis, and the admin captcha was not stored there.
What Was Actually Wrong with the Legacy Captcha
If I summarize the legacy admin problem in one sentence, I would put it like this:
The problem was not that captcha generation failed. The problem was that captcha never entered the real login validation path.
In other words, the old system looked as if it had captcha protection, but the actual login security flow did not use it at all. That is more dangerous than simply “having no captcha”, because product, QA, and even developers can all be fooled by what they see on the page.
From a specification perspective, the old version had at least two obvious gaps:
- The captcha answer was stored in Session, but the login API never validated it
- The captcha was not designed as a one-time challenge, so even if validation were later added, reuse issues would still remain
What I Changed in Local web_3
After confirming the legacy problem, I rebuilt the admin captcha flow in local web_3 as a complete backend-controlled loop.
The new flow is:
- The frontend requests
GET /api/admin/login/captcha - The backend generates:
captchaIda4-digit captcha code aBase64 PNGimage - The backend writes the captcha answer into Redis
- The frontend only displays the image and keeps the
captchaId - When the user logs in, the frontend submits:
usernameuserpwdcaptchaIdcaptchaCode - The backend validates the captcha against Redis first
- Validation uses
getAndDelete - Once it is read successfully, it is deleted immediately
This design means one captcha gets exactly one attempt.
No matter whether login eventually succeeds or fails, that captcha becomes invalid and cannot remain in the system for repeated submission.
Core Rules of the New Captcha Flow
After this refactor, I now summarize the Web login captcha specification into the following rules.
Rule 1: The Captcha Must Be Generated by the Backend
The captcha image, captcha answer, and captchaId must all be generated by the backend. The frontend only displays the image. It does not generate the answer and does not store the correct answer.
That prevents answer leakage into the frontend and ensures the validation source of truth stays on the server side only.
Rule 2: The Captcha Answer Must Be Stored in Short-Lived Backend Storage
This time I chose Redis. It fits this kind of short-lived, one-time, low-value but security-sensitive data very well.
Compared with session/file, Redis is more suitable for later expansion:
- It supports TTL naturally
- It supports atomic deletion
- It supports multi-node sharing
- It can hold temporary login-risk state as well
Rule 3: The Login API Must Validate Captcha Before Credential Authentication
Captcha validation cannot live in a side path that exists visually but does not affect the main login flow. It must happen before username and password are allowed to enter the actual authentication logic.
The correct order should be:
Validate captcha first
Then validate username and password
Finally establish the login state
If captcha validation fails, the credential flow should not continue.
Rule 4: The Captcha Must Be One-Time Use
I deliberately used getAndDelete, meaning read-and-delete, to make sure a captcha can only ever be used once.
This matters a lot. If a captcha is not destroyed immediately after validation, then several bad outcomes become possible:
- The same captcha can be reused
- Automated scripts can keep hammering the same challenge
- Retry flows may accidentally reuse an old captcha
One-time consumption is one of the core conditions for a captcha mechanism to be meaningfully effective.
Rule 5: It Must Expire on Both Success and Failure
Many implementations only delete the captcha after a successful login. That is still not strict enough.
The more robust approach is:
- Delete it immediately after a successful validation
- Delete it after a failed attempt as well
Because a captcha is meant to be a one-time challenge. A user should not be allowed to keep trying multiple guesses against the same captchaId.
The Most Practical Benefits of Moving This to Redis
My biggest takeaway from this change is not that “the technology is more advanced now.” It is that the entire login security path is finally closed.
The practical gains are:
The Logic Is Finally Complete
The old version was “there is a captcha UI, but login does not validate it.”
Now the flow is:
- Backend generation
- Backend storage
- Frontend display
- Backend validation
- One-time consumption
That is what a complete security flow actually looks like.
It No Longer Depends on Local Session Files
The old site’s captcha answer effectively lived in session/file and /tmp sess_*.
That may work in a single-machine setup, but it is still tightly coupled to one local runtime. Redis is a much better fit for this type of temporary state and is easier to control and observe.
One-Time Challenges Become Natural
Redis is naturally good at this kind of behavior:
- TTL expiration
getAndDelete- Delete-on-success
- Delete-on-failure
Those semantics map directly to captcha requirements.
It Is Better for Multi-Node Deployment
If the system later stops being single-node and multiple Web instances start handling requests, session/file quickly becomes fragile because state consistency across machines is hard to guarantee.
Redis, as shared storage, lets every Web node read the same captcha state. That is critical for future horizontal scaling.
It Makes Later Risk-Control Extensions Easier
Once captcha state lives in Redis, several related security features become easier to add later:
- SMS verification codes
- Login failure counters
- Risk-control counters
- Temporary tokens
- Short-lived AI/RAG caches
From an engineering perspective, this means temporary login-state management has moved into a place that is much easier to govern.
Supporting Work I Also Did Along the Way
To make the new web_3 flow run end to end in local development, I also handled a few supporting tasks:
- Started a local Redis instance on
127.0.0.1:6379 - Fixed a YAML configuration issue in
backend-java - Cleared backend port conflicts
- Updated the admin frontend to allow LAN access by listening on
0.0.0.0:9511 - Improved mobile and narrow-screen layout for the login page
These are not the captcha feature itself, but they are exactly the kind of supporting work that determines whether the feature merely exists in code or actually runs as a full local system.
My Final Specification Conclusion
After this audit and refactor, my baseline specification for Web login captcha is basically fixed as this:
- The captcha must be generated by the backend
- The captcha answer must exist only on the backend
- The frontend should only display the image and submit
captchaId - The login API must validate captcha before validating credentials
- The captcha must be one-time use
- It must expire on both success and failure
- Short-lived captcha state should use Redis instead of local session files
If any one of these is missing, I no longer consider the captcha implementation properly in place.
Summary
In one sentence:
The legacy server’s admin captcha was generated by ThinkPHP and stored in Session, but the admin login logic never actually validated it; the old server also was not really using Redis. The new
web_3flow has already been upgraded into a standard Redis captcha pipeline: backend generation, Redis storage, frontend display, backend validation, one-time consumption, and invalidation on both success and failure.
For me, the most important outcome of this work is not simply “moving captcha to Redis.” It is that the captcha is no longer just something that looks present in the UI. It is now part of a real backend security chain. That is the difference between decoration and protection.