ClickCaptcha.php 15 KB

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