<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Login Security on XEDCZQ Blog</title><link>https://xedczq.cn/en/tags/login-security/</link><description>Recent content in Login Security on XEDCZQ Blog</description><generator>Hugo -- gohugo.io</generator><language>en-us</language><lastBuildDate>Sat, 20 Jun 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://xedczq.cn/en/tags/login-security/index.xml" rel="self" type="application/rss+xml"/><item><title>Web Login Captcha Specification: From Legacy Admin Audit to One-Time Redis Validation</title><link>https://xedczq.cn/en/post/web_logincaptchaspec/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0800</pubDate><guid>https://xedczq.cn/en/post/web_logincaptchaspec/</guid><description>&lt;h1 id="web-login-captcha-specification-from-legacy-admin-audit-to-one-time-redis-validation"&gt;&lt;a href="#web-login-captcha-specification-from-legacy-admin-audit-to-one-time-redis-validation" class="header-anchor"&gt;&lt;/a&gt;Web Login Captcha Specification: From Legacy Admin Audit to One-Time Redis Validation
&lt;/h1&gt;&lt;p&gt;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 &lt;code&gt;web_3&lt;/code&gt; system.&lt;/p&gt;
&lt;p&gt;I mainly did three things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Audited the real captcha logic behind &lt;code&gt;yzdwx.yizhoudao.net/admin/&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Verified whether the legacy Web system was actually using Redis&lt;/li&gt;
&lt;li&gt;Rebuilt the admin captcha flow in local &lt;code&gt;web_3&lt;/code&gt; as &lt;code&gt;backend generation + Redis storage + one-time backend validation&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The final conclusion was very clear: &lt;strong&gt;the legacy admin page still displayed a captcha, but the login flow did not actually validate it; the new &lt;code&gt;web_3&lt;/code&gt; flow has already been rebuilt into a proper Redis-backed captcha pipeline.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="why-i-wrote-this-note"&gt;&lt;a href="#why-i-wrote-this-note" class="header-anchor"&gt;&lt;/a&gt;Why I Wrote This Note
&lt;/h2&gt;&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;This audit made one thing much clearer to me: &lt;strong&gt;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.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="the-real-captcha-logic-on-the-old-server"&gt;&lt;a href="#the-real-captcha-logic-on-the-old-server" class="header-anchor"&gt;&lt;/a&gt;The Real Captcha Logic on the Old Server
&lt;/h2&gt;&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;I confirmed the following site layout:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Domain &lt;code&gt;yzdwx.yizhoudao.net&lt;/code&gt; points to: &lt;code&gt;/www/wwwroot/yzdwx5.weitaibei.com/public_html&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Application entry point: &lt;code&gt;/www/wwwroot/yzdwx5.weitaibei.com/public_html/index.php&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Actual business code directory: &lt;code&gt;/www/wwwroot/yzdwx5.weitaibei.com/system/application&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/captcha
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Refreshing the image requests:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;/captcha?random=...
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This route ultimately comes from ThinkPHP&amp;rsquo;s captcha package. Tracing deeper, I confirmed that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The captcha image is indeed generated by the backend&lt;/li&gt;
&lt;li&gt;The captcha answer is written into &lt;code&gt;Session&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;It is not written to the frontend&lt;/li&gt;
&lt;li&gt;It is also not written to Redis&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The more important part came when I traced the admin login controller. The legacy login method accepted a &lt;code&gt;$verify&lt;/code&gt; parameter, but the actual logic only did the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check whether the username is empty&lt;/li&gt;
&lt;li&gt;Check whether the password is empty&lt;/li&gt;
&lt;li&gt;Call &lt;code&gt;rbaclogin(...)&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I did not find any real captcha validation logic such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;captcha_check($verify)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;validate(...captcha...)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Manual captcha comparison&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the real state of the old admin captcha was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The page shows a captcha&lt;/li&gt;
&lt;li&gt;The backend generates a captcha image&lt;/li&gt;
&lt;li&gt;The captcha answer is stored in &lt;code&gt;session&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;But the login business flow never actually validates it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That means the legacy admin captcha was very likely in a state of &amp;ldquo;the UI is still there, but validation has already failed to exist or was accidentally dropped.&amp;rdquo;&lt;/p&gt;
&lt;h2 id="how-i-confirmed-it"&gt;&lt;a href="#how-i-confirmed-it" class="header-anchor"&gt;&lt;/a&gt;How I Confirmed It
&lt;/h2&gt;&lt;p&gt;This was not guesswork, and it was not just based on what the frontend page looked like. I followed a fairly reliable verification chain.&lt;/p&gt;
&lt;p&gt;I first confirmed SSH access and the site directory:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Connect to the server through my local SSH alias&lt;/li&gt;
&lt;li&gt;Inspect the Nginx config and confirm which directory &lt;code&gt;yzdwx.yizhoudao.net&lt;/code&gt; really points to&lt;/li&gt;
&lt;li&gt;Check PHP-FPM and Nginx runtime status&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then I traced the application layer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Find the admin login template&lt;/li&gt;
&lt;li&gt;Find where &lt;code&gt;/captcha&lt;/code&gt; is routed&lt;/li&gt;
&lt;li&gt;Find the captcha generation logic&lt;/li&gt;
&lt;li&gt;Find the captcha validation logic&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finally, I inspected the admin login controller:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check whether &lt;code&gt;login()&lt;/code&gt; actually calls captcha validation&lt;/li&gt;
&lt;li&gt;The answer was no&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2 id="whether-the-legacy-web-system-really-used-redis"&gt;&lt;a href="#whether-the-legacy-web-system-really-used-redis" class="header-anchor"&gt;&lt;/a&gt;Whether the Legacy Web System Really Used Redis
&lt;/h2&gt;&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;My conclusion this time was:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The repository does contain a Redis utility class&lt;/li&gt;
&lt;li&gt;But I did not find real business code on the live legacy Web system that actually used it&lt;/li&gt;
&lt;li&gt;I also did not find a running Redis service on the server itself&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The evidence I checked included:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;ThinkPHP session configuration&lt;/li&gt;
&lt;li&gt;PHP &lt;code&gt;session.save_handler&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Whether &lt;code&gt;/tmp&lt;/code&gt; contained many &lt;code&gt;sess_*&lt;/code&gt; files&lt;/li&gt;
&lt;li&gt;Whether &lt;code&gt;redis-server&lt;/code&gt; existed on the server&lt;/li&gt;
&lt;li&gt;Whether &lt;code&gt;redis-cli&lt;/code&gt; existed&lt;/li&gt;
&lt;li&gt;Whether port &lt;code&gt;6379&lt;/code&gt; was listening&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The final facts were:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The default session driver was not Redis&lt;/li&gt;
&lt;li&gt;PHP&amp;rsquo;s current session handler was &lt;code&gt;files&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;There were indeed many &lt;code&gt;sess_*&lt;/code&gt; files under &lt;code&gt;/tmp&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;No Redis process was found&lt;/li&gt;
&lt;li&gt;No &lt;code&gt;6379&lt;/code&gt; listener was found&lt;/li&gt;
&lt;li&gt;No Redis binary was found either&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So the most accurate statement is not &amp;ldquo;the old project never had Redis&amp;rdquo;, but rather:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;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.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;h2 id="what-was-actually-wrong-with-the-legacy-captcha"&gt;&lt;a href="#what-was-actually-wrong-with-the-legacy-captcha" class="header-anchor"&gt;&lt;/a&gt;What Was Actually Wrong with the Legacy Captcha
&lt;/h2&gt;&lt;p&gt;If I summarize the legacy admin problem in one sentence, I would put it like this:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;The problem was not that captcha generation failed. The problem was that captcha never entered the real login validation path.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;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 &amp;ldquo;having no captcha&amp;rdquo;, because product, QA, and even developers can all be fooled by what they see on the page.&lt;/p&gt;
&lt;p&gt;From a specification perspective, the old version had at least two obvious gaps:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The captcha answer was stored in Session, but the login API never validated it&lt;/li&gt;
&lt;li&gt;The captcha was not designed as a one-time challenge, so even if validation were later added, reuse issues would still remain&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="what-i-changed-in-local-web_3"&gt;&lt;a href="#what-i-changed-in-local-web_3" class="header-anchor"&gt;&lt;/a&gt;What I Changed in Local web_3
&lt;/h2&gt;&lt;p&gt;After confirming the legacy problem, I rebuilt the admin captcha flow in local &lt;code&gt;web_3&lt;/code&gt; as a complete backend-controlled loop.&lt;/p&gt;
&lt;p&gt;The new flow is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The frontend requests &lt;code&gt;GET /api/admin/login/captcha&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The backend generates:
&lt;code&gt;captchaId&lt;/code&gt;
a &lt;code&gt;4&lt;/code&gt;-digit captcha code
a &lt;code&gt;Base64 PNG&lt;/code&gt; image&lt;/li&gt;
&lt;li&gt;The backend writes the captcha answer into Redis&lt;/li&gt;
&lt;li&gt;The frontend only displays the image and keeps the &lt;code&gt;captchaId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;When the user logs in, the frontend submits:
&lt;code&gt;username&lt;/code&gt;
&lt;code&gt;userpwd&lt;/code&gt;
&lt;code&gt;captchaId&lt;/code&gt;
&lt;code&gt;captchaCode&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The backend validates the captcha against Redis first&lt;/li&gt;
&lt;li&gt;Validation uses &lt;code&gt;getAndDelete&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Once it is read successfully, it is deleted immediately&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This design means one captcha gets exactly one attempt.&lt;/p&gt;
&lt;p&gt;No matter whether login eventually succeeds or fails, that captcha becomes invalid and cannot remain in the system for repeated submission.&lt;/p&gt;
&lt;h2 id="core-rules-of-the-new-captcha-flow"&gt;&lt;a href="#core-rules-of-the-new-captcha-flow" class="header-anchor"&gt;&lt;/a&gt;Core Rules of the New Captcha Flow
&lt;/h2&gt;&lt;p&gt;After this refactor, I now summarize the Web login captcha specification into the following rules.&lt;/p&gt;
&lt;h3 id="rule-1-the-captcha-must-be-generated-by-the-backend"&gt;&lt;a href="#rule-1-the-captcha-must-be-generated-by-the-backend" class="header-anchor"&gt;&lt;/a&gt;Rule 1: The Captcha Must Be Generated by the Backend
&lt;/h3&gt;&lt;p&gt;The captcha image, captcha answer, and &lt;code&gt;captchaId&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;That prevents answer leakage into the frontend and ensures the validation source of truth stays on the server side only.&lt;/p&gt;
&lt;h3 id="rule-2-the-captcha-answer-must-be-stored-in-short-lived-backend-storage"&gt;&lt;a href="#rule-2-the-captcha-answer-must-be-stored-in-short-lived-backend-storage" class="header-anchor"&gt;&lt;/a&gt;Rule 2: The Captcha Answer Must Be Stored in Short-Lived Backend Storage
&lt;/h3&gt;&lt;p&gt;This time I chose Redis. It fits this kind of short-lived, one-time, low-value but security-sensitive data very well.&lt;/p&gt;
&lt;p&gt;Compared with &lt;code&gt;session/file&lt;/code&gt;, Redis is more suitable for later expansion:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It supports TTL naturally&lt;/li&gt;
&lt;li&gt;It supports atomic deletion&lt;/li&gt;
&lt;li&gt;It supports multi-node sharing&lt;/li&gt;
&lt;li&gt;It can hold temporary login-risk state as well&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="rule-3-the-login-api-must-validate-captcha-before-credential-authentication"&gt;&lt;a href="#rule-3-the-login-api-must-validate-captcha-before-credential-authentication" class="header-anchor"&gt;&lt;/a&gt;Rule 3: The Login API Must Validate Captcha Before Credential Authentication
&lt;/h3&gt;&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;The correct order should be:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-text" data-lang="text"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Validate captcha first
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Then validate username and password
&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;Finally establish the login state
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If captcha validation fails, the credential flow should not continue.&lt;/p&gt;
&lt;h3 id="rule-4-the-captcha-must-be-one-time-use"&gt;&lt;a href="#rule-4-the-captcha-must-be-one-time-use" class="header-anchor"&gt;&lt;/a&gt;Rule 4: The Captcha Must Be One-Time Use
&lt;/h3&gt;&lt;p&gt;I deliberately used &lt;code&gt;getAndDelete&lt;/code&gt;, meaning read-and-delete, to make sure a captcha can only ever be used once.&lt;/p&gt;
&lt;p&gt;This matters a lot. If a captcha is not destroyed immediately after validation, then several bad outcomes become possible:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The same captcha can be reused&lt;/li&gt;
&lt;li&gt;Automated scripts can keep hammering the same challenge&lt;/li&gt;
&lt;li&gt;Retry flows may accidentally reuse an old captcha&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;One-time consumption is one of the core conditions for a captcha mechanism to be meaningfully effective.&lt;/p&gt;
&lt;h3 id="rule-5-it-must-expire-on-both-success-and-failure"&gt;&lt;a href="#rule-5-it-must-expire-on-both-success-and-failure" class="header-anchor"&gt;&lt;/a&gt;Rule 5: It Must Expire on Both Success and Failure
&lt;/h3&gt;&lt;p&gt;Many implementations only delete the captcha after a successful login. That is still not strict enough.&lt;/p&gt;
&lt;p&gt;The more robust approach is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Delete it immediately after a successful validation&lt;/li&gt;
&lt;li&gt;Delete it after a failed attempt as well&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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 &lt;code&gt;captchaId&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="the-most-practical-benefits-of-moving-this-to-redis"&gt;&lt;a href="#the-most-practical-benefits-of-moving-this-to-redis" class="header-anchor"&gt;&lt;/a&gt;The Most Practical Benefits of Moving This to Redis
&lt;/h2&gt;&lt;p&gt;My biggest takeaway from this change is not that &amp;ldquo;the technology is more advanced now.&amp;rdquo; It is that the entire login security path is finally closed.&lt;/p&gt;
&lt;p&gt;The practical gains are:&lt;/p&gt;
&lt;h3 id="the-logic-is-finally-complete"&gt;&lt;a href="#the-logic-is-finally-complete" class="header-anchor"&gt;&lt;/a&gt;The Logic Is Finally Complete
&lt;/h3&gt;&lt;p&gt;The old version was &amp;ldquo;there is a captcha UI, but login does not validate it.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Now the flow is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Backend generation&lt;/li&gt;
&lt;li&gt;Backend storage&lt;/li&gt;
&lt;li&gt;Frontend display&lt;/li&gt;
&lt;li&gt;Backend validation&lt;/li&gt;
&lt;li&gt;One-time consumption&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That is what a complete security flow actually looks like.&lt;/p&gt;
&lt;h3 id="it-no-longer-depends-on-local-session-files"&gt;&lt;a href="#it-no-longer-depends-on-local-session-files" class="header-anchor"&gt;&lt;/a&gt;It No Longer Depends on Local Session Files
&lt;/h3&gt;&lt;p&gt;The old site&amp;rsquo;s captcha answer effectively lived in &lt;code&gt;session/file&lt;/code&gt; and &lt;code&gt;/tmp sess_*&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h3 id="one-time-challenges-become-natural"&gt;&lt;a href="#one-time-challenges-become-natural" class="header-anchor"&gt;&lt;/a&gt;One-Time Challenges Become Natural
&lt;/h3&gt;&lt;p&gt;Redis is naturally good at this kind of behavior:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;TTL expiration&lt;/li&gt;
&lt;li&gt;&lt;code&gt;getAndDelete&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Delete-on-success&lt;/li&gt;
&lt;li&gt;Delete-on-failure&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Those semantics map directly to captcha requirements.&lt;/p&gt;
&lt;h3 id="it-is-better-for-multi-node-deployment"&gt;&lt;a href="#it-is-better-for-multi-node-deployment" class="header-anchor"&gt;&lt;/a&gt;It Is Better for Multi-Node Deployment
&lt;/h3&gt;&lt;p&gt;If the system later stops being single-node and multiple Web instances start handling requests, &lt;code&gt;session/file&lt;/code&gt; quickly becomes fragile because state consistency across machines is hard to guarantee.&lt;/p&gt;
&lt;p&gt;Redis, as shared storage, lets every Web node read the same captcha state. That is critical for future horizontal scaling.&lt;/p&gt;
&lt;h3 id="it-makes-later-risk-control-extensions-easier"&gt;&lt;a href="#it-makes-later-risk-control-extensions-easier" class="header-anchor"&gt;&lt;/a&gt;It Makes Later Risk-Control Extensions Easier
&lt;/h3&gt;&lt;p&gt;Once captcha state lives in Redis, several related security features become easier to add later:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SMS verification codes&lt;/li&gt;
&lt;li&gt;Login failure counters&lt;/li&gt;
&lt;li&gt;Risk-control counters&lt;/li&gt;
&lt;li&gt;Temporary tokens&lt;/li&gt;
&lt;li&gt;Short-lived AI/RAG caches&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From an engineering perspective, this means temporary login-state management has moved into a place that is much easier to govern.&lt;/p&gt;
&lt;h2 id="supporting-work-i-also-did-along-the-way"&gt;&lt;a href="#supporting-work-i-also-did-along-the-way" class="header-anchor"&gt;&lt;/a&gt;Supporting Work I Also Did Along the Way
&lt;/h2&gt;&lt;p&gt;To make the new &lt;code&gt;web_3&lt;/code&gt; flow run end to end in local development, I also handled a few supporting tasks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Started a local Redis instance on &lt;code&gt;127.0.0.1:6379&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Fixed a YAML configuration issue in &lt;code&gt;backend-java&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Cleared backend port conflicts&lt;/li&gt;
&lt;li&gt;Updated the admin frontend to allow LAN access by listening on &lt;code&gt;0.0.0.0:9511&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Improved mobile and narrow-screen layout for the login page&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2 id="my-final-specification-conclusion"&gt;&lt;a href="#my-final-specification-conclusion" class="header-anchor"&gt;&lt;/a&gt;My Final Specification Conclusion
&lt;/h2&gt;&lt;p&gt;After this audit and refactor, my baseline specification for Web login captcha is basically fixed as this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The captcha must be generated by the backend&lt;/li&gt;
&lt;li&gt;The captcha answer must exist only on the backend&lt;/li&gt;
&lt;li&gt;The frontend should only display the image and submit &lt;code&gt;captchaId&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;The login API must validate captcha before validating credentials&lt;/li&gt;
&lt;li&gt;The captcha must be one-time use&lt;/li&gt;
&lt;li&gt;It must expire on both success and failure&lt;/li&gt;
&lt;li&gt;Short-lived captcha state should use Redis instead of local session files&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;If any one of these is missing, I no longer consider the captcha implementation properly in place.&lt;/p&gt;
&lt;h2 id="summary"&gt;&lt;a href="#summary" class="header-anchor"&gt;&lt;/a&gt;Summary
&lt;/h2&gt;&lt;p&gt;In one sentence:&lt;/p&gt;

 &lt;blockquote&gt;
 &lt;p&gt;The legacy server&amp;rsquo;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 &lt;code&gt;web_3&lt;/code&gt; flow 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.&lt;/p&gt;

 &lt;/blockquote&gt;
&lt;p&gt;For me, the most important outcome of this work is not simply &amp;ldquo;moving captcha to Redis.&amp;rdquo; 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.&lt;/p&gt;</description></item></channel></rss>