ClickCaptcha.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. <?php
  2. namespace ba;
  3. use Throwable;
  4. use think\facade\Db;
  5. use think\facade\Lang;
  6. use think\facade\Config;
  7. /**
  8. * 点选文字验证码类
  9. */
  10. class ClickCaptcha
  11. {
  12. /**
  13. * 验证码过期时间(s)
  14. * @var int
  15. */
  16. private int $expire = 600;
  17. /**
  18. * 可以使用的背景图片路径
  19. * @var array
  20. */
  21. private array $bgPaths = [
  22. 'static/images/captcha/click/bgs/1.png',
  23. 'static/images/captcha/click/bgs/2.png',
  24. 'static/images/captcha/click/bgs/3.png',
  25. ];
  26. /**
  27. * 可以使用的字体文件路径
  28. * @var array
  29. */
  30. private array $fontPaths = [
  31. 'static/fonts/zhttfs/SourceHanSansCN-Normal.ttf',
  32. ];
  33. /**
  34. * 验证点 Icon 映射表
  35. * @var array
  36. */
  37. private array $iconDict = [
  38. 'aeroplane' => '飞机',
  39. 'apple' => '苹果',
  40. 'banana' => '香蕉',
  41. 'bell' => '铃铛',
  42. 'bicycle' => '自行车',
  43. 'bird' => '小鸟',
  44. 'bomb' => '炸弹',
  45. 'butterfly' => '蝴蝶',
  46. 'candy' => '糖果',
  47. 'crab' => '螃蟹',
  48. 'cup' => '杯子',
  49. 'dolphin' => '海豚',
  50. 'fire' => '火',
  51. 'guitar' => '吉他',
  52. 'hexagon' => '六角形',
  53. 'pear' => '梨',
  54. 'rocket' => '火箭',
  55. 'sailboat' => '帆船',
  56. 'snowflake' => '雪花',
  57. 'wolf head' => '狼头',
  58. ];
  59. /**
  60. * 配置
  61. * @var array
  62. */
  63. private array $config = [
  64. // 透明度
  65. 'alpha' => 36,
  66. // 中文字符集
  67. 'zhSet' => '们以我到他会作时要动国产的是工就年阶义发成部民可出能方进在和有大这主中为来分生对于学级地用同行面说种过命度革而多子后自社加小机也经力线本电高量长党得实家定深法表着水理化争现所起政好十战无农使前等反体合斗路图把结第里正新开论之物从当两些还天资事队点育重其思与间内去因件利相由压员气业代全组数果期导平各基或月然如应形想制心样都向变关问比展那它最及外没看治提五解系林者米群头意只明四道马认次文通但条较克又公孔领军流接席位情运器并飞原油放立题质指建区验活众很教决特此常石强极已根共直团统式转别造切九你取西持总料连任志观调么山程百报更见必真保热委手改管处己将修支识象先老光专什六型具示复安带每东增则完风回南劳轮科北打积车计给节做务被整联步类集号列温装即毫知轴研单坚据速防史拉世设达尔场织历花求传断况采精金界品判参层止边清至万确究书术状须离再目海权且青才证低越际八试规斯近注办布门铁需走议县兵固除般引齿胜细影济白格效置推空配叶率述今选养德话查差半敌始片施响收华觉备名红续均药标记难存测身紧液派准斤角降维板许破述技消底床田势端感往神便贺村构照容非亚磨族段算适讲按值美态易彪服早班麦削信排台声该击素张密害侯何树肥继右属市严径螺检左页抗苏显苦英快称坏移巴材省黑武培著河帝仅针怎植京助升王眼她抓苗副杂普谈围食源例致酸旧却充足短划剂宣环落首尺波承粉践府鱼随考刻靠够满夫失包住促枝局菌杆周护岩师举曲春元超负砂封换太模贫减阳扬江析亩木言球朝医校古呢稻宋听唯输滑站另卫字鼓刚写刘微略范供阿块某功友限项余倒卷创律雨让骨远帮初皮播优占圈伟季训控激找叫云互跟粮粒母练塞钢顶策双留误础阻故寸盾晚丝女散焊功株亲院冷彻弹错散商视艺版烈零室轻倍缺厘泵察绝富城冲喷壤简否柱李望盘磁雄似困巩益洲脱投送侧润盖挥距触星松送获兴独官混纪依未突架宽冬章偏纹吃执阀矿寨责熟稳夺硬价努翻奇甲预职评读背协损棉侵灰虽矛厚罗泥辟告箱掌氧恩爱停曾溶营终纲孟钱待尽俄缩沙退陈讨奋械载胞哪旋征槽倒握担仍呀鲜吧卡粗介钻逐弱脚怕盐末丰雾冠丙街莱贝辐肠付吉渗瑞惊顿挤秒悬姆森糖圣凹陶词迟蚕亿矩康遵牧遭幅园腔订香肉弟屋敏恢忘编印蜂急拿扩飞露核缘游振操央伍域甚迅辉异序免纸夜乡久隶念兰映沟乙吗儒汽磷艰晶埃燃欢铁补咱芽永瓦倾阵碳演威附牙芽永瓦斜灌欧献顺猪洋腐请透司括脉宜笑若尾束壮暴企菜穗楚汉愈绿拖牛份染既秋遍锻玉夏疗尖井费州访吹荣铜沿替滚客召旱悟刺脑措贯藏敢令隙炉壳硫煤迎铸粘探临薄旬善福纵择礼愿伏残雷延烟句纯渐耕跑泽慢栽鲁赤繁境潮横掉锥希池败船假亮谓托伙哲怀摆贡呈劲财仪沉炼麻祖息车穿货销齐鼠抽画饲龙库守筑房歌寒喜哥洗蚀废纳腹乎录镜脂庄擦险赞钟摇典柄辩竹谷乱虚桥奥伯赶垂途额壁网截野遗静谋弄挂课镇妄盛耐扎虑键归符庆聚绕摩忙舞遇索顾胶羊湖钉仁音迹碎伸灯避泛答勇频皇柳哈揭甘诺概宪浓岛袭谁洪谢炮浇斑讯懂灵蛋闭孩释巨徒私银伊景坦累匀霉杜乐勒隔弯绩招绍胡呼峰零柴簧午跳居尚秦稍追梁折耗碱殊岗挖氏刃剧堆赫荷胸衡勤膜篇登驻案刊秧缓凸役剪川雪链渔啦脸户洛孢勃盟买杨宗焦赛旗滤硅炭股坐蒸凝竟枪黎救冒暗洞犯筒您宋弧爆谬涂味津臂障褐陆啊健尊豆拔莫抵桑坡缝警挑冰柬嘴啥饭塑寄赵喊垫丹渡耳虎笔稀昆浪萨茶滴浅拥覆伦娘吨浸袖珠雌妈紫戏塔锤震岁貌洁剖牢锋疑霸闪埔猛诉刷忽闹乔唐漏闻沈熔氯荒茎男凡抢像浆旁玻亦忠唱蒙予纷捕锁尤乘乌智淡允叛畜俘摸锈扫毕璃宝芯爷鉴秘净蒋钙肩腾枯抛轨堂拌爸循诱祝励肯酒绳塘燥泡袋朗喂铝软渠颗惯贸综墙趋彼届墨碍启逆卸航衣孙龄岭休借',
  68. ];
  69. /**
  70. * 构造方法
  71. * @param array $config 点击验证码配置
  72. * @throws Throwable
  73. */
  74. public function __construct(array $config = [])
  75. {
  76. $clickConfig = Config::get('buildadmin.click_captcha');
  77. $this->config = array_merge($clickConfig, $this->config, $config);
  78. // 清理过期的验证码
  79. Db::name('captcha')->where('expire_time', '<', time())->delete();
  80. }
  81. /**
  82. * 创建图形验证码
  83. * @param string $id 验证码ID,开发者自定义
  84. * @return array 返回验证码图片的base64编码和验证码文字信息
  85. */
  86. public function creat(string $id): array
  87. {
  88. $imagePath = Filesystem::fsFit(public_path() . $this->bgPaths[mt_rand(0, count($this->bgPaths) - 1)]);
  89. $fontPath = Filesystem::fsFit(public_path() . $this->fontPaths[mt_rand(0, count($this->fontPaths) - 1)]);
  90. $randPoints = $this->randPoints($this->config['length'] + $this->config['confuse_length']);
  91. $lang = Lang::getLangSet();
  92. foreach ($randPoints as $v) {
  93. $tmp['size'] = rand(15, 30);
  94. if (isset($this->iconDict[$v])) {
  95. // 图标
  96. $tmp['icon'] = true;
  97. $tmp['name'] = $v;
  98. $tmp['text'] = $lang == 'zh-cn' ? "<{$this->iconDict[$v]}>" : "<$v>";
  99. $iconInfo = getimagesize(Filesystem::fsFit(public_path() . 'static/images/captcha/click/icons/' . $v . '.png'));
  100. $tmp['width'] = $iconInfo[0];
  101. $tmp['height'] = $iconInfo[1];
  102. } else {
  103. // 字符串文本框宽度和长度
  104. $fontArea = imagettfbbox($tmp['size'], 0, $fontPath, $v);
  105. $textWidth = $fontArea[2] - $fontArea[0];
  106. $textHeight = $fontArea[1] - $fontArea[7];
  107. $tmp['icon'] = false;
  108. $tmp['text'] = $v;
  109. $tmp['width'] = $textWidth;
  110. $tmp['height'] = $textHeight;
  111. }
  112. $textArr['text'][] = $tmp;
  113. }
  114. // 图片宽高和类型
  115. $imageInfo = getimagesize($imagePath);
  116. $textArr['width'] = $imageInfo[0];
  117. $textArr['height'] = $imageInfo[1];
  118. // 随机生成验证点位置
  119. foreach ($textArr['text'] as &$v) {
  120. list($x, $y) = $this->randPosition($textArr['text'], $textArr['width'], $textArr['height'], $v['width'], $v['height'], $v['icon']);
  121. $v['x'] = $x;
  122. $v['y'] = $y;
  123. $text[] = $v['text'];
  124. }
  125. unset($v);
  126. // 创建图片的实例
  127. $image = imagecreatefromstring(file_get_contents($imagePath));
  128. foreach ($textArr['text'] as $v) {
  129. if ($v['icon']) {
  130. $this->iconCover($image, $v);
  131. } else {
  132. //字体颜色
  133. $color = imagecolorallocatealpha($image, 239, 239, 234, 127 - intval($this->config['alpha'] * (127 / 100)));
  134. // 绘画文字
  135. imagettftext($image, $v['size'], 0, $v['x'], $v['y'], $color, $fontPath, $v['text']);
  136. }
  137. }
  138. $nowTime = time();
  139. $textArr['text'] = array_splice($textArr['text'], 0, $this->config['length']);
  140. $text = array_splice($text, 0, $this->config['length']);
  141. Db::name('captcha')
  142. ->replace()
  143. ->insert([
  144. 'key' => md5($id),
  145. 'code' => md5(implode(',', $text)),
  146. 'captcha' => json_encode($textArr, JSON_UNESCAPED_UNICODE),
  147. 'create_time' => date('Y-m-d H:i:s',$nowTime),
  148. 'expire_time' => $nowTime + $this->expire
  149. ]);
  150. // 输出图片
  151. while (ob_get_level()) {
  152. ob_end_clean();
  153. }
  154. if (!ob_get_level()) ob_start();
  155. switch ($imageInfo[2]) {
  156. case 1:// GIF
  157. imagegif($image);
  158. $content = ob_get_clean();
  159. break;
  160. case 2:// JPG
  161. imagejpeg($image);
  162. $content = ob_get_clean();
  163. break;
  164. case 3:// PNG
  165. imagepng($image);
  166. $content = ob_get_clean();
  167. break;
  168. default:
  169. $content = '';
  170. break;
  171. }
  172. imagedestroy($image);
  173. return [
  174. 'id' => $id,
  175. 'text' => $text,
  176. 'base64' => 'data:' . $imageInfo['mime'] . ';base64,' . base64_encode($content),
  177. 'width' => $textArr['width'],
  178. 'height' => $textArr['height'],
  179. ];
  180. }
  181. /**
  182. * 检查验证码
  183. * @param string $id 开发者自定义的验证码ID
  184. * @param string $info 验证信息
  185. * @param bool $unset 验证成功是否删除验证码
  186. * @return bool
  187. * @throws Throwable
  188. */
  189. public function check(string $id, string $info, bool $unset = true): bool
  190. {
  191. $key = md5($id);
  192. $captcha = Db::name('captcha')->where('key', $key)->find();
  193. if ($captcha) {
  194. // 验证码过期
  195. if (time() > $captcha['expire_time']) {
  196. Db::name('captcha')->where('key', $key)->delete();
  197. return false;
  198. }
  199. $textArr = json_decode($captcha['captcha'], true);
  200. list($xy, $w, $h) = explode(';', $info);
  201. $xyArr = explode('-', $xy);
  202. $xPro = $w / $textArr['width'];// 宽度比例
  203. $yPro = $h / $textArr['height'];// 高度比例
  204. foreach ($xyArr as $k => $v) {
  205. $xy = explode(',', $v);
  206. $x = $xy[0];
  207. $y = $xy[1];
  208. if ($x / $xPro < $textArr['text'][$k]['x'] || $x / $xPro > $textArr['text'][$k]['x'] + $textArr['text'][$k]['width']) {
  209. return false;
  210. }
  211. $phStart = $textArr['text'][$k]['icon'] ? $textArr['text'][$k]['y'] : $textArr['text'][$k]['y'] - $textArr['text'][$k]['height'];
  212. $phEnd = $textArr['text'][$k]['icon'] ? $textArr['text'][$k]['y'] + $textArr['text'][$k]['height'] : $textArr['text'][$k]['y'];
  213. if ($y / $yPro < $phStart || $y / $yPro > $phEnd) {
  214. return false;
  215. }
  216. }
  217. if ($unset) Db::name('captcha')->where('key', $key)->delete();
  218. return true;
  219. } else {
  220. return false;
  221. }
  222. }
  223. /**
  224. * 绘制Icon
  225. */
  226. protected function iconCover($bgImg, $iconImgData): void
  227. {
  228. $iconImage = imagecreatefrompng(Filesystem::fsFit(public_path() . 'static/images/captcha/click/icons/' . $iconImgData['name'] . '.png'));
  229. $trueColorImage = imagecreatetruecolor($iconImgData['width'], $iconImgData['height']);
  230. imagecopy($trueColorImage, $bgImg, 0, 0, $iconImgData['x'], $iconImgData['y'], $iconImgData['width'], $iconImgData['height']);
  231. imagecopy($trueColorImage, $iconImage, 0, 0, 0, 0, $iconImgData['width'], $iconImgData['height']);
  232. imagecopymerge($bgImg, $trueColorImage, $iconImgData['x'], $iconImgData['y'], 0, 0, $iconImgData['width'], $iconImgData['height'], $this->config['alpha']);
  233. imagedestroy($iconImage);
  234. imagedestroy($trueColorImage);
  235. }
  236. /**
  237. * 随机生成验证点元素
  238. * @param int $length
  239. * @return array
  240. */
  241. public function randPoints(int $length = 4): array
  242. {
  243. $arr = [];
  244. // 文字
  245. if (in_array('text', $this->config['mode'])) {
  246. for ($i = 0; $i < $length; $i++) {
  247. $arr[] = mb_substr($this->config['zhSet'], mt_rand(0, mb_strlen($this->config['zhSet'], 'utf-8') - 1), 1, 'utf-8');
  248. }
  249. }
  250. // 图标
  251. if (in_array('icon', $this->config['mode'])) {
  252. $icon = array_keys($this->iconDict);
  253. shuffle($icon);
  254. $icon = array_slice($icon, 0, $length);
  255. $arr = array_merge($arr, $icon);
  256. }
  257. shuffle($arr);
  258. return array_slice($arr, 0, $length);
  259. }
  260. /**
  261. * 随机生成位置布局
  262. * @param array $textArr 点位数据
  263. * @param int $imgW 图片宽度
  264. * @param int $imgH 图片高度
  265. * @param int $fontW 文字宽度
  266. * @param int $fontH 文字高度
  267. * @param bool $isIcon 是否是图标
  268. * @return array
  269. */
  270. private function randPosition(array $textArr, int $imgW, int $imgH, int $fontW, int $fontH, bool $isIcon): array
  271. {
  272. $x = rand(0, $imgW - $fontW);
  273. $y = rand($fontH, $imgH - $fontH);
  274. // 碰撞验证
  275. if (!$this->checkPosition($textArr, $x, $y, $fontW, $fontH, $isIcon)) {
  276. $position = $this->randPosition($textArr, $imgW, $imgH, $fontW, $fontH, $isIcon);
  277. } else {
  278. $position = [$x, $y];
  279. }
  280. return $position;
  281. }
  282. /**
  283. * 碰撞验证
  284. * @param array $textArr 验证点数据
  285. * @param int $x x轴位置
  286. * @param int $y y轴位置
  287. * @param int $w 验证点宽度
  288. * @param int $h 验证点高度
  289. * @param bool $isIcon 是否是图标
  290. * @return bool
  291. */
  292. public function checkPosition(array $textArr, int $x, int $y, int $w, int $h, bool $isIcon): bool
  293. {
  294. $flag = true;
  295. foreach ($textArr as $v) {
  296. if (isset($v['x']) && isset($v['y'])) {
  297. $flagX = false;
  298. $flagY = false;
  299. $historyPw = $v['x'] + $v['width'];
  300. if (($x + $w) < $v['x'] || $x > $historyPw) {
  301. $flagX = true;
  302. }
  303. $currentPhStart = $isIcon ? $y : $y - $h;
  304. $currentPhEnd = $isIcon ? $y + $v['height'] : $y;
  305. $historyPhStart = $v['icon'] ? $v['y'] : ($v['y'] - $v['height']);
  306. $historyPhEnd = $v['icon'] ? ($v['y'] + $v['height']) : $v['y'];
  307. if ($currentPhEnd < $historyPhStart || $currentPhStart > $historyPhEnd) {
  308. $flagY = true;
  309. }
  310. if (!$flagX && !$flagY) {
  311. $flag = false;
  312. }
  313. }
  314. }
  315. return $flag;
  316. }
  317. }