这不是一个简单的验证码,而是一套完整的人机识别方案。支持 AI 行为分析、Token 一次性绑定、图片动态水印、IP 频率限制,以及一行代码的极简接入。

项目预览


写在前面
我运营一个 WordPress 博客,流量不大,但一直有个头疼的问题:爬虫和采集器太多了。有些是搜索引擎的合法抓取,但更多的是盗站、采集、甚至 CC 攻击的脚本。
最开始我用了 Google reCAPTCHA,但国内访问太慢。后来试过极验,收费不便宜。也想过自己写一个简单的图片验证码,但那只能防最基础的脚本,稍微懂点技术的就能绕过去。
于是我决定自己写一个。
从最初的简单图片点选,到后来加入算术题、行为采集,再到这次 v4.0 完全重构,yuyu WebSafe 已经变成一个真正能对抗自动化脚本和爬虫的人机验证服务。最重要的是,它完全免费,任何网站都可以用一行代码接入。
这篇文章会详细介绍 v4.0 的所有更新,以及如何接入。如果你也在被机器人困扰,希望能给你一些参考。
一、旧版本的问题
在开始介绍 v4.0 之前,先说说旧版本有哪些让我不满意的地方:
- Token 可以重复使用
旧版本生成 token 后没有做防重放处理。这意味着如果 token 在传输过程中被截获(比如通过不安全的网络),攻击者可以拿着这个 token 反复使用。更糟糕的是,同一个 token 可以在不同 IP 下使用,完全无法追溯。
- 图片直接暴露路径
所有角色图片都直接放在 images/ 目录下,路径是公开的。攻击者可以写个脚本把所有图片下载下来,然后建立本地特征库。这样一来,角色验证就形同虚设了——机器可以直接比对本地图片,根本不需要识别。
- 算术题答案在前端生成
这是最离谱的问题。之前为了省事,算术题的题目和答案都在前端生成,然后通过 POST 提交给后端验证。稍微懂点 JavaScript 的人,打开控制台就能看到正确答案,直接绕过验证。
- 没有 IP 绑定和频率限制
同一个 IP 可以在短时间内发起无数次验证请求,没有任何限制。这意味着攻击者可以暴力尝试,或者通过大量请求消耗服务器资源。
这些问题堆积在一起,让我意识到:如果只是“能用”,那这个系统其实跟没有一样。v4.0 的目标很明确——让验证真正“防得住”。
二、v4.0 整体架构
在开始写代码之前,我先把整体架构理清楚了。
┌─────────────────────────────────────────────────────────────┐
│ 接入方网站 │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ <script src="https://ovo.yuyu09.com/verify-client.js"> │
│ └─────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────┐
│ 验证服务 (ovo.yuyu09.com) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ index.html │ 验证页面 (Vue 3 + Tailwind) │ │
│ │ api.php │ 后端核心 (出题/验证/频率限制) │ │
│ │ safe.php │ 安全防护 (防直接请求) │ │
│ │ yuyu.js │ 静态站防御脚本 │ │
│ │ verify-client.js│ 通用接入脚本 │ │
│ │ how-to.html │ 接入指南页面 │ │
│ │ images/ │ 角色图片 (动态加水印) │ │
│ │ data/ │ 运行时数据 (Token记录/频率计数) │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
整个系统分为三层:
· 接入层:第三方网站通过引入一个 JS 文件即可接入
· 验证层:独立的验证服务,提供人机验证界面和 API
· 数据层:存储 Token、频率计数、域名白名单等运行时数据
三、安全加固详解
3.1 Token 一次性 + IP 绑定
这是 v4.0 最重要的安全改进。
Token 结构(JWT):
{
"iss": "yuyu",
"iat": 1740965310,
"exp": 1741051710,
"jti": "a006630316eace36b35d2d359fd3d356",
"ip": "f885cbd1c3cc7ce219683c9615046d5691bf5cf877af58fe357460a2140003bb",
"ua": "bb752c50a00839b3879be37474c39b38"
}
每个字段都有具体作用:
· iss:签发者标识
· iat:签发时间
· exp:过期时间(1小时后自动失效)
· jti:JWT ID,唯一标识,用于防重放
· ip:用户 IP 的 SHA-256 哈希值
· ua:User-Agent 的 MD5 哈希值
验证流程:
- 用户访问接入网站,被跳转到验证页
- 验证页调用 api.php?action=question 获取题目
- 用户完成验证后,调用 api.php?action=verify 提交答案
- 验证通过后,生成包含 IP 和 UA 哈希的 JWT Token
- Token 返回给前端,并携带跳转回原网站
- 原网站的 verify-client.js 收到 Token 后设置 Cookie
- Token 被记录到 data/jti.json,同一 Token 再次使用会被拒绝
为什么这样做:
· Token 绑定 IP,即使被截获也无法在其他网络使用
· Token 绑定 UA,即使在同一 IP 下更换浏览器也无法使用
· 一次性机制确保每个 Token 只能用一次,防止重放攻击
· 1小时过期时间,避免 Token 长期有效带来的风险
3.2 图片动态水印
这是针对 AI 图像识别的防御手段。
旧版本的图片直接暴露路径,攻击者可以写脚本把所有图片下载下来,建立特征库。v4.0 的图片不再直接访问,而是通过 api.php?action=img&id=xxx 动态输出。
每次输出时,会通过 GD 库在图片上叠加:
· 50 个随机噪点像素:位置和颜色随机
· 4 条半透明线条:位置、角度、透明度随机
// 添加随机噪点
for ($i = 0; $i < 50; $i++) {
$x = rand(0, $width);
$y = rand(0, $height);
$color = imagecolorallocate($img, rand(0, 255), rand(0, 255), rand(0, 255));
imagesetpixel($img, $x, $y, $color);
}
// 添加随机线条
for ($i = 0; $i < 4; $i++) {
$color = imagecolorallocatealpha($img, rand(0, 255), rand(0, 255), rand(0, 255), rand(0, 50));
imageline($img, rand(0, $width), rand(0, $height), rand(0, $width), rand(0, $height), $color);
}
效果:
· 水印位置和透明度随机,人眼看不出区别
· AI 识别时,这些噪点和线条会被当成图像特征的一部分
· 同一张图片每次请求的水印都不同,AI 无法建立稳定的特征库
3.3 行为分析
这是区分真人和脚本的核心。
前端 yuyu.js 会持续采集用户行为数据:
· 鼠标移动轨迹(坐标、时间戳)
· 点击位置和时机
· 键盘敲击频率
· 触摸屏操作(移动端)
· 页面滚动行为
提交验证时,这些数据会一起发送到后端:
// 行为数据采集示例
const behavior = {
startTime: Date.now() - pageLoadTime, // 从页面加载到开始验证的时间
reactionMs: verifyClickTime - startTime, // 点击验证按钮的反应时间
mousePoints: trajectory.points.length, // 鼠标轨迹点数量
mouseDist: trajectory.totalDistance, // 鼠标移动总距离
webdriver: navigator.webdriver, // 是否是无头浏览器
touchEvents: touchCount, // 触摸事件次数
viewportWidth: window.innerWidth,
viewportHeight: window.innerHeight
};
服务端逻辑:
检测项 阈值 判定
反应时间 < 300ms 脚本(真人不可能这么快)
鼠标轨迹点 < 3 个(桌面端) 脚本(真人移动一定有轨迹)
鼠标移动距离 < 5px(桌面端) 脚本(真人不会原地点击)
navigator.webdriver true 脚本(无头浏览器特征)
移动端触摸事件 0 次 脚本(移动端必定有触摸)
为什么这样设计:
· 真人的反应时间通常在 300ms 以上
· 真人点击前鼠标一定会移动,会产生轨迹点
· 无头浏览器(如 Puppeteer)会暴露 webdriver 属性
· 移动端用户必然有触摸操作
3.4 IP + Session 双重频率限制
这是为了防止暴力请求和 CC 攻击。
频率限制分为两个维度:
Session 维度(基于 PHP Session):
· 每分钟最多 60 次请求
· 超限后返回 429 状态码
IP 维度(基于文件缓存):
· 每分钟最多 30 次请求
· 超限后返回429 状态码
// 频率限制核心逻辑
function rateLimit($key, $limit, $window) {
$file = DATA_PATH . "/ip_{$key}.json";
$now = time();
$data = file_exists($file) ? json_decode(file_get_contents($file), true) : [];
// 清理过期记录
$data = array_filter($data, function($ts) use ($now, $window) {
return $ts > $now - $window;
});
// 检查是否超限
if (count($data) >= $limit) {
return false;
}
// 记录本次请求
$data[] = $now;
file_put_contents($file, json_encode($data));
return true;
}
两个维度独立计算,触发任意一个都会拒绝请求,并返回剩余锁定时间。
3.5 CSP 安全头
所有 JSON 接口响应都会添加:
Content-Security-Policy: default-src 'none'
这个头部的作用是防止 XSS 攻击。即使攻击者找到了注入点,也无法通过接口响应执行恶意脚本。
四、UI 重构:Vue 3 + Tailwind CSS
旧版本的 UI 是用原生 JS 写的,虽然能用,但代码维护起来很痛苦。v4.0 完全重写了前端,采用了现代技术栈。
技术选型
· Vue 3:响应式状态管理,代码更清晰
· Tailwind CSS:原子化样式,不用写一行 CSS
· Lucide Icons:统一的 SVG 图标库
页面结构
验证页有两个模式,通过 Tab 切换:
角色验证模式:
· 2×2 图片网格
· 题目显示需要选择的角色名
· 点击图片即选中,再次点击可切换
· 选中后显示绿色勾选标记
算术验证模式:
· 支持加法、减法、乘法
· 数字精确到一位小数(如 3.2、7.5)
· 答案精确到两位小数
· 干扰选项在正确答案附近随机分布
· 允许 ±0.015 的误差(防止浮点精度问题)
验证流程
- 页面加载时,调用 api.php?action=question 获取题目
- 根据当前模式渲染对应的 UI
- 用户选择答案后,点击「验证身份」
- 前端收集行为数据,连同答案一起 POST 到 api.php?action=verify
- 服务端校验:
· 检查行为数据是否异常
· 检查频率限制
· 验证答案是否正确
· 记录失败次数,超限则锁定
- 验证通过后,生成 Token 并跳转回原网站
失败处理
· 验证失败时显示剩余尝试次数
· 失败 5 次后锁定 15 分钟
· 锁定时显示倒计时提示
五、接入方式(完整版)
这是 v4.0 最重要的改进之一:接入方式从“需要修改多处配置”变成了“一行代码”。
5.1 核心脚本
接入只需要在网站底部添加一行代码:
<script src="https://ovo.yuyu09.com/verify-client.js"></script>
这个脚本会自动处理:
· 检查用户是否已验证(通过 Cookie 或 localStorage)
· 未验证则跳转到验证页,并保存当前页面地址
· 验证成功后设置 Cookie 并自动刷新
5.2 分平台接入说明
不同平台的添加位置略有不同,下面是详细指南:
WordPress
推荐方法:安装「Insert Headers and Footers」插件,在「页脚脚本」里粘贴代码。
手动方法:编辑主题文件 footer.php,在











评论(0)
暂无评论