image-validation / rotate-captcha
旋转扶正验证 SDK,用于发送验证码前的人机校验
Package info
gitee.com/changrui_com/image-validation.git
pkg:composer/image-validation/rotate-captcha
Requires
- php: >=8.1
- ext-gd: *
- ext-json: *
Requires (Dev)
None
Suggests
- ext-redis: redis.client 为 phpredis 时需要
- predis/predis: redis.client 为 predis 时需要
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-24 06:38:24 UTC
README
发送验证码之前,让用户把一张被旋转过的图片扶正。这个包是 Composer SDK:出图、校验角度、签发一次性通行证。HTTP 接口和短信发送写在业务项目里。
图片字段是完整的 data URI,可以直接作为 img / image 的 src:
data:image/png;base64,iVBORw0KGgo...
安装
composer require image-validation/rotate-captcha
需要 PHP 8.1+、ext-gd、ext-json。Redis 使用本机已安装的 phpredis 或 predis,在配置里用 redis.client 选择。
默认原图目录是包内 captcha/in/image,里面只放 jpg、png、webp,并且保持正放。
配置
Hyperf:
php bin/hyperf.php vendor:publish image-validation/rotate-captcha
生成 config/autoload/rotate_captcha.php。
Webman:
mkdir -p config/plugin/image-validation/rotate-captcha
cp vendor/image-validation/rotate-captcha/publish/rotate_captcha.php config/plugin/image-validation/rotate-captcha/rotate_captcha.php
PHP-FPM:
mkdir -p config
cp vendor/image-validation/rotate-captcha/publish/rotate_captcha.php config/rotate_captcha.php
FPM 使用的 php.ini 要启用 gd。redis.client 为 phpredis 时还要启用 redis 扩展。可以用 FPM 对应的 php -m 确认,它和命令行 PHP 不一定是同一份配置。
业务侧要改的主要是这三项:
'redis' => [
'client' => 'phpredis', // phpredis 或 predis
'host' => '127.0.0.1',
'port' => 6379,
'username' => '',
'password' => '',
'database' => 0,
'timeout' => 2.0,
'prefix' => 'rotate_captcha:',
],
// 出图时顺时针旋转的度数,每次在闭区间内随机取一个角度
'rotate_min' => 30,
'rotate_max' => 330,
// 扶正后与正确角度相差在这个度数以内算通过
'verify_degrees' => 10,
rotate_min 与 rotate_max 写成同一个数时,每次都转这个固定度数。image_dir 为 null 时使用包内 captcha/in/image。
调用
use ImageValidation\RotateCaptcha\Exception\PassException;
use ImageValidation\RotateCaptcha\Exception\VerifyException;
$result = $captcha->create('sms', $phone);
// $result['token']
// $result['image']
try {
$passed = $captcha->verify($token, (float) $angle);
// $passed['pass_token']
// $passed['expires_in']
} catch (VerifyException $e) {
// 未通过。$e->remaining() 为剩余次数,0 表示需要重新 create
}
try {
$captcha->assertPass($passToken, 'sms', $phone);
// 通过后再发送验证码
} catch (PassException $e) {
// 通行证无效、已使用,或与场景、手机号不一致
}
用户提交的 angle 是为了扶正而顺时针转过的角度。同一张图默认最多校验 3 次。失败结果里只有剩余次数,没有角度差。通行证默认 60 秒,使用一次后失效,并且绑定 scene 和手机号或邮箱。
Hyperf
发布配置后,容器里可以直接注入 ImageValidation\RotateCaptcha\RotateCaptcha。
Webman
use ImageValidation\RotateCaptcha\Webman\RotateCaptchaProvider;
$captcha = RotateCaptchaProvider::make();
make() 读取 config/plugin/image-validation/rotate-captcha/rotate_captcha.php。也可以传入配置数组。
PHP-FPM
在站点入口引入 Composer 自动加载,用配置文件创建实例。每个请求新建一次即可,不需要常驻进程。
<?php
use ImageValidation\RotateCaptcha\Exception\PassException;
use ImageValidation\RotateCaptcha\Exception\VerifyException;
use ImageValidation\RotateCaptcha\RotateCaptcha;
require __DIR__ . '/vendor/autoload.php';
$captcha = RotateCaptcha::fromConfigFile(__DIR__ . '/config/rotate_captcha.php');
$payload = json_decode(file_get_contents('php://input') ?: '', true);
if (!is_array($payload)) {
$payload = $_POST;
}
header('Content-Type: application/json; charset=utf-8');
$action = $_GET['action'] ?? '';
if ($action === 'create') {
echo json_encode($captcha->create((string) $payload['scene'], (string) $payload['target']));
return;
}
if ($action === 'verify') {
try {
echo json_encode($captcha->verify((string) $payload['token'], (float) $payload['angle']));
} catch (VerifyException $e) {
http_response_code(422);
echo json_encode(['message' => $e->getMessage(), 'remaining' => $e->remaining()]);
}
return;
}
if ($action === 'send') {
try {
$captcha->assertPass((string) $payload['pass_token'], 'sms', (string) $payload['phone']);
// 通过后再调用原来的短信发送
echo json_encode(['ok' => true]);
} catch (PassException $e) {
http_response_code(422);
echo json_encode(['message' => $e->getMessage()]);
}
}
Nginx 把该文件交给 php-fpm 即可,例如 /captcha.php?action=create。路由仍由业务自己定,SDK 不注册接口。
前端示例
滑块松手再提交角度。拖动过程中只用 CSS 旋转预览。
配合前端 front-image-validation/rotate-captcha 使用的前端组件。弹窗里拖动左侧滑块,把圆形图片转正。松手后才提交角度。手机和 PC 都能拖。
组件不发请求地址,由业务传入 create 和 verify。验证通过后抛出 passToken,页面再去发验证码。
安装
npm install front-image-validation
Vue 3 项目需要已安装 vue@3。
Vue 3
<script setup>
import { ref } from 'vue'
import RotateCaptcha from 'front-image-validation/vue'
const open = ref(false)
const phone = ref('')
async function create(scene, target) {
const data = await fetch('/your/captcha/create', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ scene, target }),
}).then((response) => response.json())
return data
}
async function verify(token, angle) {
const response = await fetch('/your/captcha/verify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ token, angle }),
})
const data = await response.json()
if (!response.ok) {
const error = new Error(data.message || '验证未通过')
error.remaining = data.remaining
throw error
}
return data
}
function onSuccess(passToken) {
open.value = false
// 带着 passToken 去发送验证码
}
</script>
<template>
<button type="button" @click="open = true">获取验证码</button>
<RotateCaptcha
v-model:open="open"
scene="sms"
:target="phone"
:create="create"
:verify="verify"
@success="onSuccess"
>
<template #feedback>
<a href="/feedback">意见反馈</a>
</template>
</RotateCaptcha>
</template>
create 返回 { token, image },image 是后端给出的 data:image/png;base64,...。verify 成功返回 { pass_token, expires_in }。失败时抛出带 remaining 的错误。剩余次数为 0,或消息里包含「失效」时,组件会自动重新拉图。
uni-app
使用 Vue 3。页面里:
<script setup>
import { ref } from 'vue'
import RotateCaptcha from 'front-image-validation/uni'
const open = ref(false)
const phone = ref('')
function create(scene, target) {
return new Promise((resolve, reject) => {
uni.request({
url: '/your/captcha/create',
method: 'POST',
data: { scene, target },
success: (res) => resolve(res.data),
fail: reject,
})
})
}
function verify(token, angle) {
return new Promise((resolve, reject) => {
uni.request({
url: '/your/captcha/verify',
method: 'POST',
data: { token, angle },
success(res) {
if (res.statusCode >= 400) {
const error = new Error(res.data.message || '验证未通过')
error.remaining = res.data.remaining
reject(error)
return
}
resolve(res.data)
},
fail: reject,
})
})
}
</script>
<template>
<button @click="open = true">获取验证码</button>
<RotateCaptcha
v-model:open="open"
scene="sms"
:target="phone"
:create="create"
:verify="verify"
@success="onSuccess"
/>
</template>
小程序里用手指拖滑块。H5 上鼠标和触摸都可以。意见反馈用同名插槽 feedback,不传就不显示。
属性
| 属性 | 说明 |
|---|---|
open / v-model:open | 是否显示弹窗 |
scene | 场景,例如 sms |
target | 手机号或邮箱 |
create | (scene, target) => Promise<{ token, image }> |
verify | (token, angle) => Promise<{ pass_token, expires_in }> |
title | 默认「安全验证」 |
subtitle | 默认「请完成下方验证后继续操作」 |
hint | 默认「拖动左侧滑块使图片为正」 |
滑块最左是 0°,最右是 360°,提交的是顺时针角度。点遮罩不会关闭,点右上角关闭。