Captcha.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470
  1. <?php
  2. namespace Tool\MayouTool\Captcha\Src;
  3. class Captcha
  4. {
  5. /**
  6. * 验证码配置
  7. * @var array
  8. */
  9. protected $config = [
  10. /**
  11. * 调试模型
  12. */
  13. 'debug' => false,
  14. /**
  15. * 默认验证码长度
  16. * @var int
  17. */
  18. 'length' => 4,
  19. /**
  20. * 验证码字符集
  21. * @var string
  22. */
  23. 'charset' => 'abcdefghijklmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ123456789',
  24. /**
  25. * 是否开启严格模式(区分大小写)
  26. * @var bool
  27. */
  28. 'strict' => false,
  29. /**
  30. * 默认验证码宽度
  31. * @var int
  32. */
  33. 'width' => 150,
  34. /**
  35. * 默认验证码高度
  36. * @var int
  37. */
  38. 'height' => 40,
  39. /**
  40. * 指定文字颜色
  41. * @var string
  42. */
  43. 'textColor' => null,
  44. /**
  45. * 文字字体文件
  46. * @var string
  47. */
  48. 'textFont' => null,
  49. /**
  50. * 指定图片背景色
  51. * @var string
  52. */
  53. 'backgroundColor' => null,
  54. /**
  55. * 开启失真模式
  56. * @var bool
  57. */
  58. 'distortion' => true,
  59. /**
  60. * 最大前景线条数
  61. * @var int
  62. */
  63. 'maxFrontLines' => null,
  64. /**
  65. * 最大背景线条数
  66. * @val int
  67. */
  68. 'maxBehindLines' => null,
  69. /**
  70. * 文字最大角度
  71. * @var int
  72. */
  73. 'maxAngle' => 8,
  74. /**
  75. * 文字最大偏移量
  76. * @var int
  77. */
  78. 'maxOffset' => 5
  79. ];
  80. /**
  81. * Captcha constructor.
  82. * @param array $config
  83. */
  84. public function __construct(array $config = [])
  85. {
  86. $this->setConfig($config);
  87. }
  88. /**
  89. * 设置验证码配置
  90. *
  91. * @param array|string $config 配置数组或配置项key
  92. * @param mixed $value 配置项值
  93. * @return $this
  94. */
  95. public function setConfig($config, $value = null)
  96. {
  97. if (!is_array($config)) {
  98. $config = [$config => $value];
  99. }
  100. foreach ($config as $key => $value) {
  101. if (array_key_exists($key, $this->config)) {
  102. $this->config[$key] = $value;
  103. }
  104. }
  105. return $this;
  106. }
  107. /**
  108. * 获取配置
  109. *
  110. * @param string|null $key 配置项key
  111. * @return string|number|array
  112. */
  113. public function getConfig($key = null)
  114. {
  115. if ($key !== null) {
  116. return $this->config[$key];
  117. }
  118. return $this->config;
  119. }
  120. /**
  121. * 生成验证码
  122. *
  123. * @return Image
  124. */
  125. public function make($key)
  126. {
  127. $code = $this->generate();
  128. $hash = password_hash($code, PASSWORD_BCRYPT, array('cost' => 10));
  129. if ($hash === false) {
  130. throw new \RuntimeException('Bcrypt hashing not supported.');
  131. }
  132. app("redis")->setex($key,180,$hash);
  133. return new Image($this->build($code));
  134. }
  135. /**
  136. * 仅测试正确性, 不删除验证码
  137. * @param $key string
  138. * @param string $input
  139. * @return bool
  140. */
  141. public function test($key,$input)
  142. {
  143. $code = app("redis")->get($key);
  144. if ($this->config['strict']) {
  145. // 开启严格模式
  146. password_verify($input, $code);
  147. }
  148. //返回验证结果
  149. return password_verify(strtoupper($input), $code);
  150. }
  151. /**
  152. * 检测正确性,并删除验证码
  153. *
  154. * @param string $key
  155. * @param string $input
  156. * @return bool
  157. */
  158. public function check($key,$input)
  159. {
  160. $result = $this->test($key,$input);
  161. app("redis")->del($key);
  162. return $result;
  163. }
  164. /**
  165. * 生成验证码
  166. *
  167. * @return string
  168. */
  169. protected function generate()
  170. {
  171. $characters = str_split($this->getConfig('charset'));
  172. $length = $this->getConfig('length');
  173. $code = '';
  174. for ($i = 0; $i < $length; $i++) {
  175. $code .= $characters[rand(0, count($characters) - 1)];
  176. }
  177. if ($this->config['strict']) {
  178. return $code;
  179. }
  180. return strtoupper($code);
  181. }
  182. /**
  183. * 创建验证码图片
  184. *
  185. * @param string $code
  186. * @return resource
  187. */
  188. protected function build($code)
  189. {
  190. // 图片宽
  191. $width = $this->getConfig('width');
  192. // 图片高
  193. $height = $this->getConfig('height');
  194. // 背景颜色
  195. $backgroundColor = $this->getConfig('backgroundColor');
  196. // 随机取一个字体
  197. $font = $this->getTextFont();
  198. // 根据宽高创建一个背景画布
  199. $image = imagecreatetruecolor($width, $height);
  200. if ($backgroundColor === null) {
  201. $backgroundColor = imagecolorallocate($image, mt_rand(200, 255), mt_rand(200, 255), mt_rand(200, 255));
  202. } else {
  203. $color = $backgroundColor;
  204. $backgroundColor = imagecolorallocate($image, $color[0], $color[1], $color[2]);
  205. }
  206. // 填充背景色
  207. imagefill($image, 0, 0, $backgroundColor);
  208. // 绘制背景干扰线
  209. $this->drawLines($image, $this->getConfig('maxBehindLines'));
  210. // 写入验证码文字
  211. $color = $this->renderText($image, $code, $font);
  212. // 绘制前景干扰线
  213. $this->drawLines($image, $this->getConfig('maxFrontLines'), $color);
  214. if ($this->getConfig('distortion')) {
  215. // 创建失真
  216. $image = $this->createDistortion($image, $width, $height, $backgroundColor);
  217. }
  218. //如果不指定字体颜色和背景颜色,则使用图像过滤器修饰
  219. if (function_exists('imagefilter') && is_null($backgroundColor) && is_null($this->getConfig('textColor'))) {
  220. // 颜色翻转 - 1/2几率
  221. if (mt_rand(0, 1) == 0) {
  222. imagefilter($image, IMG_FILTER_NEGATE);
  223. }
  224. // 用边缘检测来突出图像的边缘 - 1/11几率
  225. if (mt_rand(0, 10) == 0) {
  226. imagefilter($image, IMG_FILTER_EDGEDETECT);
  227. }
  228. // 改变图像的对比度
  229. imagefilter($image, IMG_FILTER_CONTRAST, mt_rand(-50, 10));
  230. if (mt_rand(0, 5) == 0) {
  231. // 用高斯算法和指定颜色模糊图像
  232. imagefilter($image, IMG_FILTER_COLORIZE, mt_rand(-80, 50), mt_rand(-80, 50), mt_rand(-80, 50));
  233. }
  234. }
  235. return $image;
  236. }
  237. /**
  238. * 创建失真
  239. *
  240. * @param resource $image
  241. * @param int $width
  242. * @param int $height
  243. * @param int $backgroundColor
  244. * @return resource
  245. */
  246. protected function createDistortion($image, $width, $height, $backgroundColor)
  247. {
  248. //创建失真
  249. $contents = imagecreatetruecolor($width, $height);
  250. $rWidth = mt_rand(0, $width);
  251. $rHeight = mt_rand(0, $height);
  252. $phase = mt_rand(0, 10);
  253. $scale = 1.1 + mt_rand(0, 10000) / 30000;
  254. for ($x = 0; $x < $width; $x++) {
  255. for ($y = 0; $y < $height; $y++) {
  256. $vX = $x - $rWidth;
  257. $vY = $y - $rHeight;
  258. $vN = sqrt($vX * $vX + $vY * $vY);
  259. if ($vN != 0) {
  260. $vN2 = $vN + 4 * sin($vN / 30);
  261. $nX = $rWidth + ($vX * $vN2 / $vN);
  262. $nY = $rHeight + ($vY * $vN2 / $vN);
  263. } else {
  264. $nX = $rWidth;
  265. $nY = $rHeight;
  266. }
  267. $nY = $nY + $scale * sin($phase + $nX * 0.2);
  268. $pixel = $this->getColor($image, round($nX), round($nY), $backgroundColor);
  269. if ($pixel == 0) {
  270. $pixel = $backgroundColor;
  271. }
  272. imagesetpixel($contents, $x, $y, $pixel);
  273. }
  274. }
  275. return $contents;
  276. }
  277. /**
  278. * 获取一个字体
  279. *
  280. * @return string
  281. */
  282. protected function getTextFont()
  283. {
  284. // 指定字体
  285. if ($this->getConfig('textFont') && file_exists($this->getConfig('textFont'))) {
  286. return $this->getConfig('textFont');
  287. }
  288. // 随机字体
  289. return __DIR__ . '/../fonts/' . mt_rand(0, 5) . '.ttf';
  290. }
  291. /**
  292. * 写入验证码到图片中
  293. *
  294. * @param resource $image
  295. * @param string $phrase
  296. * @param string $font
  297. * @return int
  298. */
  299. protected function renderText($image, $phrase, $font)
  300. {
  301. $length = strlen($phrase);
  302. if ($length === 0) {
  303. return imagecolorallocate($image, 0, 0, 0);
  304. }
  305. // 计算文字尺寸
  306. $size = $this->getConfig('width') / $length - mt_rand(0, 3) - 1;
  307. $box = imagettfbbox($size, 0, $font, $phrase);
  308. $textWidth = $box[2] - $box[0];
  309. $textHeight = $box[1] - $box[7];
  310. $x = ($this->getConfig('width') - $textWidth) / 2;
  311. $y = ($this->getConfig('height') - $textHeight) / 2 + $size;
  312. if (!$this->getConfig('textColor')) {
  313. $textColor = array(mt_rand(0, 150), mt_rand(0, 150), mt_rand(0, 150));
  314. } else {
  315. $textColor = $this->getConfig('textColor');
  316. }
  317. $color = imagecolorallocate($image, $textColor[0], $textColor[1], $textColor[2]);
  318. // 循环写入字符,随机角度
  319. for ($i = 0; $i < $length; $i++) {
  320. $box = imagettfbbox($size, 0, $font, $phrase[$i]);
  321. $w = $box[2] - $box[0];
  322. $angle = mt_rand(-$this->getConfig('maxAngle'), $this->getConfig('maxAngle'));
  323. $offset = mt_rand(-$this->getConfig('maxOffset'), $this->getConfig('maxOffset'));
  324. imagettftext($image, $size, $angle, $x, $y + $offset, $color, $font, $phrase[$i]);
  325. $x += $w;
  326. }
  327. return $color;
  328. }
  329. /**
  330. * 画线
  331. *
  332. * @param resource $image
  333. * @param int $width
  334. * @param int $height
  335. * @param int|null $color
  336. */
  337. protected function renderLine($image, $width, $height, $color = null)
  338. {
  339. $color = $color ?: imagecolorallocate($image, mt_rand(100, 255), mt_rand(100, 255), mt_rand(100, 255));
  340. if (mt_rand(0, 1)) {
  341. // 横向
  342. $xA = mt_rand(0, $width / 2);
  343. $yA = mt_rand(0, $height);
  344. $xB = mt_rand($width / 2, $width);
  345. $yB = mt_rand(0, $height);
  346. } else {
  347. // 纵向
  348. $xA = mt_rand(0, $width);
  349. $yA = mt_rand(0, $height / 2);
  350. $xB = mt_rand(0, $width);
  351. $yB = mt_rand($height / 2, $height);
  352. }
  353. imagesetthickness($image, mt_rand(1, 3));
  354. imageline($image, $xA, $yA, $xB, $yB, $color);
  355. }
  356. /**
  357. * 画线
  358. *
  359. * @param resource $image
  360. * @param int $max
  361. * @param int|null $color
  362. */
  363. protected function drawLines($image, $max, $color = null)
  364. {
  365. $square = $this->getConfig('width') * $this->getConfig('height');
  366. $effects = mt_rand($square / 3000, $square / 2000);
  367. // 计算线条数
  368. if ($max != null && $max > 0) {
  369. $effects = min($max, $effects);
  370. }
  371. if ($max !== 0) {
  372. for ($e = 0; $e < $effects; $e++) {
  373. if ($color !== null) {
  374. $this->renderLine($image, $this->getConfig('width'), $this->getConfig('height'), $color);
  375. } else {
  376. $this->renderLine($image, $this->getConfig('width'), $this->getConfig('height'));
  377. }
  378. }
  379. }
  380. }
  381. /**
  382. * 获取颜色
  383. *
  384. * @param resource $image
  385. * @param int $width
  386. * @param int $height
  387. * @param int $background
  388. * @return int
  389. */
  390. protected function getColor($image, $width, $height, $background)
  391. {
  392. $sWidth = imagesx($image);
  393. $sHeight = imagesy($image);
  394. if ($width < 0 || $width >= $sWidth || $height < 0 || $height >= $sHeight) {
  395. return $background;
  396. }
  397. return imagecolorat($image, $width, $height);
  398. }
  399. /**
  400. * @param string $name
  401. * @param array $arguments
  402. * @return $this
  403. */
  404. public function __call($name, $arguments)
  405. {
  406. if (array_key_exists($name, $this->config)) {
  407. $this->config[$name] = $arguments[0];
  408. }
  409. return $this;
  410. }
  411. }