Search by

xupaoye / cangqiong-captcha

XuPaoYe

Cangqiong CAPTCHA: open-source image challenges and proof-of-work primitives implemented in PHP/GD.

Package info

github.com/XuPaoYe/cangqiong-captcha

pkg:composer/xupaoye/cangqiong-captcha

Statistics

Installs: 32

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v1.2.0 2026-11-04 07:17 UTC

This package is auto-updated.

Last update: 2026-11-04 07:21:39 UTC


README

Cangqiong CAPTCHA · 纯 PHP/GD 开源验证码

简体中文 · English

苍穹验证码是一套独立维护、使用纯 PHP/GD 实现的验证码核心,支持文字点选、数字字母点选、图形点选、滑块拼图、旋转图片,以及独立的工作量证明(POW)。图像生成和答案校验均在 PHP 中完成,不依赖在线图片服务或第三方验证码平台。

它专注于图像生成和答案校验,不绑定业务框架。HTTP 接口、浏览器界面、会话、存储及登录认证由接入项目负责。

效果预览

以下为苍穹验证码接入项目后的完整弹窗实拍,包含顶部提示、验证码图片、刷新、关闭及确定按钮/拖动滑轨。图片由本包实际生成,界面使用接入项目的前端 SDK 和主题适配;本 PHP 包不包含这些前端文件。

截图以两倍渲染尺寸采集(748×724),点击图片可查看大图。

浅色主题

文字点选 数字字母点选 图形点选
文字点选 数字字母点选 图形点选
滑块拼图 旋转图片
滑块拼图 旋转图片

深色主题

文字点选 数字字母点选 图形点选
深色文字点选 深色数字字母点选 深色图形点选
滑块拼图 旋转图片
深色滑块拼图 深色旋转图片

另提供 POW 计算验证核心,无图形界面。背景图可替换,示例图片不代表安全强度。

能力

  • 五种图形题型,共用生成与校验接口。
  • 当前源码默认加入 1~2 个不同轮廓的滑块伪凹槽,保留单缺口模式。
  • 服务端生成带随机干扰的点选提示图片,不通过 JSON 下发目标明文或答案坐标。
  • 本地随机背景及有界裁切、翻转、亮度变化;可独立替换背景目录。
  • 严格检查数值、坐标、事件顺序、图片尺寸及时间边界。
  • 无状态 POW 核心,参数和计算量有上限。
  • Apache-2.0 开源;PHP 8.4、8.5 有自动化测试。

环境要求

PHP 8.4+,GD(JPEG、PNG、FreeType)和 mbstring。浏览器端需接入兼容 SDK;本仓库不包含完整前端,协议兼容基线及第三方前端许可见 来源与兼容记录。

接入项目必须提供私有答案存储、过期机制、会话及用途绑定、限流,以及原子化的一次性校验和凭证消费。

安装

当前已发布版本为 v1.2.0,已发布到 Packagist。直接安装:

composer require xupaoye/cangqiong-captcha:^1.2.0

检查并提交 composer.lock,部署执行 composer install。无需额外配置 VCS 仓库。GitHub 推送新版本标签后,Packagist 会自动同步。

已发布版本使用本仓库的 v1.2.0 标签;旧仓库的历史版本下载不再保证可用。已有应用须显式迁移依赖及锁文件,不能仅修改仓库地址。PHP 命名空间继续使用 Foundation\Captcha。

图形验证码

use Foundation\Captcha\ImageCaptcha;

$engine = new ImageCaptcha();
$challenge = $engine->generate('SLIDER');

// 仅将 public 返回浏览器。
$public = $challenge['public'];

// answer 仅保存在受信任的服务端存储中。
$answer = $challenge['answer'];

// 根据服务端验证过的挑战 ID、会话及用途取回答案后校验。
$passed = $engine->verify($answer, $submittedTrack);

支持以下五种服务端题型,ImageCaptcha::TYPES 列出完整集合:

类型 传入 generate() 的值
文字点选 WORD_IMAGE_CLICK
数字字母点选 ALPHANUMERIC_CLICK
图形点选 SHAPE_CLICK
滑块拼图 SLIDER
旋转图片 ROTATE

三种点选共用三次顺序点击和确定按钮,每张图为 3 个目标+3 个干扰:数字字母使用避开易混字符的双字符组合;图形使用六种不同轮廓,不要求提示图和主图颜色相同。位置随机生成,保留边距和元素间距,轻微旋转;提示只通过图片提供,不返回字符、图形标识或答案坐标。

为复用现有浏览器 SDK,三种点选的 public.type 都为 WORD_IMAGE_CLICK;私有 answer.type 保留实际题型。生成请求的类型与浏览器交互类型不能混为一谈,校验必须使用服务端保存的完整 answer。

已移除 CONCAT 图片拼接;生成会拒绝该类型,旧拼接答案也不会通过校验。升级时应停发旧题,并清理或等待旧挑战过期;旧四目标点选答案同样拒绝,需要刷新成三目标题目。

题型和容差必须由服务端决定,不能让浏览器挑选更弱的策略或提交自己的答案。

组件不生成图形挑战 ID、不提供 HTTP 端点、不认证用户,也不签发登录凭证。接入方需生成不可预测的 ID、私有保存答案,并将挑战绑定到当前会话和操作。失败尝试也必须原子作废,浏览器返回的布尔值不能当作验证凭据。

坐标与轨迹

输出图片为 600×360,支持的浏览器显示及轨迹坐标为 300×180;旋转题型横向分母固定为滑轨行程 242px,不能任意缩放坐标。

点选在 300×180 的显示坐标下,横纵各允许中心附近最多 25px 的偏差;各元素点击区域互不重叠。服务端 tolerant 按横向归一化单位保存,纵向按同样的像素半径换算,不能让客户端指定。

轨迹包含 bgImageWidth、bgImageHeight、整数毫秒 startTime / stopTime 和 trackList。每个事件包含 x、y、t、type;数值必须为有限 JSON 数字,不接受数字字符串。拖动顺序为 down、零个或多个 move、up;点选允许 click 和指针 move。总时长限制为 100–120000ms。

上述规则用于拒绝畸形输入,不证明轨迹来自真人。

滑块伪凹槽(v1.2.0)

默认随机加入 1~2 个伪凹槽;空间不足时保留一个,不堆叠重试。伪凹槽与真实目标处于同一拖动水平线,但使用另一套明显不同的轮廓,只有一个缺口与滑块形状及图像纹理匹配。所有缺口保留间距,不覆盖起始滑块或真实裁片;沿用原尺寸、透明抗锯齿和凹陷阴影,不再逐像素淡化或缩放伪凹槽。答案及坐标协议不变,伪凹槽不能通过服务端校验。

可信服务端配置 new ImageCaptcha(sliderDecoy: false) 可恢复单缺口模式。v1.1.1 及更早版本默认仍为单缺口;升级到 v1.2.0 并更新接入方锁文件后才会应用上述变化,不要直接修改 vendor。

伪凹槽可干扰只找深色区域的简单定位,但无法阻止轮廓或纹理匹配,也不能识别 AI。它不替代一次性消费、会话绑定、过期和限流。

强化验证策略(推荐)

ImageCaptcha 保留基础图像接口;ChallengePolicy 提供与业务框架无关的强化规则。应用只负责持久化、会话绑定和原子消费,不自行改写答案容差。

use Foundation\Captcha\ChallengePolicy;

// pool / previous 从当前会话的可信服务端状态读取,不接收浏览器传入值。
$selection = ChallengePolicy::nextType($pool, $previous);
$challenge = $engine->generate($selection['type']);
$answer = ChallengePolicy::prepare($challenge['answer'], (int) round(microtime(true) * 1000));
// 保存私有 answer 成功后,再保存 selection['remaining'] 与本次题型。
// 仅发送 challenge['public'] 给浏览器。
$passed = ChallengePolicy::verify($engine, $answer, $submittedTrack, (int) round(microtime(true) * 1000));

每轮覆盖五类,不连续重复;服务端发题后至少300ms,客户端报告时长不超过实际经过时长+250ms,答案120秒过期。不比较客户端绝对时钟。滑块容差约±3.6px、旋转±5.4°,点选保持原容差。升级启用策略后,未带服务端时间的在途答案需刷新。

ChallengePolicy::failureCooldownScript() 返回可用于Redis EVAL的脚本:一个带实例命名空间的会话绑定键,三个参数依次为 check/failure/success、随机唯一事件ID、空字符串(生产使用Redis TIME)。返回 [allowed, retrySeconds, failureCount]。仅对尚未消费、绑定匹配的真实答错调用failure;生成/校验前调用check,通过调用success。5分钟窗口内8次失败冷却30秒、12次冷却120秒;刷新和拒绝不延期,不连带同IP其他会话。宿主将拒绝映射为429与Retry-After,Redis故障须拒绝放行;另原子执行 TICKET_LIMIT / TICKET_WINDOW_SECONDS 成功凭证额度。这些均不替代账号/IP限流。

基础图像功能不依赖Redis;启用失败冷却才需要Redis及执行EVAL的客户端。脚本使用TIME、ZREMRANGEBYSCORE、ZCARD、ZRANGE、ZADD、PEXPIRE,键闲置自动过期。第三个参数的时钟覆盖只供受控测试,不能暴露为请求参数。计时与轨迹规则只能增加脚本成本,不能识别AI身份。

自定义素材

$engine = new ImageCaptcha(
    backgroundDirectory: '/trusted/project/captcha-backgrounds',
    minimumCropUnits: ['portrait' => 138],
);

背景目录直接存放 720×432 的 .jpg 文件,解码前检查类型和尺寸。裁切配置键为不含扩展名的文件名,值为 120–144 的整数;裁切宽高分别为 unit × 5、unit × 3。默认 120,138 可保留较完整构图,144 不作空间裁切。目录缺失或为空时拒绝生成,不静默回退。

也可用 new ImageCaptcha('/trusted/absolute/resources') 替换包含 templates/、fonts/、backgrounds/ 的完整资源目录。素材路径属于受信任的服务端配置,不能直接使用上传目录或请求参数。

内置两套原创拼图模板,重绘方法见 模板说明。

内置 21 张 AI 生成背景,包含人物、卡通与层叠山脉等;素材说明见 背景说明。

POW 核心

use Foundation\Captcha\PowCaptcha;

$pow = new PowCaptcha();
$challenge = $pow->generate(); // 难度 4、盐长度 16、解数量 4
$passed = $pow->verify($challenge['answer'], ['data' => $nonceString]);

track.data 使用 - 连接十进制 nonce。服务端只校验,不执行昂贵求解。难度按十六进制位数计,范围 1–6;盐长度 1–64,解数量 1–16。nonce 必须为规范的无符号 JavaScript 安全整数,拒绝带符号、引号、小数及指数形式。

POW 证明计算成本,不能证明真人,也不能由客户端自行用来替代交互验证。本组件不提供 POW HTTP 端点或完整浏览器 Worker 生命周期。

开发与验证

composer install
composer validate --strict
composer resources:check
composer test
composer audit

单独执行固定基线回归(已包含在 composer test 中):

composer parity

开发和测试需要 PHP、所需扩展和 Composer。自动化测试覆盖图像生成、答案校验、输入边界及固定基线回归;测试数据带 SHA256 校验,维护规则见 基线说明。

核心测试不能代替接入方的 HTTP、数据库、并发、浏览器和部署验收;本地小样本图像匹配测试已确认滑块可被自动解题,详见 安全策略;尚无生产环境、OCR 或完整多题型攻防评估。

安全与维护

PHP 核心由本项目独立维护,按需审查和吸收第三方修复,不自动同步外部代码。见 安全策略 和 更新记录。

验证码只是登录防护的一环,不能替代密码认证、会话与账户/IP 限流,也不等于商业风控引擎。漏洞请通过 GitHub Security → Report a vulnerability 私密报告,不在公开 issue 中提交生产答案、密钥或可利用细节。

许可证与来源

代码及拼图模板采用 Apache-2.0,第三方声明见 NOTICE。

字体为重新命名的 Noto Sans SC 子集,单独遵循 SIL OFL 1.1。背景图的授权范围以各自素材说明为准。