快轉到主要內容

本站嘗試使用數學題用於驗證訪客年齡是否已成年

· 民國115年丙午年
·
切換正體/简体 ·
分類 人文藝術 隨筆
標籤 Blogging Hugo
目錄

基於義務教育階段數學的網頁年齡驗證機制。This is a JavaScript age verification system that requires visitors to solve a math problem from the K–12 curriculum before accessing contents.

喔呀?

你因爲

數學不好

被傳送到了

這個頁面?

沒事沒事。

放心,所有答案都在這裡了。

起因
#

2026年6月,在網頁加入數學題用於驗證訪客年齡是否成年。

答對了有獎勵,答錯了有懲罰(畏怖)

在色色的頁面插入數學題驗證訪客年齡。例如這個頁面:個人推薦10部實用的裏番

事由:最近很多國家的法律紛紛要求加強網站驗證使用者年齡,於是我就在想,不侵犯使用者隱私的年齡驗證方法…就是加入數學題!驗證你高中有沒有畢業。讓我看看你發育正不正常啊!

用JavaScript在色色NSFW的頁面插入題目。重新整理頁面可換題目。爲了防止直接右鍵Google算式,使用CSS禁止選取文字。爲了簡化設計我沒有使用LaTeX渲染公式。

數學很好!是宇宙語言,外國人不用翻譯也看得懂。

透過一系列跨冊跨科目的隨機題目檢測訪客的年齡。內容包含冪次、開根號、三角函數、對數、插值法、微積分的簡單運算。微積分因爲一些高中文組沒有教所以把抽到的機率設很低。這些題目的程度應該是臺灣、中國大陸、美國高中生都能解出來的簡單題目。

破解這個年齡驗證JavaScript的方法很簡單,因爲大體沿用Hugo年齡驗證程式碼,就是在瀏覽器針對ivonblog.com放一塊「登dua郎」臺羅拼音的「餅乾」,或是直接用uBlock Origin封鎖元素。但是知道怎麼繞過去的人,應該不會連這些題目都答不出來吧。

題目處理邏輯如下。嗯,就是這麼暴力簡單。我原本的設計還想把題目存在Cloudflare Workers然後交換金鑰解密後才會回傳答案,避免script kiddie直接看到答案作弊。


<script>
  (function () {
    const cookieName = "tng-toa-lang"; // tńg-tōa-lâng. Are you adult?
    const cookieValue = "yes";
    const cookieMaxAge = 86400;
    const rickrollUrl = "https://ivonblog.com/posts/hugo-age-verification-with-math-questions/";
    const YOU_SEE_THIS_NOW_YOU_ARE_THE_CHEATER = "";

    const normalQuestions = [
      { question: "2^6 + √100  = ?", encodedAnswers: ["NzQ="] },
      { question: "√144 + 3^2 = ?", encodedAnswers: ["MjE="] },
      { question: "2 × sin(0°) = ?", encodedAnswers: ["MA=="] },
      { question: "cos(90°) x 999 = ?", encodedAnswers: ["MA=="] },
      { question: "tan(45°) x 5 = ?", encodedAnswers: ["NQ=="] },
      { question: "cos(90°) + sin(0°) = ?", encodedAnswers: ["MA=="] },
      { question: "log10(1000) = ?", encodedAnswers: ["Mw=="] },
      { question: "log10(100) = ?", encodedAnswers: ["Mg=="] },
      { question: "log10(10) = ?", encodedAnswers: ["MQ=="] },
      { question: "log10(1) = ?", encodedAnswers: ["MA=="] },
      { question: "x + 7 = 13, x = ?", encodedAnswers: ["Ng=="] },
      { question: "3x = 21, x = ?", encodedAnswers: ["Nw=="] },
      { question: "3² + 4² = c². c = ?", encodedAnswers: ["NQ=="] },
      { question: "6² + 8² = c². c = ?", encodedAnswers: ["MTA="] },
      { question: "| -9 | = ?", encodedAnswers: ["OQ=="] },
      { question: "| -7 | = ?", encodedAnswers: ["Nw=="] },
      { question: "| -8 | = ?", encodedAnswers: ["OA=="] },
      { question: "| (-9) + 8 | = ?", encodedAnswers: ["MQ=="] },
      { question: "| (-731) + 731| = ?", encodedAnswers: ["MA=="] },
      { question: "| (-228) - 228 | = ?", encodedAnswers: ["NDU2"] },
      { question: "| -67 | = ?", encodedAnswers: ["Njc="] },
      { question: "| -1 - (1) | = ?", encodedAnswers: ["Mg=="] },
      { question: "| 666 - (-333) | = ?", encodedAnswers: ["OTk5"] },
      { question: "P = 1, Q = 0. P ∧ Q = ?", encodedAnswers: ["MA=="] },
      { question: "P = 1, Q = 0. P ∨ Q = ?", encodedAnswers: ["MQ=="] },
      { question: "∫_0^1 2x dx = ?", encodedAnswers: ["MQ=="] },
      { question: "∫_0^2 2x dx = ?", encodedAnswers: ["NA=="] },
      { question: "∫_0^3 2x dx = ?", encodedAnswers: ["OQ=="] },
      { question: "∫_0^4 2x dx = ?", encodedAnswers: ["MTY="] }
    ];
    const luckyQuestions = [
      { question: "f'(c) = (f(3)-f(1))/(3-1), f(3)=7, f(1)=3, f'(c)=?", encodedAnswers: ["Mg=="] },
      { question: "∫_0^5 2x dx = ?", encodedAnswers: ["MjU="] },
      { question: "Σ(k=1..10) k + Σ(k=1..10)(11-k) - 10 = ?", encodedAnswers: ["MTAw"] }
    ];

    const luckyRate = 0.2;

    const overlay = document.getElementById("content-warning-overlay");
    const closeTarget = document.getElementById("content-warning-accepted");

    if (!overlay) return;

    const hasAccepted = document.cookie
      .split("; ")
      .some((cookie) => cookie === cookieName + "=" + cookieValue);

    const unlockPage = function () {
      document.documentElement.classList.remove("content-warning-locked");
    };

    const closeWarning = function () {
      overlay.remove();
      if (closeTarget) closeTarget.remove();
      unlockPage();
    };

    if (hasAccepted) {
      closeWarning();
      return;
    }

    document.documentElement.classList.add("content-warning-locked");

    const intro = overlay.querySelector("[data-content-warning-intro]");
    const startButton = overlay.querySelector("[data-content-warning-start]");
    const questionForm = overlay.querySelector("[data-content-warning-question]");
    const questionText = overlay.querySelector("[data-content-warning-question-text]");
    const answerInput = overlay.querySelector("[data-content-warning-answer]");
    const feedback = overlay.querySelector("[data-content-warning-feedback]");
    const declineButtons = overlay.querySelectorAll("[data-content-warning-decline]");

    let currentQuestion = null;

    const pickQuestion = function () {
      const pool = Math.random() < luckyRate
        ? luckyQuestions
        : normalQuestions;

      return pool[Math.floor(Math.random() * pool.length)];
    };

    const normalizeAnswer = function (value) {
      return String(value)
        .trim()
        .toLowerCase()
        .replace(/\s+/g, "")
        .replace(/ /g, "")
        .replace(/×/g, "*")
        .replace(/−/g, "-")
        .replace(/,/g, ",")
        .replace(/。/g, ".")
        .replace(/²/g, "^2")
        .replace(/³/g, "^3");
    };

    const decodeAnswer = function (value) {
      return atob(value);
    };

    const isCorrectAnswer = function (input, question) {
      const normalizedInput = normalizeAnswer(input);
      const answers = Array.isArray(question.encodedAnswers) ? question.encodedAnswers.map(decodeAnswer) : question.answers;
      return answers.some(function (answer) {
        return normalizeAnswer(answer) === normalizedInput;
      });
    };

    const setAcceptedCookie = function () {
      const secure = window.location.protocol === "https:" ? "; Secure" : "";
      document.cookie =
        cookieName +
        "=" +
        cookieValue +
        "; path=/; max-age=" +
        cookieMaxAge +
        "; SameSite=Lax" +
        secure;
    };

    const showQuestion = function () {
      currentQuestion = pickQuestion();

      if (intro) intro.hidden = true;
      if (questionForm) questionForm.hidden = false;
      if (questionText) questionText.textContent = currentQuestion.question;
      if (feedback) feedback.textContent = "";
      if (answerInput) {
        answerInput.value = "";
        window.setTimeout(function () {
          answerInput.focus();
        }, 0);
      }
    };

    const passGate = function () {
      setAcceptedCookie();
      closeWarning();
    };

    const failGate = function () {
      window.location.href = rickrollUrl;
    };

    if (startButton) {
      startButton.addEventListener("click", function () {
        showQuestion();
      });
    }

    if (questionForm) {
      questionForm.addEventListener("submit", function (event) {
        event.preventDefault();

        if (!currentQuestion) {
          showQuestion();
          return;
        }

        if (isCorrectAnswer(answerInput ? answerInput.value : "", currentQuestion)) {
          passGate();
        } else {
          failGate();
        }
      });
    }

    declineButtons.forEach(function (button) {
      button.addEventListener("click", function (event) {
        event.preventDefault();
        failGate();
      });
    });
  })();
</script>

(題目滾動式更新,請勿洩題。)

本實驗爲期一個月,將搭配Google Analytics觀察跳出率變化,看看數學究竟能夠擊退多少訪客。

一個月後再回來看看成果。

結果
#

一個月前,我做了一個實驗,在查看網站NSFW內容之前需要通過數學題的年齡驗證,用來看看你發育正不正常。看看這會對網站流量造成多大影響。

題目大概高中數學程度,最難的題目也就簡單微積分以及拉格朗日多項式而已的說。

結論是:好像沒差?從Google Analytics後臺資料來看,網站的幾個熱門NSFW網頁流量還是很高,參與度(87%)和跳出率(12%)都沒有顯著變化。一堆人成功通過了考驗,看來大家的文化水準都很高。也有可能AI解題真的太方便了。

好的,所以這個年齡驗證機制我就決定保留下來了!我會酬情增加送分題(判斷絕對值)的比例的。

刪除
#

幾天後。

因爲急用,我快爆炸了,卻在看自己文章的時候忘記數學題答案被擋在外面5分鐘,感覺龜懶趴火,所以我決定把blog年齡驗證數學題的機制刪除了。實驗一個月我蒐集到的資料也滿意了。

啊啊~我終究覺得年齡驗證這個框框會阻礙資訊自由流通,成爲變相paywall(儘管你只要付出腦力就能通過)。還有NoScript和RSS用戶表示他們關不掉數學題驗證,影響閱讀,所以我決定還是刪除了,現在各位可以自由色色啦。

但形式上的年齡驗證提示框還是要有,目的是爲了防止被iWIN之類的機構找碴,故需要在進入網站前設立R18防護機制。年齡驗證提示本身是用HTML與CSS實現的,在沒有JavaScript的環境也能關掉。

相關文章


感謝您的閱讀。我寫作是為了誠實表達想法,而不是追逐社群互動與流量。我很樂意傾聽你在仔細閱讀我文章之後的心得。若有內容勘誤 or 技術問題 or 回饋想法,歡迎透過本站「關於」頁面的Email與我聯絡。