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