Rotate image captcha,旋转图片验证码

Rotate captcha

旋转图片角度验证码, 使用 PHP 生成验证图片(gd 或者 imagick) 用于旋转验证,可用于各种框架

前端已经支持原生JSjqueryvue2uniapp版本, 持续更新, 可放心使用

暂未实现react版,有能力的朋友参考现有版自行实现下哦
已知uniapp打包微信小程序(IOS有卡顿bug希望有能力的可以修复下,我没有设备无法复现问题)

若发现bug, 或更好的建议, 还请issue反馈

更新

2021-09-10 新增

新增原生JS版本, 优化部分代码

2021-09-16 新增

增加存储驱动功能可使用session,cache,cookie驱动 验证方式改为token交换,利于vue,react,app等调用 加密方式更改为AES

2021-09-17 新增

新增输出格式设置,可设置webp,生成图片更小,清晰度更高且支持透明底色

2021-09-19 更新

移除thinkphp6的依赖,可在其他框架增加少量代码使用啦

2021-09-20 更新

token存储增加了前缀 新增Redis存储驱动,不依赖框架,支持redis即可

2021-09-22 更新

新增uniapp版,暂时兼容PC版有BUG

2021-09-23 更新

新增vue版,基于vue2,未测试vue3

2021-09-24 更新

修复uniapp小程序安卓真机卡顿问题(ios貌似还是有问题, 因为没设备测试, 暂时无法修复- -...)

2021-09-25 更新

vue版增加了touch事件的支持, 兼容h5

2021-09-26 更新

vue版改为canvas

2021-10-07 更新

修复Imagick方式旋转角度问题 修复旧的存储方式逻辑bug,隔月无法找到相同角度图片 新增图片存储开关,存储后,生成相同角度图片时,可以二次找回,无需再次生成 启用存储生成图片时,可以设置存储图片深度,storeImage设置true1时存储为角度文件夹,设置2时根据角度生成2个文件夹,大于2时生成3个文件夹 未启用存储生成图片时,每次图片访问后会清理存储图片的目录内所有文件,删除当前访问生成验证码图片

2021-10-20 更新

将语言改到为配置项

2022-01-05 更新

增加facade注释 移除助手类的rotate_captcha_img方法使用rotate_captcha_output代替,用法和\isszz\captcha\rotate\facade\Captcha::output方法相同,返回数组[$mime, $image],生成图片的mime类型和图片内容

2022-09-12 更新

增加非TP6验证说明 修复原生JS事件处理问题(感谢 笨笨天才 的issue) 修改说明中X-CaptchaToken大写linux拿不到的问题, 应该拿的时候用X-Captchatoken(感谢 笨笨天才 的issue) 安装

composer require isszz/rotate-captcha -vvv 演示图

Ctrl+鼠标左键, 查看演示视频

配置说明

<?php return [ 'lang' => 'zh-cn', // 默认语言 'size' => 350, // 生成图片尺寸 'expire' => 300, // 生成验证有效期 'salt' => '%%*$*$#$~#$^isszz@cfyun.cc^&*$#$~', 'outputType' => 'webp', // 输出类型, png, jpg, webp, 建议使用webp, png文件较大, jpg不支持透明 'storeImage' => true, // 是否存储生成的图片, 如果保存, 也可以设置存储深度, true或1是角度文件夹, 2根据角度生成2个文件夹, 大于2则根据角度生成3个文件夹 'handle' => 'gd', // 服务器支持imagick时, 建议使用imagick 'earea' => 10, // 容错率 'gd' => [ 'quality' => 80, 'bgcolor' => '', // 底色, #fff ], 'imagick' => [ 'quality' => 80, 'bgcolor' => '', // 底色, white ], // Redis存储驱动需要的配置 'redis' => [ 'host' => '127.0.0.1', 'port' => 6379, 'password' => '', 'select' => 0, 'timeout' => 0, 'expire' => 0, 'persistent' => false, 'prefix' => 'captcha_', ], // token存储驱动,默认为thinkphp6,需要其他的可以参考下面实现 // 'store' => isszz\captcha\rotate\store\CacheStore::class, // cache token存储驱动,基于thinkphp6 // 'store' => isszz\captcha\rotate\store\CookieStore::class, // cookie token存储驱动,基于thinkphp6 // 'store' => isszz\captcha\rotate\store\SessionStore::class, // session token存储驱动,基于thinkphp6 'store' => isszz\captcha\rotate\store\RedisStore::class, // redis token存储驱动,需支持redis,不依赖框架 ]; PHP部分使用

tp6中使用

<?php declare (strict_types = 1); namespace app\common\controller; use isszz\captcha\rotate\CaptchaException; use isszz\captcha\rotate\facade\Captcha as RotateCaptcha; use isszz\captcha\rotate\support\File; use think\Response; use think\Request; class Captcha { /** * 生成验证码图片和相关信息 */ public function rotate(Request $request) { // 用于测试, 这部分, 可以自己整个素材库, 去数据库, 或者缓存下来总之很灵活 /*$list = [ '1.png', '2.png', '1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg', '6.jpg', '7.jpg', '8.jpg', '9.jpg', '10.jpg', '11.jpg', '12.jpg', '13.jpg', ]; // upload_path 需要自己写一个 // 随机拿一个图片 $key = array_rand($list, 1); if(isset($list[$key])) { // 从素材存放目录拿一个图 $image = upload_path('captcha_mtl') . $list[$key]; }*/ // 说明: upload_path方法为自定义方法,更具自己使用的框架获取你存储素材的根目录即可 // 例如在tp6框架可以这么写: /* function upload_path(string $path = ''): string { return public_path(DIRECTORY_SEPARATOR .'uploads' . DIRECTORY_SEPARATOR . ($path ? ltrim($path, DIRECTORY_SEPARATOR) : $path)); } */ // 新增: 从指定目录随机读取文件,这个方法不知道效率如何,基于FilesystemIterator类实现 $image = File::make(upload_path('captcha_mtl'))->rand(); // 生成验证码需要的图片 // setLang设置语言 // $rotateCaptcha = RotateCaptcha::setLang('zh-cn'); // $rotateCaptcha->create(...); $data = RotateCaptcha::create( $image, upload_path('captcha') // 用于存储生成图片的目录 )->get(260); // 260为最终生成的图片尺寸 if(!$data) { $this->result(1, 'error'); } // $data['str']是图片的path加密, 用于前端交换验证码图片 // 这里前端不涉及拿到角度, 都是去后端验证 // 可以使用header传递token为X-Captchatoken $this->result(0, 'success', ['str' => $data['str']], ['X-Captchatoken' => $data['token']]); } /** * 验证 */ public function verify(Request $request) { $angle = $request->get('angle'); // 优先从header获取token $token = $request->header('X-Captchatoken') ?: $request->get('token'); if(empty($token) || empty($angle)) { $this->result(1, 'error'); } try { if(RotateCaptcha::check($token, $angle) === true) { $this->result(0, 'success'); } } catch(CaptchaException $e) { $this->result(1, $e->getMessage()); } $this->result(1, 'error'); } /** * 通过前端传递str字段给后端叫唤图片显示到前端 */ public function img(Request $request) { $str = $request->get('id'); [$mime, $image] = RotateCaptcha::output($str, upload_path('captcha')); if(empty($image)) { return ''; } return response($image, 200, [ 'Cache-Control' => 'private, no-cache, no-store, must-revalidate', 'Content-Type' => $mime, 'Content-Length' => strlen($image) ]); } /** * 返回json */ public function result(int|string $code = 0, string $msg = 'success', array|string $data, array $header = []) { $result = [ 'code' => $code, 'data' => $data, 'msg' => lang($msg) ?: ($code > 0 ? 'error' : 'success'), ]; $response = Response::create($result, 'json')->code(200); if(!empty($header)) { $response = $response->header($header); } throw new \think\exception\HttpResponseException($response); } } 非thinkphp6框架, 可以参考如下

<?php use isszz\captcha\rotate\support\File; use isszz\captcha\rotate\facade\Captcha; use isszz\captcha\rotate\CaptchaException; use think\facade\Config; // 这里用到的Config用自己框架的配置类 class CaptchaConfig extends \isszz\captcha\rotate\Config { public function get(string $name, string $defaultValue = null): mixed { return Config::get($name, $defaultValue); } public function put(string $name, array|string $data): bool { return Config::set($name, $data); } public function forget(string $name): bool { return Config::delete($name); } } /* $list = [ '1.png', '2.png', '1.jpg', '2.jpg', '3.jpg', '4.jpg', '5.jpg', '6.jpg', '7.jpg', '8.jpg', '9.jpg', '10.jpg', '11.jpg', '12.jpg', '13.jpg', ]; $key = array_rand($list, 1); if(isset($list[$key])) { $image = $list[$key]; } $image = path('upload') . 'captcha_mtl' . DIRECTORY_SEPARATOR . $image; */ // 新增: 从指定目录随机读取文件,这个方法不知道效率如何,基于FilesystemIterator类实现 $image = File::make(path('upload') . 'captcha_mtl' . DIRECTORY_SEPARATOR)->rand(); $data = Captcha::configDrive(\CaptchaConfig::class)->create($image, path('upload') . 'captcha' . DIRECTORY_SEPARATOR)->get(260); header('Content-Type:application/json; charset=utf-8'); if($data) { // 可以使用header传递token header('X-CaptchaToken: '. $data['token']); echo json_encode([ 'code' => 0, 'msg' => 'success', 'data' => [/*'token' => $data['token'], */'str' => $data['str']], ]); } echo json_encode([ 'code' => 1, 'msg' => 'error', 'data' => null, ]); 非thinkphp6框架,验证

<?php use isszz\captcha\rotate\support\request\Request; use isszz\captcha\rotate\CaptchaException; use isszz\captcha\rotate\facade\Captcha; // 下面Config配置类自行实现 class CaptchaConfig extends \isszz\captcha\rotate\Config { public function get(string $name, string $defaultValue = null): mixed { return \Config::get($name, $defaultValue); } public function put(string $name, array|string $data): bool { return \Config::put($name, $data); } public function forget(string $name): bool { return \Config::forget($name); } } $angle = $_GET['angle'] ?? ''; // 优先从header获取token $token = Request::header('X-Captchatoken') ?: $_GET['token'] ?? ''; if(empty($token) || empty($angle)) { exit(json_encode(['code' => 1, 'msg' => 'error'])); } try { if(Captcha::configDrive(\CaptchaConfig::class)->setLang('zh-cn')->check($token, $angle) === true) { exit(json_encode(['code' => 0, 'msg' => 'success'])); } } catch(CaptchaException $e) { exit(json_encode(['code' => 1, 'msg' => $e->getMessage()])); } exit(json_encode(['code' => 1, 'msg' => 'error'])); 非thinkphp6框架,输出图片

<?php use isszz\captcha\rotate\facade\Captcha; $id = $_GET['id'] ?? null; if(empty($id)) { exit(''); } [$mime, $image] = Captcha::output($id, upload_path('captcha')); if(empty($image)) { exit(''); } header('Cache-Control: private, no-cache, no-store, must-revalidate'); // header('Content-Disposition: inline; filename=captcha_' . $id . '.' . str_replace('image/', '.', $mime)); header('Content-type: '. $mime); header('Content-Length: '. strlen($image)); echo $image; 关于配置驱动和自定义存储驱动说明

use isszz\captcha\rotate\Store; use isszz\captcha\rotate\Config; // 配置获取驱动,需要基于\isszz\captcha\rotate\Config实现如下方法: class CaptchaConfig extends Config { public function get(string $name, string $defaultValue = null): mixed { // 获取配置 return Config::get($name, $defaultValue); } public function put(string $name, array|string $data): bool { // 存储配置 - 暂时无用 return Config::put($name, $data); } public function forget(string $name): bool { // 删除配置 - 暂时无用 return Config::forget($name); } } // 自定义存储驱动,需要基于\isszz\captcha\rotate\Store实现如下方法: // 更多方式参考\isszz\captcha\rotate\store\文件夹内示例,只要能存储token怎么存自由发挥哈 // 这里大家只需要实现, 验证token是否存在(当然此处可以省略,获取后判断也是一样), 获取token, 和删除token, 存储token class CaptchaSessionStore extends Store { public function get(string $token): array { // 检测token是否存在 if(!Session::has($token)) { return []; } // 获取token内容 $payload = Session::get($token); if(empty($payload)) { return []; } // 解析token内容 $payload = $this->encrypter->decrypt($payload); if(empty($payload)) { return []; } // 删除token Session::forget($token); // 返回解析后的token return json_decode($payload, true); } public function put(?int $degrees): string { [$token, $payload] = $this->buildPayload($degrees); // 存储token, 并设置token过期时间ttl Session::put($token, $payload, $this->ttl); return $token; } } 前端配置项

options = { theme: '#07f', // 验证码主色调 title: '安全验证', desc: '拖动滑块,使图片角度为正', width: 305, // 验证界面的宽度 successClose: 1500, // 验证成功后页面关闭时间 timerProgressBar: !0, // 验证成功后关闭时是否显示进度条 timerProgressBarColor: '#07f', // 进度条颜色 url: { info: '/captcha', // 获取验证码信息 check: '/captcha/check', // 验证 img: '/captcha/img', // 交换图片 }, init: function (captcha) {}, // 初始化 success: function () {}, // 验证成功 fail: function () {}, // 验证失败 complete: function (state) {}, // 触发验证, 不管成功与否 close: function (state) {}, // 关闭验证码窗口并返回验证状态state }; 前端使用 原生JS版本的使用方式

// .J__captcha__是输出验证码的容器 // 方式1 let myCaptcha = document.querySelectorAll('.J__captcha__').item(0).captcha({ // 验证成功时显示 timerProgressBar: !0, // 是否启用进度条 timerProgressBarColor: '#07f', // 进度条颜色 url: { create: '/common/captcha/rotate', // 获取验证码信息 check: '/common/captcha/verify', // 验证 img: '/common/captcha/img', // 交换旋转图 }, // 初始化回调 init: function (captcha) { // console.log(captcha); }, // 验证成回调 success: function() { console.log('Captcha state:success'); }, // 验证失败回调 fail: function() { console.log('Captcha state:fail'); }, // 触发验证回调, 不管成功与否 complete: function(state) { console.log('Captcha complete, state:', state); }, // 验证码触发关闭(验证成功后需要关闭) close: function(state) { modal.close(); // 关闭你的modal console.log('Captcha close, state:', state); } }); // 方式2 let myCaptcha = new Captcha(document.querySelectorAll('.J__captcha__').item(0), { // options同上... }); jquery版本的使用方式

// .J__captcha__是输出验证码的容器 modal.element.find('.J__captcha__').captcha({ // options同上... }); // 获取Captcha实例 // var captcha = modal.element.find('.J__captcha__').data('captcha'); // console.log(captcha.state()); vue使用示例

<template> <div id="app"> <div @click="openCaptcha"> <img alt="Vue logo" src="./assets/logo.png"> <div>打开验证码</div> </div> <RoutateCaptcha :options="captchaOptions" v-if="captchaShow" @close="captchaShow = false" @init="captchaInit" @complete="captchaComplete" @success="captchaSuccess" @fail="captchaFail"></RoutateCaptcha> </div> </template> <script> // 组件路径, 在这里引入. 组件位置:js/vue/RoutateCaptcha import RoutateCaptcha from './components/RoutateCaptcha/index.vue' export default { name: 'captchaDemo', components: { RoutateCaptcha }, data() { return { captchaShow: false, captchaOptions: { theme: '#07f', title: '安全验证', desc: '拖动滑块,使图片角度为正', successClose: 1500, // 验证成功后页面关闭时间 timerProgressBar: true, // 验证成功后关闭时是否显示进度条 timerProgressBarColor: 'rgba(0, 0, 0, 0.2)', request: { // 获取验证码信息 info: (callback) => { this.getJSON('http://vmd.co/common/captcha/rotate', null, function(res, xhr) { if(xhr.status != 200) { alert('系统出错:'+ res.statusCode +',请关闭重试!') return false } // 第二个参数传递从header中获取的token, 如果嫌麻烦, 可以在res内返回token callback(res, xhr.getResponseHeader('X-CaptchaToken')) }) }, // 验证, angle用户旋转角度 check: (angle, token, callback) => { // 将token设置好, 数据验证时传递给后端 // 当然这里的数据请求只作为参考, 实际使用中以你的数据请求组件方式为准 this.token = token this.getJSON('http://vmd.co/common/captcha/verify', {angle: angle}, function(res, xhr) { if(xhr.status != 200) { alert('系统出错:'+ res.statusCode +',请关闭重试!') return false } callback(res, xhr) }) }, // 交换图片 img: (id) => { return 'http://vmd.co/common/captcha/img?id=' + id }, }, }, } }, methods: { openCaptcha() { this.captchaShow = true }, captchaInit(captcha) { }, captchaSuccess() { console.log('captcha success') }, captchaFail() { console.log('caotcha fail') }, captchaComplete(state) { // console.log('caotcha complete state: ' + state) }, // ajax请求, 这里只做演示, 请使用自己项目中的 getJSON(url, data, callback) { const _this = this; let params = ''; if(data && typeof data == 'object') { params = Object.keys(data).map(function(key) { return encodeURIComponent(key) + '=' + encodeURIComponent(data[key]); }).join('&'); url = url + ((url.indexOf('?') == -1 ? '?' : '&') + params); } let xhr, formData = null; xhr = new XMLHttpRequest(); xhr.withCredentials = false; xhr.open('GET', url); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded'); if(_this.token) { xhr.setRequestHeader('X-CaptchaToken', _this.token); } xhr.onload = function() { if (xhr.status != 200) { return; } try { let res = JSON.parse(xhr.responseText) || null; if (!res) { return; } callback(res, xhr); } catch(e) { return; } }; xhr.send(formData); } } } </script> <style> html, body { margin: 0; padding: 0; width: 100%; height: 100%; } #app { font-family: 'Avenir', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-align: center; color: #2c3e50; display: flex; flex-direction: column; justify-content: center; align-content: center; align-items: center; width: 100%; height: 100%; margin-top: 0; } </style> 在uniapp中使用

组件位置:js/vue/uniapp放在uniapp项目的uni_modules目录或者去官方插件中心搜索 isszz-captcha

<template > <view> <rotate-captcha :options="captchaOptions" v-if="captchaShow" @init="captchaInit" @close="captchaShow = false" @complete="captchaComplete" @success="captchaSuccess" @fail="captchaFail"></rotate-captcha> </view> </template> <script> export default { data() { return { captchaShow: false, captchaOptions: { theme: '#07f', title: '安全验证', desc: '拖动滑块,使图片角度为正', successClose: 1500, // 验证成功后页面关闭时间 timerProgressBar: true, // 验证成功后关闭时是否显示进度条 timerProgressBarColor: 'rgba(0, 0, 0, 0.2)', url: { info: 'http://cfyun.cc/common/captcha/rotate', // 获取验证码信息 check: 'http://cfyun.cc/common/captcha/verify', // 验证 img: 'http://cfyun.cc/common/captcha/img', // 交换图片 }, }, } }, created: function () { // 显示验证码 this.captchaShow = true }, methods: { captchaInit(captcha) { // console.log(captcha) }, captchaSuccess() { // console.log('captcha success') }, captchaFail() { // console.log('caotcha fail') }, captchaComplete(state) { // console.log('caotcha complete state: ' + state) }, } } </script>

版权声明:

1、该文章(资料)来源于互联网公开信息,我方只是对该内容做点评,所分享的下载地址为原作者公开地址。
2、网站不提供资料下载,如需下载请到原作者页面进行下载。