Search by

image-validation / rotate-captcha

hechangrui

旋转扶正验证 SDK,用于发送验证码前的人机校验

Package info

gitee.com/changrui_com/image-validation.git

Homepage

pkg:composer/image-validation/rotate-captcha

Statistics

Installs: 3

Dependents: 0

Suggesters: 0

v1.0.2 2026-09-24 06:35 UTC

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°,提交的是顺时针角度。点遮罩不会关闭,点右上角关闭。