You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
531 lines
20 KiB
531 lines
20 KiB
<?php
|
|
/**
|
|
* WanlCaptcha 1.0.0
|
|
*/
|
|
namespace addons\wanlshop\library\WanlSdk;
|
|
|
|
use think\Exception;
|
|
use think\Session;
|
|
use think\Request;
|
|
|
|
class Captcha
|
|
{
|
|
// 验证码加密盐
|
|
private $saltKey = 'WDaFnlSGhEo5p';
|
|
|
|
private $codeSet = '3456789ABCDEFGHJKLMNPQRTUVWXY';
|
|
private $fontSize = 26; // 验证码字体大小(px)
|
|
|
|
private $canvasSize = 200; // 画布大小
|
|
private $randomPoint = 200; // 随机点数量
|
|
private $randomLine = 50; // 随机线条数量
|
|
|
|
private $_image = null; // 验证码图片实例
|
|
private $_color = null; // 验证码字体颜色
|
|
|
|
/**
|
|
* 判断当前用户是否已完成验证码效验,且在验证码有效期内
|
|
*/
|
|
public function check($endCode = false, $id = '') {
|
|
$config = get_addon_config('wanlshop');
|
|
if ($config['captcha']['captcha_switch'] == 'Y') {
|
|
if (!$endCode) {
|
|
$key = $this->authcode($this->saltKey) . $id . '_log';
|
|
$secode = Session::get($key, '');
|
|
// 判断
|
|
if (!isset($secode) || $secode['captcha_use'] >= $config['captcha']['captchaUseMaxNum'] || $secode['captcha_time'] + $config['captcha']['captchaUseMaxTime'] < time()) {
|
|
return false;
|
|
} else {
|
|
Session::set($key . '.captcha_use', $secode['captcha_use'] + 1, '');
|
|
}
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* 输出验证码并把验证码的值保存的session中
|
|
* @access public
|
|
* @param string $id 要生成验证码的标识
|
|
*/
|
|
public function image($id = '') {
|
|
$request = Request::instance();
|
|
$config = get_addon_config('wanlshop');
|
|
// 查询判断
|
|
$loglist = model('app\admin\model\wanlshop\CaptchaLog')->where(['ip' => $request->ip() ])->select();
|
|
$hourError = 0;
|
|
$hourAll = 0;
|
|
$dayError = 0;
|
|
$dayAll = 0;
|
|
foreach ($loglist as $vo) {
|
|
// 计算一小时内
|
|
if ($vo['createtime'] > (time() - 60 * 60)) {
|
|
$hourAll+= 1;
|
|
$hourError+= $vo['times'];
|
|
}
|
|
// 计算一天内,当天凌晨计算
|
|
if ($vo['createtime'] > strtotime(date('Y-m-d 00:00:00'))) {
|
|
$dayAll+= 1;
|
|
$dayError+= $vo['times'];
|
|
}
|
|
}
|
|
//单IP一小时内允许出错多少次
|
|
if ($hourError > $config['captcha']['ipHourError']) {
|
|
return $this->result('10001:验证频繁请一小时后再试');
|
|
}
|
|
//单IP一小时内允许生成多少张验证码
|
|
if ($hourAll > $config['captcha']['ipHourAll']) {
|
|
return $this->result('10002:验证频繁请一小时后再试');
|
|
}
|
|
//单IP一天允许验证出错次数
|
|
if ($dayError > $config['captcha']['ipDayError']) {
|
|
return $this->result('10003:验证频繁请明天后再试');
|
|
}
|
|
//单IP一天允许生成多少次验证码
|
|
if ($dayAll > $config['captcha']['ipDayAll']) {
|
|
return $this->result('10004:验证频繁请明天后再试');
|
|
}
|
|
// 获取一张图库图片
|
|
$image = model('app\admin\model\wanlshop\Captcha')->orderRaw('rand()')->limit(1)->find();
|
|
if (!$image) {
|
|
return $this->result('10005:人机验证图库暂无图片');
|
|
}
|
|
// 拼接验证码原图路径
|
|
$capImgFile = $_SERVER['DOCUMENT_ROOT'] . $image['file'];
|
|
//验证码生成后的临时路径
|
|
$outImg = TEMP_PATH . md5(time() . '|' . rand(0, 999) . '|' . rand(0, 999) . $capImgFile) . '.png';
|
|
//验证码随机旋转角度
|
|
$angle = (int)rand(0, 360);
|
|
//验证码原图
|
|
$this->canvasSize = $config['captcha']['canvasSize']; // 画布大小
|
|
$this->randomPoint = $config['captcha']['randomPoint']; // 随机点数量
|
|
$this->randomLine = $config['captcha']['randomLine']; // 随机线条数量
|
|
// 服务端生成验证图片
|
|
// [验证码原图] [验证码输出地址] [验证码图旋转角度] [画布大小] [随机点数量] [随机线条数量] [随机大矩形数量]
|
|
if ($config['captcha']['captchaService'] == 'node') {
|
|
$node = ROOT_PATH .'node' .DS. 'captcha' .DS. 'index.js';
|
|
$cmd = "{$config['captcha']['nodePath']} {$node} {$capImgFile} {$outImg} {$angle} {$this->canvasSize} {$this->randomPoint} {$this->randomLine} {$config['captcha']['randomBlock']}";
|
|
if (intval(shell_exec($cmd)) !== 200) {
|
|
return $this->result('请按文档配置正确node效验', false, 5);
|
|
}
|
|
} else {
|
|
$cdnurl = \think\Config::get('upload.cdnurl');
|
|
$captchaGd = $this->_captchaGd($cdnurl ? $cdnurl. $image['file'] : $capImgFile, $outImg, $angle);
|
|
if (!$captchaGd) {
|
|
return $this->result('服务器生成验证图像失败', false, 5);
|
|
}
|
|
}
|
|
// 验证码图片转base64
|
|
$imgBase64 = base64_encode(file_get_contents($outImg));
|
|
//删除临时验证码
|
|
unlink($outImg);
|
|
// 记录日志
|
|
$log = model('app\admin\model\wanlshop\CaptchaLog');
|
|
$log->captcha_id = $image['id'];
|
|
$log->angle = $angle;
|
|
$log->ip = $request->ip();
|
|
$log->save();
|
|
if (!$log) {
|
|
return $this->result('10007:网络繁忙请稍后重试');
|
|
}
|
|
if (!$image->setInc('times')) {
|
|
return $this->result('10008:网络繁忙请稍后重试');
|
|
}
|
|
// 保存验证码
|
|
$key = $this->authcode($this->saltKey) . $id;
|
|
$angle = $this->authcode($angle);
|
|
$secode = ['verify_id' => $log['id'], 'verify_code' => $angle, 'verify_time' => time() ];
|
|
Session::set($key, $secode, '');
|
|
return $this->result('获取图片成功', $imgBase64, 0);
|
|
}
|
|
|
|
|
|
/**
|
|
* 验证验证图
|
|
* @param string $rotationAngle 旋转角度
|
|
* @param string $mouseTrackList 滑动轨迹
|
|
* @param string $dragUseTime 拖动用时
|
|
* @param string $dragStartTime 拖动开始时间
|
|
*/
|
|
public function checkCaptcha($rotationAngle, $mouseTrackList, $dragUseTime, $dragStartTime, $id = '')
|
|
{
|
|
$config = get_addon_config('wanlshop');
|
|
$key = $this->authcode($this->saltKey) . $id;
|
|
// 验证码不能为空
|
|
$secode = Session::get($key, '');
|
|
if (empty($secode)) {
|
|
return $this->result('系统无法获取到Session');
|
|
}
|
|
if (empty($rotationAngle) || empty($secode)) {
|
|
return $this->result('未安装node或者未按文档初始', true);
|
|
}
|
|
//缺少角度值,不进行数据效验
|
|
if (!isset($rotationAngle)) {
|
|
return $this->result('11002:人机验证失败', true);
|
|
} else {
|
|
$rotationAngle = (int)$rotationAngle;
|
|
}
|
|
if ($rotationAngle < 0 || $rotationAngle > 360) {
|
|
return $this->result('12001:请拖动旋转图像');
|
|
} else {
|
|
//旋转角度,四舍五入
|
|
$rotationAngle = (int)round(360 - $rotationAngle);
|
|
}
|
|
if (!isset($dragUseTime)) {
|
|
return $this->result('12002:人机效验数据异常');
|
|
} else {
|
|
$dragUseTime = (int)$dragUseTime;
|
|
}
|
|
if (!isset($dragStartTime)) {
|
|
return $this->result('12003:人机效验数据异常');
|
|
} else {
|
|
$dragStartTime = (int)$dragStartTime / 1000;
|
|
}
|
|
if (!isset($mouseTrackList) || !$mouseTrackList) {
|
|
return $this->result('12004:人机效验数据异常');
|
|
} else {
|
|
$mouseTrackList = $this->getJson((string)$mouseTrackList);
|
|
}
|
|
// 查询记录
|
|
$log = model('app\admin\model\wanlshop\CaptchaLog')->get($secode['verify_id']);
|
|
if (!$log) {
|
|
return $this->result('11003:网络繁忙请稍后再试', true);
|
|
}
|
|
//验证次数超出限制
|
|
if ($log['times'] >= $config['captcha']['oneCapErrNum']) {
|
|
return $this->result('验证失误过多系统将更换图片请重试', true);
|
|
}
|
|
//验证码超时时间效验
|
|
if ($log['createtime'] + $config['captcha']['checkTimeOut'] < time() || $secode['verify_time'] + $config['captcha']['checkTimeOut'] < time()) {
|
|
Session::delete($key, '');
|
|
return $this->result('11005:效验超时请重试', true);
|
|
}
|
|
$captchaCheckOutTime = $log['createtime'] + $config['captcha']['checkTimeOut'];
|
|
// 拖拽用时
|
|
if ($dragUseTime > $config['captcha']['dragTimeMax'] || $dragUseTime < $config['captcha']['dragTimeMin']) {
|
|
return $this->result('拖拽用时"过快"或"过慢" ~', true);
|
|
}
|
|
$dragUseTime = $dragUseTime / 1000;
|
|
// $dragStartTime < $log['createtime'] ||
|
|
if (($dragStartTime + $dragUseTime) > $captchaCheckOutTime) {
|
|
return $this->result('人机效验超时请重试', true);
|
|
}
|
|
/**
|
|
* 鼠标轨迹解析
|
|
*/
|
|
if (!$mouseTrackList || count($mouseTrackList) < 2) {
|
|
return $this->result('11008:拖拽过快请重试', true);
|
|
}
|
|
foreach ($mouseTrackList as $index => $item) {
|
|
if (!isset($item['r']) || !isset($item['t'])) {
|
|
return $this->result('11009:人机验证失败', true);
|
|
}
|
|
$item['t'] = $item['t'] / 1000;
|
|
//转为秒单位
|
|
if ($item['t'] < $dragStartTime) {
|
|
return $this->result('11010:人机验证失败', true);
|
|
}
|
|
if ($item['t'] > ($dragUseTime + $dragStartTime)) {
|
|
return $this->result('11011:人机验证失败', true);
|
|
}
|
|
$lastTime = $index == 0 ? $dragStartTime : $mouseTrackList[$index - 1]['t'] / 1000;
|
|
if ($item['t'] < $lastTime + ($config['captcha']['dragInterval'] / 1000)) {
|
|
return $this->result('11012:人机验证失败', true);
|
|
}
|
|
$item['r'] = (int)round($item['r']);
|
|
if ($item['r'] < 0 || $item['r'] > 100) {
|
|
return $this->result('11013:人机验证失败', true);
|
|
}
|
|
}
|
|
if (!in_array($rotationAngle, $this->getSuccessRotationAngle($log['angle']))) {
|
|
//角度效验失败
|
|
if (!$log->setInc('times')) {
|
|
return $this->result('12005:网络繁忙请稍后重试');
|
|
}
|
|
if ($log['times'] >= $config['captcha']['oneCapErrNum']) {
|
|
return $this->result('验证失误过多系统将更换图片请重试', true);
|
|
}
|
|
return $this->result('验证失败,请重试 ~');
|
|
} else {
|
|
//角度效验成功
|
|
if (!$log->save(['updatetime' => time() , 'succeedtime' => $dragUseTime])) {
|
|
return $this->result('12007:网络繁忙请稍后重试');
|
|
}
|
|
// 删除session
|
|
Session::delete($key, '');
|
|
//记录验证码使用次数
|
|
Session::set($key . '_log', ['captcha_use' => 0, 'captcha_time' => time() ], '');
|
|
return $this->result('人机验证效验成功', false, 0);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* 获取可通过的角度列表
|
|
* @param {Object} $rotationAngle 角度列表
|
|
*/
|
|
private function getSuccessRotationAngle($rotationAngle) {
|
|
$config = get_addon_config('wanlshop');
|
|
$yesArray = [$rotationAngle];
|
|
for ($i = 0; $i < $config['captcha']['errorAccuracy']; $i++) {
|
|
$yesArray[] = $rotationAngle - ($i + 1);
|
|
$yesArray[] = $rotationAngle + ($i + 1);
|
|
}
|
|
foreach ($yesArray as $index => $value) {
|
|
if ($value < 0) {
|
|
$yesArray[$index] = 360 + $value;
|
|
} else if ($value > 360) {
|
|
$yesArray[$index] = $value - 360;
|
|
}
|
|
}
|
|
if (in_array(0, $yesArray) && !in_array(360, $yesArray)) {
|
|
$yesArray[] = 360;
|
|
}
|
|
if (!in_array(0, $yesArray) && in_array(360, $yesArray)) {
|
|
$yesArray[] = 0;
|
|
}
|
|
return $yesArray;
|
|
}
|
|
|
|
/**
|
|
* php 生成验证图像
|
|
*
|
|
* @param {Object} $file 验证码原图
|
|
* @param {Object} $outImg 验证码输出地址
|
|
*/
|
|
private function _captchaGd($file = '', $outImg = '', $angle = 0) {
|
|
// 判断图片格式
|
|
$imageSize = getimagesize($file);
|
|
$imageSize = explode('/', $imageSize['mime']);
|
|
$type = $imageSize[1];
|
|
// 由文件创建图片
|
|
switch ($type) {
|
|
case "png":
|
|
@$this->_image = imagecreatefrompng($file);
|
|
break;
|
|
case "jpeg":
|
|
@$this->_image = imagecreatefromjpeg($file);
|
|
break;
|
|
case "jpg":
|
|
@$this->_image = imagecreatefromjpeg($file);
|
|
break;
|
|
case "gif":
|
|
@$this->_image = imagecreatefromgif($file);
|
|
break;
|
|
}
|
|
// 切图、旋转、再切图
|
|
return self::_cutSquares($outImg, $angle);
|
|
}
|
|
|
|
// 首次裁切
|
|
private function _cutSquares($outImg = '', $angle = 0) {
|
|
$w = imagesx($this->_image);
|
|
$h = imagesy($this->_image);
|
|
if ($w > $h) {
|
|
$new_height = $this->canvasSize;
|
|
$new_width = floor($w * ($new_height / $h));
|
|
$crop_x = ceil(($w - $h) / 2);
|
|
$crop_y = 0;
|
|
} else {
|
|
$new_width = $this->canvasSize;
|
|
$new_height = floor($h * ($new_width / $w));
|
|
$crop_x = 0;
|
|
$crop_y = ceil(($h - $w) / 2);
|
|
}
|
|
$tmp_img = imagecreatetruecolor($this->canvasSize, $this->canvasSize);
|
|
imagecopyresampled($tmp_img, $this->_image, 0, 0, $crop_x, $crop_y, $new_width, $new_height, $w, $h);
|
|
// 旋转角度
|
|
$this->_image = imagerotate($tmp_img, -$angle, 0);
|
|
// 销毁一图像
|
|
imagedestroy($tmp_img);
|
|
return self::_cuttingSquaretwice($outImg);
|
|
}
|
|
|
|
|
|
// 二次裁切
|
|
private function _cuttingSquaretwice($outImg = '') {
|
|
$w = imagesx($this->_image);
|
|
$h = imagesy($this->_image);
|
|
$new_width = $w * 2 * $this->canvasSize / $w;
|
|
$new_height = $h * 2 * $this->canvasSize / $w;
|
|
$crop_x = $w / 4;
|
|
$crop_y = $h / 4;
|
|
$tmp_img = imagecreatetruecolor($this->canvasSize, $this->canvasSize);
|
|
imagecopyresampled($tmp_img, $this->_image , 0, 0, $crop_x, $crop_y, $new_width, $new_height, $w, $h);
|
|
$this->_image = $tmp_img;
|
|
// 销毁一图像
|
|
// imagedestroy($tmp_img);
|
|
// 画曲线
|
|
return self::_writeCurve($outImg);
|
|
}
|
|
|
|
/**
|
|
* 画一条由两条连在一起构成的随机正弦函数曲线作干扰线(你可以改成更帅的曲线函数)
|
|
* 正弦型函数解析式:y=Asin(ωx+φ)+b
|
|
* 各常数值对函数图像的影响:
|
|
* A:决定峰值(即纵向拉伸压缩的倍数)
|
|
* b:表示波形在Y轴的位置关系或纵向移动距离(上加下减)
|
|
* φ:决定波形与X轴位置关系或横向移动距离(左加右减)
|
|
* ω:决定周期(最小正周期T=2π/∣ω∣)
|
|
*/
|
|
private function _writeCurve($outImg = '') {
|
|
$color = imagecolorallocate($this->_image, mt_rand(1, 120) , mt_rand(1, 120) , mt_rand(1, 120));
|
|
$A = mt_rand(1, $this->canvasSize / 2); // 振幅
|
|
$b = mt_rand(-$this->canvasSize / 4, $this->canvasSize / 4); // Y轴方向偏移量
|
|
$f = mt_rand(-$this->canvasSize / 4, $this->canvasSize / 4); // X轴方向偏移量
|
|
$T = mt_rand($this->canvasSize * 1.5, $this->canvasSize * 2); // 周期
|
|
$w = (2 * M_PI) / $T;
|
|
$px1 = 0; // 曲线横坐标起始位置
|
|
$px2 = mt_rand($this->canvasSize / 2, $this->canvasSize * 0.667); // 曲线横坐标结束位置
|
|
for ($px = $px1; $px <= $px2; $px = $px + 0.9) {
|
|
if ($w != 0) {
|
|
$py = $A * sin($w * $px + $f) + $b + $this->canvasSize / 2; // y = Asin(ωx+φ) + b
|
|
$i = (int)(($this->fontSize - 6) / 4);
|
|
while ($i > 0) {
|
|
imagesetpixel($this->_image, $px + $i, $py + $i, $color);
|
|
//这里画像素点比imagettftext和imagestring性能要好很多
|
|
$i--;
|
|
}
|
|
}
|
|
}
|
|
$A = mt_rand(1, $this->canvasSize / 2); // 振幅
|
|
$f = mt_rand(-$this->canvasSize / 4, $this->canvasSize / 4); // X轴方向偏移量
|
|
$T = mt_rand($this->canvasSize * 1.5, $this->canvasSize * 2); // 周期
|
|
$w = (2 * M_PI) / $T;
|
|
$b = $py - $A * sin($w * $px + $f) - $this->canvasSize / 2;
|
|
$px1 = $px2;
|
|
$px2 = $this->canvasSize;
|
|
for ($px = $px1; $px <= $px2; $px = $px + 0.9) {
|
|
if ($w != 0) {
|
|
$py = $A * sin($w * $px + $f) + $b + $this->canvasSize / 2; // y = Asin(ωx+φ) + b
|
|
$i = (int)(($this->fontSize - 8) / 4);
|
|
while ($i > 0) {
|
|
imagesetpixel($this->_image, $px + $i, $py + $i, $color);
|
|
$i--;
|
|
}
|
|
}
|
|
}
|
|
// 画背景
|
|
return self::_writeNoise($outImg);
|
|
}
|
|
|
|
|
|
/**
|
|
* 画杂点
|
|
* 往图片上写不同颜色的字母或数字
|
|
*/
|
|
private function _writeNoise($outImg = '') {
|
|
for ($i = 0; $i < 10; $i++) {
|
|
//杂点颜色
|
|
$noiseColor = imagecolorallocate($this->_image, mt_rand(150, 225) , mt_rand(150, 225) , mt_rand(150, 225));
|
|
for ($j = 0; $j < 5; $j++) {
|
|
// 绘杂点
|
|
imagestring($this->_image, 5, mt_rand(-10, $this->canvasSize) , mt_rand(-10, $this->canvasSize) , $this->codeSet[mt_rand(0, 28) ], // 杂点文本为随机的字母或数字
|
|
$noiseColor);
|
|
}
|
|
}
|
|
return self::_writeLine($outImg);
|
|
}
|
|
|
|
// 画线
|
|
private function _writeLine($outImg = '') {
|
|
//画干扰点
|
|
for ($i = 0; $i < $this->randomPoint; $i++) {
|
|
//设置随机颜色
|
|
$randColor = imagecolorallocate($this->_image, rand(0, 255) , rand(0, 255) , rand(0, 255));
|
|
//画点
|
|
imagesetpixel($this->_image, rand(1, $this->canvasSize) , rand(1, $this->canvasSize) , $randColor);
|
|
}
|
|
//画干扰线
|
|
for ($i = 0; $i < $this->randomLine; $i++) {
|
|
//设置随机颜色
|
|
$randColor = imagecolorallocate($this->_image, rand(0, 200) , rand(0, 200) , rand(0, 200));
|
|
//画线
|
|
imageline($this->_image, rand(1, $this->canvasSize) , rand(1, $this->canvasSize) , rand(1, $this->canvasSize) , rand(1, $this->canvasSize) , $randColor);
|
|
}
|
|
return self::_cutRound($outImg);
|
|
}
|
|
|
|
|
|
// 裁切圆形
|
|
private function _cutRound($outImg = '') {
|
|
//创建新图
|
|
$tmp_img = imagecreatetruecolor($this->canvasSize, $this->canvasSize);
|
|
// 启用混色模式
|
|
imagealphablending($tmp_img, false); //设定图像的混色模式
|
|
$transparent = imagecolorallocatealpha($tmp_img, 255, 255, 255, 127); //边缘透明
|
|
$w = $this->canvasSize;
|
|
$h = $this->canvasSize;
|
|
$w = min($w, $h);
|
|
$h = $w;
|
|
$r = $w / 2;
|
|
for ($x = 0; $x < $w; $x++) for ($y = 0; $y < $h; $y++) {
|
|
$c = imagecolorat($this->_image, $x, $y);
|
|
$_x = $x - $w / 2;
|
|
$_y = $y - $h / 2;
|
|
if ((($_x * $_x) + ($_y * $_y)) < ($r * $r)) {
|
|
imagesetpixel($tmp_img, $x, $y, $c);
|
|
} else {
|
|
imagesetpixel($tmp_img, $x, $y, $transparent);
|
|
}
|
|
}
|
|
$png = imagepng($tmp_img, $outImg);
|
|
// 销毁一图像
|
|
imagedestroy($this->_image);
|
|
imagedestroy($tmp_img);
|
|
return $png;
|
|
}
|
|
|
|
|
|
/**
|
|
* 加密验证码
|
|
* @param {Object} $str 字符串
|
|
*/
|
|
private function authcode($str) {
|
|
$config = get_addon_config('wanlshop');
|
|
$key = substr(md5($config['captcha']['seKey']) , 5, 8);
|
|
$str = substr(md5($str) , 8, 10);
|
|
return md5($key . $str);
|
|
}
|
|
|
|
|
|
/**
|
|
* json_decode 二次封装
|
|
* @param {Object} $json
|
|
*/
|
|
private function getJson($json)
|
|
{
|
|
if (is_array($json)) {
|
|
return $json;
|
|
}else{
|
|
$json = htmlspecialchars_decode($json);
|
|
}
|
|
if (!$json) {
|
|
return false;
|
|
}
|
|
$rt = false;
|
|
try {
|
|
$rt = json_decode($json, true);
|
|
}
|
|
catch(Exception $th) {
|
|
var_dump($th);
|
|
}
|
|
if (!is_array($rt)) {
|
|
return false;
|
|
}
|
|
return $rt;
|
|
}
|
|
|
|
|
|
/**
|
|
* 结束代码并返回需要刷新验证码
|
|
*/
|
|
public function result($msg = '请先完成人机效验!', $data = false, $code = 4) {
|
|
return ['code' => $code, 'data' => $data, 'msg' => $msg];
|
|
}
|
|
} |