使用 Node.js 构建一个 CAPTCHA 生成器

2025-06-08

使用 Node.js 构建一个 CAPTCHA 生成器

CAPTCHA无法访问,在某些情况下甚至无效,但通过生成我们自己的 CAPTCHA,我们可以学到很多东西!

本文的源代码位于healeycodes/captcha-api

垃圾邮件的解决方案

假设有一位客户需要解决机器人垃圾邮件问题。他们要求提供一张图片和一串图片文字。你回想起了那些你苦苦挣扎却无法解决的、令人费解的字母和数字组合。尽管如此,你还是同意了这项任务。

我是人类吗?是的。我能凭空打出phi吗?不行。硬验证码图片

该客户拥有一系列网站。不同位置需要不同大小的验证码。他们会提供宽度和高度。这描述了我们 API 的规范。

JavaScript 非常适合生成图像,因为我们可以依赖Canvas API。我发现,当我遇到很多 Stackoverflow 内容卡住的时候,用它非常方便。

我们不想在浏览器领域生成我们的验证码,因为我们试图阻止的机器人可以检查源代码,找到内存中的值,并尝试各种其他棘手的策略。

Node.js 服务

让我们把它移到后端,变成一个可以按需调用的服务。有人已经解决了在没有 Web API 的情况下访问该 API 的问题,方法是使用node-canvasnpm i canvas

[canvas] 是 Web Canvas API 的一个实现,并尽可能紧密地实现该 API。

我们每次都需要生成一些随机文本。所以,让我们编写两个函数来帮助我们。对于我们的 API,我们将逻辑分解成几个函数,每个函数只做一件事(并且要把这件事做好),这样最终的结果就更容易理解和维护了。

/* captcha.js */

// We'll need this later
const { createCanvas } = require("canvas");

// https://gist.github.com/wesbos/1bb53baf84f6f58080548867290ac2b5
const alternateCapitals = str =>
  [...str].map((char, i) => char[`to${i % 2 ? "Upper" : "Lower"}Case`]()).join("");

// Get a random string of alphanumeric characters
const randomText = () =>
  alternateCapitals(
    Math.random()
      .toString(36)
      .substring(2, 8)
  );

画布中无法自动缩放文本(就像浏览器中的weeps一样),所以我们还需要一些辅助函数来实现。根据验证码的长度以及文本在图像中的显示位置,你可能需要进行测试运行。以下是我之前准备的一些变量。

const FONTBASE = 200;
const FONTSIZE = 35;

// Get a font size relative to base size and canvas width
const relativeFont = width => {
  const ratio = FONTSIZE / FONTBASE;
  const size = width * ratio;
  return `${size}px serif`;
};

这样就可以缩放文本,只要画布的比例保持不变,我们就可以得到相似的图像。

对于本文,我们只需旋转文本,但有很多方法可以扭曲文本以将其隐藏在机器人之外,我很想看看您想出了什么(尝试搜索“透视变换画布 javascript”)。

旋转画布时,我们传递的值是弧度,因此我们需要将随机度数乘以Math.PI / 180

// Get a float between min and max
const arbitraryRandom = (min, max) => Math.random() * (max - min) + min;

// Get a rotation between -degrees and degrees converted to radians
const randomRotation = (degrees = 15) => (arbitraryRandom(-degrees, degrees) * Math.PI) / 180;

我保证,不再使用辅助函数了。我们现在要开始真正的核心部分了。逻辑被分解成两个函数。configureText一个函数接受一个 Canvas 对象,并添加和居中我们的随机文本。generate另一个函数接受一个宽度和高度值(还记得我们之前给出的规范吗?),并返回一个 PNG 图像的数据 URL——也就是我们的验证码。

数据 URL(以data:方案为前缀的 URL)允许内容创建者在文档中嵌入小文件。

// Configure captcha text
const configureText = (ctx, width, height) => {
  ctx.font = relativeFont(width);
  ctx.textBaseline = "middle";
  ctx.textAlign = "center";
  const text = randomText();
  ctx.fillText(text, width / 2, height / 2);
  return text;
};

// Get a PNG dataURL of a captcha image
const generate = (width, height) => {
  const canvas = createCanvas(width, height);
  const ctx = canvas.getContext("2d");
  ctx.rotate(randomRotation());
  const text = configureText(ctx, width, height);
  return {
    image: canvas.toDataURL(),
    text: text
  };
};

我们可以将除 之外的所有函数视为generate私有函数,这些函数不应在其他地方使用,因此我们只需导出此函数。

module.exports = generate;

Express 提供的 API

目前为止,我们有一个文件,captcha.js其中包含图像生成逻辑。为了让其他人可以调用此功能,我们将通过 HTTP API 提供它。Express 为此类任务提供了最强大的社区支持。

我们将举办的路线是:

  • /test/:width?/:height?/
    • 用于获取手动测试的图像标签。
  • /captcha/:width?/:height?/
    • 用于获取 CAPTCHA 对象以供正确使用。

此处路由中的问号是 Express 中可选 URL 参数的语法。这意味着客户端可以不提供任何参数,也可以只提供第一个参数,或者两个参数都提供。我们将验证传入的值是否为整数(这是 Canvas 的必需参数),如果不是,我们将使用合理的默认值。

Express 应用程序完整内容:

/* app.js */

const captcha = require("./captcha");
const express = require("express");
const app = express();

// Human checkable test path, returns image for browser
app.get("/test/:width?/:height?/", (req, res) => {
  const width = parseInt(req.params.width) || 200;
  const height = parseInt(req.params.height) || 100;
  const { image } = captcha(width, height);
  res.send(`<img class="generated-captcha" src="${image}">`);
});

// Captcha generation, returns PNG data URL and validation text
app.get("/captcha/:width?/:height?/", (req, res) => {
  const width = parseInt(req.params.width) || 200;
  const height = parseInt(req.params.height) || 100;
  const { image, text } = captcha(width, height);
  res.send({ image, text });
});

module.exports = app;

此 Express 应用已导出,以便我们进行测试。此时我们的 API 已可用。我们只需提供以下文件负责的服务即可。

/* server.js */

const app = require("./app");
const port = process.env.PORT || 3000;

app.listen(port, () => console.log(`captcha-api listening on ${port}!`));

导航至 会http://localhost:3000/test奖励我们基本的验证码。如果省略,浏览器会添加bodyand标签。html

我们的验证码

有效的数据 URL

是时候写一些测试了,但首先,别再用那些笨重的正则表达式了。有一个已经解决了这个问题。valid-data-url它的功能和它的名字完全一致。

我喜欢用 Jest 作为我的测试运行器。原因很简单,就是它总是对我有用,即使它没用,我也能找到解决办法。我的设置是像这样设置scripts密钥package.json

  "scripts": {
    "test": "jest"
  }

这样我就可以输入了npm test(这也是许多 CI 系统的默认设置)。然后 Jest 会查找并运行我们所有的测试。

我们应用的测试文件导入了 Express 应用程序对象,并用supertest它来模拟 HTTP 请求。我们使用 async/await 语法来减少回调。

/* app.test.js */

const request = require("supertest");
const assert = require("assert");
const validDataURL = require("valid-data-url");
const app = require("../app");

describe("captcha", () => {
  describe("testing captcha default", () => {
    it("should respond with a valid data URL", async () => {
      const image = await request(app)
        .get("/captcha")
        .expect(200)
        .then(res => res.body.image);
      assert(validDataURL(image));
    });
  });

  describe("testing captcha default with custom params", () => {
    it("should respond with a valid data URL", async () => {
      const image = await request(app)
        .get("/captcha/300/150")
        .expect(200)
        .then(res => res.body.image);
      assert(validDataURL(image));
    });
  });
});

考虑到这个应用程序的大小(小),我满足于对其进行两次集成测试。

与 GitHub 工作流持续集成

由于我们使用了标准的 npm test 命令(npm test)来配置我们的仓库,因此只需点击几下即可设置 GitHub 工作流。这样,每次推送代码时,我们的应用程序都会被构建和测试。

构建并测试你的 JavaScript 代码库。Node.js 使用 npm 构建并测试 Node.js 项目。“设置此工作流程”。npm ci\nnpm run build --if-present\nnpm test

现在我们有一个甜蜜的徽章可以炫耀!


与 150 多名订阅我的关于编程和个人成长的新闻通讯的人一起!

我发布有关科技的推文@healeycodes

鏂囩珷鏉ユ簮锛�https://dev.to/healeycodes/let-s-build-a-captcha-generator-with-node-js-165i
PREV
我的结对编程 AWS Security LIVE 经验!
NEXT
Web 缓存简介(含 Python 示例)Django 怎么样?最难的部分