thinkphp实现图像验证码

news2025/4/20 20:19:49

示例

在这里插入图片描述

服务类 app\common\lib\captcha

<?php
namespace app\common\lib\captcha;

use think\facade\Cache;
use think\facade\Config;
use Exception;

class Captcha
{
    private $im = null; // 验证码图片实例
    private $color = null; // 验证码字体颜色

    // 默认配置
    protected $config = [
        'length'      => 4,      // 验证码位数
        'fontSize'    => 25,     // 字体大小(px)
        'imageH'      => 0,      // 验证码高度
        'imageW'      => 0,      // 验证码宽度
        'useCurve'    => true,   // 是否画混淆曲线
        'useNoise'    => false,  // 是否添加杂点(已禁用)
        'bg'          => [243, 251, 254], // 背景颜色
        'fontttf'     => '',     // 字体文件路径
        'useZh'       => false,  // 使用中文验证码
        'math'        => false,  // 算术验证码
        'alpha'       => 0,      // 透明度(0-127)
        'api'         => false,  // API模式
        'fontPath'    => '',     // 字体文件目录
        'bgPath'      => '',     // 背景图片目录
        'expire'      => 1800,   // 验证码过期时间(s)
    ];

    // 简化后的验证码字符集合(仅数字和大写字母)
    protected $codeSet = '23456789ABCDEFGHJKLMNPQRSTUVWXYZ';

    /**
     * 构造函数
     * @param array $config 配置参数
     */
    public function __construct(array $config = [])
    {
        // 合并配置参数
        $this->config = array_merge($this->config, Config::get('captcha', []), $config);

        // 设置字体路径
        if (empty($this->config['fontPath'])) {
            $this->config['fontPath'] = __DIR__ . '/ttfs/';
        }
    }

    /**
     * 生成验证码
     * @param string $uniqueId 前端传递的唯一标识(如时间戳)
     * @return string 图片二进制内容
     */
    public function create(string $uniqueId = ''): string
    {
        // 清理过期缓存
        $this->clearExpiredCaptchas();

        // 如果未提供 uniqueId,则生成一个默认值
        if (empty($uniqueId)) {
            $uniqueId = uniqid('captcha_');
        }

        // 生成验证码文本
        $generator = $this->generate($uniqueId);

        // 计算图片宽高
        $this->config['imageW'] = $this->config['imageW'] ?: $this->config['length'] * $this->config['fontSize'] * 1.5;
        $this->config['imageH'] = $this->config['imageH'] ?: $this->config['fontSize'] * 2;

        // 创建图片资源
        $this->im = imagecreate((int)$this->config['imageW'], (int)$this->config['imageH']);

        // 设置背景色
        $bgColor = imagecolorallocate(
            $this->im,
            $this->config['bg'][0],
            $this->config['bg'][1],
            $this->config['bg'][2]
        );
        imagefill($this->im, 0, 0, $bgColor);

        // 设置字体颜色
        $this->color = imagecolorallocate($this->im, mt_rand(0, 100), mt_rand(0, 100), mt_rand(0, 100));

        // 添加干扰线
        if ($this->config['useCurve']) {
            $this->writeCurve();
        }

        // 绘制验证码
        $text = str_split($generator['value']);
        $space = $this->config['imageW'] / $this->config['length'];

        foreach ($text as $index => $char) {
            // 计算位置
            $x = $space * $index + mt_rand(5, 10);
            $y = $this->config['imageH'] / 2 + $this->config['fontSize'] / 2;
            $angle = mt_rand(-15, 15);

            imagettftext(
                $this->im,
                (int)$this->config['fontSize'],
                $angle,
                (int)$x,
                (int)$y,
                $this->color,
                $this->getFont(),
                $char
            );
        }

        ob_start();
        imagepng($this->im);
        $content = ob_get_clean();
        imagedestroy($this->im);

        return $content;
    }

    /**
     * 验证验证码
     * @param string $code 用户输入的验证码
     * @param string $uniqueId 前端传递的唯一标识(如时间戳)
     * @return bool
     */
    public function check(string $code, string $uniqueId = ''): bool
    {
        if (empty($uniqueId)) {
            return false;
        }

        // 从 Cache 中获取数据
        $cacheData = Cache::get($uniqueId);

        if (!$cacheData || time() - $cacheData['time'] > $this->config['expire']) {
            $this->removeCaptchaFromRecords($uniqueId);
            return false;
        }

        // 验证码校验
        $result = password_verify(strtoupper($code), $cacheData['key']);

        return $result;
    }

    /**
     * 生成验证码文本
     * @param string $uniqueId 前端传递的唯一标识(如时间戳)
     * @return array ['value' => 显示的文本, 'key' => 加密后的验证码]
     */
    protected function generate(string $uniqueId): array
    {
        $bag = '';
        $characters = str_split($this->codeSet);

        for ($i = 0; $i < $this->config['length']; $i++) {
            $bag .= $characters[random_int(0, count($characters) - 1)];
        }

        $key = strtoupper($bag);

        // 使用 Bcrypt 加密验证码
        $hash = password_hash($key, PASSWORD_BCRYPT, ['cost' => 10]);

        // 将验证码信息存储到 Cache 中
        Cache::set($uniqueId, [
            'key' => $hash,
            'time' => time(),
            // 'raw' => $key // 调试用,正式环境移除
        ], $this->config['expire']);
        
        // 记录到清理队列
        $this->addCaptchaToRecords($uniqueId);

        return ['value' => $bag, 'key' => $hash];
    }

    /**
     * 添加验证码记录到清理队列
     */
    protected function addCaptchaToRecords(string $uniqueId): void
    {
        $records = Cache::get('captcha_records', []);
        $records[$uniqueId] = time() + $this->config['expire'];

        // 限制最大记录数,防止内存占用过大
        if (count($records) > 1000) {
            $records = array_slice($records, -500, null, true);
        }

        Cache::set('captcha_records', $records);
    }

    /**
     * 从清理队列中移除验证码记录
     */
    protected function removeCaptchaFromRecords(string $uniqueId): void
    {
        $records = Cache::get('captcha_records', []);
        unset($records[$uniqueId]);
        Cache::set('captcha_records', $records);
    }

    /**
     * 清理过期的验证码缓存
     */
    protected function clearExpiredCaptchas(): void
    {
        // 每小时清理一次
        $lastClear = Cache::get('last_captcha_clear', 0);
        if (time() - $lastClear < 3600) {
            return;
        }

        $records = Cache::get('captcha_records', []);
        $now = time();
        $cleaned = false;

        foreach ($records as $uid => $expireTime) {
            if ($expireTime < $now) {
                Cache::delete($uid);
                unset($records[$uid]);
                $cleaned = true;
            }
        }

        if ($cleaned) {
            Cache::set('captcha_records', $records);
            Cache::set('last_captcha_clear', time());
        }
    }

    /**
     * 获取字体文件路径
     * @return string
     * @throws Exception
     */
    protected function getFont(): string
    {
        if (!empty($this->config['fontttf'])) {
            return $this->config['fontttf'];
        }

        $fonts = glob($this->config['fontPath'] . '*.ttf') +
                 glob($this->config['fontPath'] . '*.otf');

        if (empty($fonts)) {
            throw new Exception('验证码字体文件不存在,请检查字体路径: ' . $this->config['fontPath']);
        }

        return $fonts[array_rand($fonts)];
    }

    /**
     * 画干扰曲线
     */
    protected function writeCurve(): void
    {
        $px = $py = 0;

        // 曲线前部分
        $A = mt_rand(1, (int)($this->config['imageH'] / 2)); // 振幅
        $b = mt_rand((int)(-$this->config['imageH'] / 4), (int)($this->config['imageH'] / 4)); // Y轴偏移
        $f = mt_rand((int)(-$this->config['imageH'] / 4), (int)($this->config['imageH'] / 4)); // X轴偏移
        $T = mt_rand($this->config['imageH'], $this->config['imageW'] * 2); // 周期
        $w = (2 * M_PI) / $T;

        $px1 = 0; // 起始X坐标
        $px2 = mt_rand((int)($this->config['imageW'] / 2), (int)($this->config['imageW'] * 0.8)); // 结束X坐标

        for ($px = $px1; $px <= $px2; $px++) {
            if ($w != 0) {
                $py = $A * sin($w * $px + $f) + $b + $this->config['imageH'] / 2;
                $i = (int)($this->config['fontSize'] / 5);
                while ($i > 0) {
                    imagesetpixel($this->im, $px + $i, $py + $i, $this->color);
                    $i--;
                }
            }
        }

        // 曲线后部分
        $A = mt_rand(1, (int)($this->config['imageH'] / 2));
        $f = mt_rand((int)(-$this->config['imageH'] / 4), (int)($this->config['imageH'] / 4));
        $T = mt_rand($this->config['imageH'], $this->config['imageW'] * 2);
        $w = (2 * M_PI) / $T;
        $b = $py - $A * sin($w * $px + $f) - $this->config['imageH'] / 2;
        $px1 = $px2;
        $px2 = $this->config['imageW'];

        for ($px = $px1; $px <= $px2; $px++) {
            if ($w != 0) {
                $py = $A * sin($w * $px + $f) + $b + $this->config['imageH'] / 2;
                $i = (int)($this->config['fontSize'] / 5);
                while ($i > 0) {
                    imagesetpixel($this->im, $px + $i, $py + $i, $this->color);
                    $i--;
                }
            }
        }
    }
}

控制器调用

// 生成图形验证码
    public function getCaptcha()
    {
         $uid = 'captcha_' . uniqid('', true);
        $captcha = new \app\common\lib\captcha\Captcha();
        $img = $captcha->create($uid);
    
        return json([
            'image' => 'data:image/png;base64,'.base64_encode($img),
            'uid' => $uid
        ]);
    }
    
    // 校验图形验证码
    public function checkCaptcha()
    {
        $code = input('post.code');
        $uid = input('post.uid');
    
        $captcha = new \app\common\lib\captcha\Captcha();
        $result = $captcha->check($code, $uid);
    
        return json([
            'success' => $result,
            'input_code' => $code,
            'uid' => $uid
        ]);
    }

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2338194.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【k8s系列4】工具介绍

1、虚拟机软件 vmware workstation 2、shell 软件 MobaXterm 3、centos7.9 下载地址 &#xff08;https://mirrors.aliyun.com/centos/7.9.2009/isos/x86_64/?spma2c6h.25603864.0.0.374bf5adOaiFPW&#xff09; 4、上网软件

Spark-SQL核心编程2

路径问题 相对路径与绝对路径&#xff1a;建议使用绝对路径&#xff0c;避免复制粘贴导致的错误&#xff0c;必要时将斜杠改为双反斜杠。 数据处理与展示 SQL 风格语法&#xff1a;创建临时视图并使用 SQL 风格语法查询数据。 DSL 风格语法&#xff1a;使用 DSL 风格语法查询…

STM32单片机入门学习——第41节: [12-1] Unix时间戳

写这个文章是用来学习的,记录一下我的学习过程。希望我能一直坚持下去,我只是一个小白,只是想好好学习,我知道这会很难&#xff0c;但我还是想去做&#xff01; 本文写于&#xff1a;2025.04.18 STM32开发板学习——第41节: [12-1] Unix时间戳 前言开发板说明引用解答和科普一…

无人机自主导航与路径规划技术要点!

一、自主导航与路径规划技术要点 1. 传感器融合 GPS/北斗定位&#xff1a;提供全局定位&#xff0c;但在室内或遮挡环境下易失效。 惯性测量单元&#xff08;IMU&#xff09;**&#xff1a;通过加速度计和陀螺仪实时追踪姿态&#xff0c;弥补GPS信号丢失时的定位空缺。 …

AI绘画SD中,如何保持生成人物角色脸部一致?Stable Diffusion精准控制AI人像一致性两种实用方法教程!

在AI绘画StableDiffusion中&#xff0c;一直都有一个比较困难的问题&#xff0c;就是如何保证每次出图都是同一个人。今天就这个问题分享一些个人实践&#xff0c;大家和我一起来看看吧。 一. 有哪些实现方式 方式1&#xff1a;固定Seed种子值。 固定Seed种子值出来的图片人…

RK3588S开发板将SPI1接口改成GPIO

参考官方教程&#xff1a;ROC-RK3588S-PC 一.基本知识&#xff1a; 1.GPIO引脚计算&#xff1a; ROC-RK3588S-PC 有 5 组 GPIO bank&#xff1a;GPIO0~GPIO4&#xff0c;每组又以 A0~A7, B0~B7, C0~C7, D0~D7 作为编号区分&#xff0c;常用以下公式计算引脚&#xff1a;GPIO…

PLOS ONE:VR 游戏扫描揭示了 ADHD 儿童独特的大脑活动

在孩子的成长过程中&#xff0c;总有那么一些“与众不同”的孩子。他们似乎总是坐不住&#xff0c;课堂上小动作不断&#xff0c;注意力难以集中&#xff0c;作业总是拖拖拉拉……这些行为常常被家长和老师简单地归结为“淘气”“不听话”。然而&#xff0c;他们可能并不只是“…

DemoGen:用于数据高效视觉运动策略学习的合成演示生成

25年2月来自清华、上海姚期智研究院和上海AI实验室的论文“DemoGen: Synthetic Demonstration Generation for Data-Efficient Visuomotor Policy Learning”。 视觉运动策略在机器人操控中展现出巨大潜力&#xff0c;但通常需要大量人工采集的数据才能有效执行。驱动高数据需…

@JsonView + 单一 DTO:如何实现多场景 JSON 字段动态渲染

JsonView 单一 DTO&#xff1a;如何实现多场景 JSON 字段动态渲染 JsonView 单一 DTO&#xff1a;如何实现多场景 JSON 字段动态渲染1、JsonView 注解产生的背景2、为了满足不同场景下返回对应的属性的做法有哪些&#xff1f;2.1 最快速的实现则是针对不同场景新建不同的 DTO…

15 nginx 中默认的 proxy_buffering 导致基于 http 的流式响应存在 buffer, 以 4kb 一批次返回

前言 这也是最近碰到的一个问题 直连 流式 http 服务, 发现 流式响应正常, 0.1 秒接收到一个响应 但是 经过 nginx 代理一层之后, 就发现了 类似于缓冲的效果, 1秒接收到 10个响应 最终 调试 发现是 nginx 的 proxy_buffering 配置引起的 然后 更新 proxy_buffering 为…

安卓手机万能遥控器APP推荐

软件介绍 安卓手机也能当“家电总控台”&#xff1f;这款小米旗下的万能遥控器APP&#xff0c;直接把遥控器做成“傻瓜式操作”——不用配对&#xff0c;不连蓝牙&#xff0c;点开就能操控电视、空调、机顶盒&#xff0c;甚至其他品牌的电器&#xff01;雷总这波操作直接封神&…

PH热榜 | 2025-04-18

1. Wiza Monitor 标语&#xff1a;跟踪工作变动&#xff0c;接收Slack和电子邮件的提醒。 介绍&#xff1a;Wiza Monitor是一款用于追踪职位变动的工具&#xff0c;可以实时跟踪客户和潜在客户的工作变动&#xff0c;还可以通过电子邮件和Slack发送提醒&#xff0c;让你的客户…

Android平台 Hal AIDL 系列文章目录

目录 1. Android Hal AIDL 简介2. AIDL 语言简介3. Android 接口定义语言 (AIDL)4. 定义AIDL 接口5. AIDL 中如何传递 Parcelable 对象6. 如何使用AIDL 定义的远程接口进行跨进程通信7. 适用于 HAL 的 AIDL8. Android Hal AIDL 编译调试9. 高版本Android (AIDL HAL) 沿用HIDL方…

十、数据库day02--SQL语句01

文章目录 一、新建查询1.查询窗口的开启方法2. 单语句运行方法 二、数据库操作1.创建数据库2. 使用数据库3. 修改数据库4. 删除数据库和查看所有数据库5. 重点&#xff1a;数据库备份5.1 应用场景5.2 利用工具备份备份操作还原操作 5.3 扩展&#xff1a;使用命令备份 三、数据表…

实时直播弹幕系统设计

整个服务读多写少&#xff0c;读写比例大概几百比1. 如果实时性要求高的话&#xff0c;可以采用长连接模式&#xff08;轮询的话&#xff0c;时效性不好&#xff0c;同时对于评论少的直播间可能空转&#xff09; websocket 和 SSE架构 只要求服务端推送的话&#xff0c;可以…

[Java · 初窥门径] Java 语言初识

&#x1f31f; 想系统化学习 Java 编程&#xff1f;看看这个&#xff1a;[编程基础] Java 学习手册 0x01&#xff1a;Java 编程语言简介 Java 是一种高级计算机编程语言&#xff0c;它是由 Sun Microsystems 公司&#xff08;已被 Oracle 公司收购&#xff09;于 1995 年 5 …

【SQL Server】数据探查工具1.0研发可行性方案

&#x1f449; 点击关注不迷路 &#x1f449; 点击关注不迷路 &#x1f449; 点击关注不迷路 想抢先解锁数据自由的宝子&#xff0c;速速戳我&#xff01;评论区蹲一波 “蹲蹲”&#xff0c;揪人唠唠你的超实用需求&#xff01; 【SQL Server】数据探查工具1.0研发可行性方案…

谓词——C++

1.一元谓词 1.定义 2.案例 查找容器有没有大于五的数字 #include<stdio.h> using namespace std; #include<string> #include<vector> #include<set> #include <iostream> class myfind { public:bool operator()(int a){return a > 5;} …

『前端样式分享』联系我们卡片式布局 自适应屏幕 hover动效 在wikijs中使用 (代码拿来即用)

目录 预览效果分析要点响应式网格布局卡片样式&#xff1a;阴影和过渡效果 代码优化希望 长短不一的邮箱地址在左右居中的同时,做到左侧文字对齐(wikijs可用)总结 欢迎关注 『前端布局样式』 专栏&#xff0c;持续更新中 欢迎关注 『前端布局样式』 专栏&#xff0c;持续更新中…

MySQL 缓存机制全解析:从磁盘 I/O 到性能优化

MySQL 缓存机制全解析&#xff1a;从磁盘 I/O 到性能优化 MySQL 的缓存机制是提升数据库性能的关键部分&#xff0c;它通过多级缓存减少磁盘 I/O 和计算开销&#xff0c;从而提高查询和写入的效率。 1. 为什么需要缓存&#xff1f; 数据库的性能瓶颈通常集中在磁盘 I/O 上。…