|
@@ -0,0 +1,470 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace Tool\Captcha\Src;
|
|
|
+
|
|
|
+
|
|
|
+class Captcha
|
|
|
+{
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 验证码配置
|
|
|
+ * @var array
|
|
|
+ */
|
|
|
+ protected $config = [
|
|
|
+ /**
|
|
|
+ * 调试模型
|
|
|
+ */
|
|
|
+ 'debug' => false,
|
|
|
+ /**
|
|
|
+ * 默认验证码长度
|
|
|
+ * @var int
|
|
|
+ */
|
|
|
+ 'length' => 4,
|
|
|
+ /**
|
|
|
+ * 验证码字符集
|
|
|
+ * @var string
|
|
|
+ */
|
|
|
+ 'charset' => 'abcdefghijklmnpqrstuvwxyzABCDEFGHIJKLMNPQRSTUVWXYZ123456789',
|
|
|
+ /**
|
|
|
+ * 是否开启严格模式(区分大小写)
|
|
|
+ * @var bool
|
|
|
+ */
|
|
|
+ 'strict' => false,
|
|
|
+ /**
|
|
|
+ * 默认验证码宽度
|
|
|
+ * @var int
|
|
|
+ */
|
|
|
+ 'width' => 150,
|
|
|
+ /**
|
|
|
+ * 默认验证码高度
|
|
|
+ * @var int
|
|
|
+ */
|
|
|
+ 'height' => 40,
|
|
|
+ /**
|
|
|
+ * 指定文字颜色
|
|
|
+ * @var string
|
|
|
+ */
|
|
|
+ 'textColor' => null,
|
|
|
+ /**
|
|
|
+ * 文字字体文件
|
|
|
+ * @var string
|
|
|
+ */
|
|
|
+ 'textFont' => null,
|
|
|
+ /**
|
|
|
+ * 指定图片背景色
|
|
|
+ * @var string
|
|
|
+ */
|
|
|
+ 'backgroundColor' => null,
|
|
|
+ /**
|
|
|
+ * 开启失真模式
|
|
|
+ * @var bool
|
|
|
+ */
|
|
|
+ 'distortion' => true,
|
|
|
+ /**
|
|
|
+ * 最大前景线条数
|
|
|
+ * @var int
|
|
|
+ */
|
|
|
+ 'maxFrontLines' => null,
|
|
|
+ /**
|
|
|
+ * 最大背景线条数
|
|
|
+ * @val int
|
|
|
+ */
|
|
|
+ 'maxBehindLines' => null,
|
|
|
+ /**
|
|
|
+ * 文字最大角度
|
|
|
+ * @var int
|
|
|
+ */
|
|
|
+ 'maxAngle' => 8,
|
|
|
+ /**
|
|
|
+ * 文字最大偏移量
|
|
|
+ * @var int
|
|
|
+ */
|
|
|
+ 'maxOffset' => 5
|
|
|
+ ];
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Captcha constructor.
|
|
|
+ * @param array $config
|
|
|
+ */
|
|
|
+ public function __construct(array $config = [])
|
|
|
+ {
|
|
|
+ $this->setConfig($config);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 设置验证码配置
|
|
|
+ *
|
|
|
+ * @param array|string $config 配置数组或配置项key
|
|
|
+ * @param mixed $value 配置项值
|
|
|
+ * @return $this
|
|
|
+ */
|
|
|
+ public function setConfig($config, $value = null)
|
|
|
+ {
|
|
|
+
|
|
|
+ if (!is_array($config)) {
|
|
|
+ $config = [$config => $value];
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($config as $key => $value) {
|
|
|
+ if (array_key_exists($key, $this->config)) {
|
|
|
+ $this->config[$key] = $value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $this;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取配置
|
|
|
+ *
|
|
|
+ * @param string|null $key 配置项key
|
|
|
+ * @return string|number|array
|
|
|
+ */
|
|
|
+ public function getConfig($key = null)
|
|
|
+ {
|
|
|
+ if ($key !== null) {
|
|
|
+ return $this->config[$key];
|
|
|
+ }
|
|
|
+
|
|
|
+ return $this->config;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成验证码
|
|
|
+ *
|
|
|
+ * @return Image
|
|
|
+ */
|
|
|
+ public function make($key)
|
|
|
+ {
|
|
|
+ $code = $this->generate();
|
|
|
+
|
|
|
+ $hash = password_hash($code, PASSWORD_BCRYPT, array('cost' => 10));
|
|
|
+
|
|
|
+ if ($hash === false) {
|
|
|
+ throw new \RuntimeException('Bcrypt hashing not supported.');
|
|
|
+ }
|
|
|
+
|
|
|
+ app("redis")->setex($key,180,$hash);
|
|
|
+ return new Image($this->build($code));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 仅测试正确性, 不删除验证码
|
|
|
+ * @param $key string
|
|
|
+ * @param string $input
|
|
|
+ * @return bool
|
|
|
+ */
|
|
|
+ public function test($key,$input)
|
|
|
+ {
|
|
|
+
|
|
|
+ $code = app("redis")->get($key);
|
|
|
+
|
|
|
+ if ($this->config['strict']) {
|
|
|
+ // 开启严格模式
|
|
|
+ password_verify($input, $code);
|
|
|
+ }
|
|
|
+
|
|
|
+ //返回验证结果
|
|
|
+ return password_verify(strtoupper($input), $code);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检测正确性,并删除验证码
|
|
|
+ *
|
|
|
+ * @param string $key
|
|
|
+ * @param string $input
|
|
|
+ * @return bool
|
|
|
+ */
|
|
|
+ public function check($key,$input)
|
|
|
+ {
|
|
|
+ $result = $this->test($key,$input);
|
|
|
+ app("redis")->del($key);
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成验证码
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ protected function generate()
|
|
|
+ {
|
|
|
+ $characters = str_split($this->getConfig('charset'));
|
|
|
+ $length = $this->getConfig('length');
|
|
|
+
|
|
|
+ $code = '';
|
|
|
+ for ($i = 0; $i < $length; $i++) {
|
|
|
+ $code .= $characters[rand(0, count($characters) - 1)];
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($this->config['strict']) {
|
|
|
+ return $code;
|
|
|
+ }
|
|
|
+
|
|
|
+ return strtoupper($code);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建验证码图片
|
|
|
+ *
|
|
|
+ * @param string $code
|
|
|
+ * @return resource
|
|
|
+ */
|
|
|
+ protected function build($code)
|
|
|
+ {
|
|
|
+
|
|
|
+ // 图片宽
|
|
|
+ $width = $this->getConfig('width');
|
|
|
+ // 图片高
|
|
|
+ $height = $this->getConfig('height');
|
|
|
+ // 背景颜色
|
|
|
+ $backgroundColor = $this->getConfig('backgroundColor');
|
|
|
+
|
|
|
+ // 随机取一个字体
|
|
|
+ $font = $this->getTextFont();
|
|
|
+
|
|
|
+ // 根据宽高创建一个背景画布
|
|
|
+ $image = imagecreatetruecolor($width, $height);
|
|
|
+
|
|
|
+ if ($backgroundColor === null) {
|
|
|
+ $backgroundColor = imagecolorallocate($image, mt_rand(200, 255), mt_rand(200, 255), mt_rand(200, 255));
|
|
|
+ } else {
|
|
|
+ $color = $backgroundColor;
|
|
|
+ $backgroundColor = imagecolorallocate($image, $color[0], $color[1], $color[2]);
|
|
|
+ }
|
|
|
+ // 填充背景色
|
|
|
+ imagefill($image, 0, 0, $backgroundColor);
|
|
|
+
|
|
|
+ // 绘制背景干扰线
|
|
|
+ $this->drawLines($image, $this->getConfig('maxBehindLines'));
|
|
|
+
|
|
|
+ // 写入验证码文字
|
|
|
+ $color = $this->renderText($image, $code, $font);
|
|
|
+
|
|
|
+ // 绘制前景干扰线
|
|
|
+ $this->drawLines($image, $this->getConfig('maxFrontLines'), $color);
|
|
|
+
|
|
|
+ if ($this->getConfig('distortion')) {
|
|
|
+ // 创建失真
|
|
|
+ $image = $this->createDistortion($image, $width, $height, $backgroundColor);
|
|
|
+ }
|
|
|
+
|
|
|
+ //如果不指定字体颜色和背景颜色,则使用图像过滤器修饰
|
|
|
+ if (function_exists('imagefilter') && is_null($backgroundColor) && is_null($this->getConfig('textColor'))) {
|
|
|
+ // 颜色翻转 - 1/2几率
|
|
|
+ if (mt_rand(0, 1) == 0) {
|
|
|
+ imagefilter($image, IMG_FILTER_NEGATE);
|
|
|
+ }
|
|
|
+ // 用边缘检测来突出图像的边缘 - 1/11几率
|
|
|
+ if (mt_rand(0, 10) == 0) {
|
|
|
+ imagefilter($image, IMG_FILTER_EDGEDETECT);
|
|
|
+ }
|
|
|
+ // 改变图像的对比度
|
|
|
+ imagefilter($image, IMG_FILTER_CONTRAST, mt_rand(-50, 10));
|
|
|
+
|
|
|
+ if (mt_rand(0, 5) == 0) {
|
|
|
+ // 用高斯算法和指定颜色模糊图像
|
|
|
+ imagefilter($image, IMG_FILTER_COLORIZE, mt_rand(-80, 50), mt_rand(-80, 50), mt_rand(-80, 50));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return $image;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建失真
|
|
|
+ *
|
|
|
+ * @param resource $image
|
|
|
+ * @param int $width
|
|
|
+ * @param int $height
|
|
|
+ * @param int $backgroundColor
|
|
|
+ * @return resource
|
|
|
+ */
|
|
|
+ protected function createDistortion($image, $width, $height, $backgroundColor)
|
|
|
+ {
|
|
|
+ //创建失真
|
|
|
+ $contents = imagecreatetruecolor($width, $height);
|
|
|
+ $rWidth = mt_rand(0, $width);
|
|
|
+ $rHeight = mt_rand(0, $height);
|
|
|
+ $phase = mt_rand(0, 10);
|
|
|
+ $scale = 1.1 + mt_rand(0, 10000) / 30000;
|
|
|
+
|
|
|
+ for ($x = 0; $x < $width; $x++) {
|
|
|
+ for ($y = 0; $y < $height; $y++) {
|
|
|
+ $vX = $x - $rWidth;
|
|
|
+ $vY = $y - $rHeight;
|
|
|
+ $vN = sqrt($vX * $vX + $vY * $vY);
|
|
|
+
|
|
|
+ if ($vN != 0) {
|
|
|
+ $vN2 = $vN + 4 * sin($vN / 30);
|
|
|
+ $nX = $rWidth + ($vX * $vN2 / $vN);
|
|
|
+ $nY = $rHeight + ($vY * $vN2 / $vN);
|
|
|
+ } else {
|
|
|
+ $nX = $rWidth;
|
|
|
+ $nY = $rHeight;
|
|
|
+ }
|
|
|
+ $nY = $nY + $scale * sin($phase + $nX * 0.2);
|
|
|
+
|
|
|
+ $pixel = $this->getColor($image, round($nX), round($nY), $backgroundColor);
|
|
|
+
|
|
|
+ if ($pixel == 0) {
|
|
|
+ $pixel = $backgroundColor;
|
|
|
+ }
|
|
|
+
|
|
|
+ imagesetpixel($contents, $x, $y, $pixel);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $contents;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取一个字体
|
|
|
+ *
|
|
|
+ * @return string
|
|
|
+ */
|
|
|
+ protected function getTextFont()
|
|
|
+ {
|
|
|
+ // 指定字体
|
|
|
+ if ($this->getConfig('textFont') && file_exists($this->getConfig('textFont'))) {
|
|
|
+ return $this->getConfig('textFont');
|
|
|
+ }
|
|
|
+ // 随机字体
|
|
|
+ return __DIR__ . '/../fonts/' . mt_rand(0, 5) . '.ttf';
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 写入验证码到图片中
|
|
|
+ *
|
|
|
+ * @param resource $image
|
|
|
+ * @param string $phrase
|
|
|
+ * @param string $font
|
|
|
+ * @return int
|
|
|
+ */
|
|
|
+ protected function renderText($image, $phrase, $font)
|
|
|
+ {
|
|
|
+ $length = strlen($phrase);
|
|
|
+ if ($length === 0) {
|
|
|
+ return imagecolorallocate($image, 0, 0, 0);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 计算文字尺寸
|
|
|
+ $size = $this->getConfig('width') / $length - mt_rand(0, 3) - 1;
|
|
|
+ $box = imagettfbbox($size, 0, $font, $phrase);
|
|
|
+ $textWidth = $box[2] - $box[0];
|
|
|
+ $textHeight = $box[1] - $box[7];
|
|
|
+ $x = ($this->getConfig('width') - $textWidth) / 2;
|
|
|
+ $y = ($this->getConfig('height') - $textHeight) / 2 + $size;
|
|
|
+
|
|
|
+ if (!$this->getConfig('textColor')) {
|
|
|
+ $textColor = array(mt_rand(0, 150), mt_rand(0, 150), mt_rand(0, 150));
|
|
|
+ } else {
|
|
|
+ $textColor = $this->getConfig('textColor');
|
|
|
+ }
|
|
|
+ $color = imagecolorallocate($image, $textColor[0], $textColor[1], $textColor[2]);
|
|
|
+
|
|
|
+ // 循环写入字符,随机角度
|
|
|
+ for ($i = 0; $i < $length; $i++) {
|
|
|
+ $box = imagettfbbox($size, 0, $font, $phrase[$i]);
|
|
|
+ $w = $box[2] - $box[0];
|
|
|
+ $angle = mt_rand(-$this->getConfig('maxAngle'), $this->getConfig('maxAngle'));
|
|
|
+ $offset = mt_rand(-$this->getConfig('maxOffset'), $this->getConfig('maxOffset'));
|
|
|
+ imagettftext($image, $size, $angle, $x, $y + $offset, $color, $font, $phrase[$i]);
|
|
|
+ $x += $w;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $color;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 画线
|
|
|
+ *
|
|
|
+ * @param resource $image
|
|
|
+ * @param int $width
|
|
|
+ * @param int $height
|
|
|
+ * @param int|null $color
|
|
|
+ */
|
|
|
+ protected function renderLine($image, $width, $height, $color = null)
|
|
|
+ {
|
|
|
+ $color = $color ?: imagecolorallocate($image, mt_rand(100, 255), mt_rand(100, 255), mt_rand(100, 255));
|
|
|
+
|
|
|
+ if (mt_rand(0, 1)) {
|
|
|
+ // 横向
|
|
|
+ $xA = mt_rand(0, $width / 2);
|
|
|
+ $yA = mt_rand(0, $height);
|
|
|
+ $xB = mt_rand($width / 2, $width);
|
|
|
+ $yB = mt_rand(0, $height);
|
|
|
+ } else {
|
|
|
+ // 纵向
|
|
|
+ $xA = mt_rand(0, $width);
|
|
|
+ $yA = mt_rand(0, $height / 2);
|
|
|
+ $xB = mt_rand(0, $width);
|
|
|
+ $yB = mt_rand($height / 2, $height);
|
|
|
+ }
|
|
|
+ imagesetthickness($image, mt_rand(1, 3));
|
|
|
+ imageline($image, $xA, $yA, $xB, $yB, $color);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 画线
|
|
|
+ *
|
|
|
+ * @param resource $image
|
|
|
+ * @param int $max
|
|
|
+ * @param int|null $color
|
|
|
+ */
|
|
|
+ protected function drawLines($image, $max, $color = null)
|
|
|
+ {
|
|
|
+ $square = $this->getConfig('width') * $this->getConfig('height');
|
|
|
+ $effects = mt_rand($square / 3000, $square / 2000);
|
|
|
+
|
|
|
+ // 计算线条数
|
|
|
+ if ($max != null && $max > 0) {
|
|
|
+ $effects = min($max, $effects);
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($max !== 0) {
|
|
|
+ for ($e = 0; $e < $effects; $e++) {
|
|
|
+
|
|
|
+ if ($color !== null) {
|
|
|
+ $this->renderLine($image, $this->getConfig('width'), $this->getConfig('height'), $color);
|
|
|
+ } else {
|
|
|
+ $this->renderLine($image, $this->getConfig('width'), $this->getConfig('height'));
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取颜色
|
|
|
+ *
|
|
|
+ * @param resource $image
|
|
|
+ * @param int $width
|
|
|
+ * @param int $height
|
|
|
+ * @param int $background
|
|
|
+ * @return int
|
|
|
+ */
|
|
|
+ protected function getColor($image, $width, $height, $background)
|
|
|
+ {
|
|
|
+ $sWidth = imagesx($image);
|
|
|
+ $sHeight = imagesy($image);
|
|
|
+ if ($width < 0 || $width >= $sWidth || $height < 0 || $height >= $sHeight) {
|
|
|
+ return $background;
|
|
|
+ }
|
|
|
+
|
|
|
+ return imagecolorat($image, $width, $height);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @param string $name
|
|
|
+ * @param array $arguments
|
|
|
+ * @return $this
|
|
|
+ */
|
|
|
+ public function __call($name, $arguments)
|
|
|
+ {
|
|
|
+ if (array_key_exists($name, $this->config)) {
|
|
|
+ $this->config[$name] = $arguments[0];
|
|
|
+ }
|
|
|
+
|
|
|
+ return $this;
|
|
|
+ }
|
|
|
+}
|