汪总电商平台
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

<?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];
}
}