Plugin.php

<?php
namespace TypechoPlugin\LoginCaptcha;
use Typecho\Common;
use Typecho\Cookie;
use Typecho\Plugin\PluginInterface;
use Typecho\Widget\Helper\Form;
use Typecho\Widget\Helper\Form\Element\Select;
use Typecho\Request as HttpRequest;
use Typecho\Response as HttpResponse;
use Typecho\Widget\Request;
use Typecho\Widget\Response;
use Utils\Helper;
use Widget\Notice;
if (!defined('__TYPECHO_ROOT_DIR__')) exit;
/**
 * Typecho 后台登录页面增加验证码保护
 *
 * @package LoginCaptcha
 * @authors 
 *   Ryan
 *   Yaner
 * @version 2.0.0
 * @link https://doufu.ru
 *       https://typecho.com.mp
 */

class Plugin implements PluginInterface
{
    public static function activate()
    {
        \Typecho\Plugin::factory('index.php')->begin = [__CLASS__, 'hookRoute'];
        Helper::addRoute('login-captcha', '/login-captcha', Action::class, 'renderCaptcha');
        \Typecho\Plugin::factory('admin/footer.php')->end = [__CLASS__, 'addCaptcha'];
    }
    public static function deactivate()
    {
        Helper::removeRoute('login-captcha');
    }
    public static function config(Form $form)
    {
        // 添加验证码类型选择配置
        $captchaType = new Select(
            'captchaType',
            [
                'alphabet' => '纯字母',
                'numeric' => '数字加法',
                'mixed' => '数字汉字加法',
                'random' => '随机'
            ],
            'numeric',
            _t('选择验证码类型')
        );
        $form->addInput($captchaType);
    }
    public static function personalConfig(Form $form)
    {
        // 个人配置项
    }
    public static function hookRoute()
    {
        $request = new Request(HttpRequest::getInstance());
        $response = new Response(HttpRequest::getInstance(), HttpResponse::getInstance());
        $pathinfo = $request->getPathInfo();
        
        // 确保会话已启动
        if (session_status() == PHP_SESSION_NONE) {
            session_start();
        }
        
        // 检查验证码
        if (preg_match("#/action/login#", $pathinfo)) {
            if (!isset($_SESSION['captcha']) || $_POST['captcha'] != $_SESSION['captcha']) {
                Notice::alloc()->set(_t('验证码错误'), 'error');
                Cookie::set('__typecho_remember_captcha', '');
                $response->goBack();
            }
        }
    }
    public static function addCaptcha()
    {
        $request = new Request(HttpRequest::getInstance());
        $pathinfo = $request->getRequestUri();
        $loginPath = Common::url('login.php', defined('__TYPECHO_ADMIN_DIR__') ? __TYPECHO_ADMIN_DIR__ : '/admin/');
        $secureUrl = Helper::security()->getIndex('login-captcha');
        if (stripos($pathinfo, $loginPath) === 0) {
            ?>
            <script>
                (function () {
                    let _src = '<?php echo $secureUrl ?>';
                    const src = _src + (_src.includes('?') ? '&t=' : '?t=');
                    let pwd = document.getElementById('password');
                    pwd?.parentNode?.insertAdjacentHTML('afterend', `<p id="captcha-section">
                        <label class="sr-only" for="captcha"><?php _e("验证码"); ?></label>
                        <input type="text" name="captcha" id="captcha" class="text-l w-100" placeholder="<?php _e("填写答案"); ?>" required />
                        <img id="captcha-img" src="<?php echo $secureUrl ?>" title="<?php _e("点击刷新") ?>" />
                    </p>`);
                    let img = document.getElementById('captcha-img');
                    let timeOut;
                    img?.addEventListener('click', function () {
                        if (img.classList.contains('not-allow')) {
                            return;
                        }
                        img.classList.add('not-allow');
                        img.src = src + Math.random();
                        timeOut = setTimeout(() => {
                            img.classList.remove('not-allow');
                        }, 1000);
                    });
                })()
            </script>
            <style>
                #captcha-section {
                    display: flex;
                }
                #captcha {
                    box-sizing: border-box;
                }
                #captcha:invalid:not(:placeholder-shown) {
                    border: 2px solid red; /* 不符合模式时显示红框 */
                }
                #captcha:valid {
                    border: 2px solid green; /* 符合模式时显示绿框 */
                }
                #captcha-img {
                    cursor: pointer;
                }
                #captcha-img.not-allow {
                    cursor: not-allowed;
                }
            </style>
            <?php
        }
    }
}
?>

Action.php

<?php
namespace TypechoPlugin\LoginCaptcha;
use Typecho\Widget;
use Utils\Helper;
use Widget\ActionInterface;
class Action extends Widget implements ActionInterface
{
private $captchaType;
public function __construct()
{
$options = Helper::options();
$this->captchaType = $options->plugin('LoginCaptcha')->captchaType;
}
// 渲染验证码
public function renderCaptcha()
{
Helper::security()->protect();
if (session_status() == PHP_SESSION_NONE) {
session_start();
}
$captcha = $this->generateCaptcha();
$_SESSION['captcha'] = $captcha['code'];
header('Content-type: image/png');
imagepng($captcha['image']);
imagedestroy($captcha['image']);
exit;
}
// 生成验证码
private function generateCaptcha(): array
{
if ($this->captchaType === 'random') {
$types = ['alphabet', 'numeric', 'mixed'];
$this->captchaType = $types[array_rand($types)]; // 随机选择一种验证码类型
}
switch ($this->captchaType) {
case 'alphabet':
$code = $this->generateAlphabetCode();
break;
case 'numeric':
$code = $this->generateNumericCode();
break;
case 'mixed':
$code = $this->generateMixedCode();
break;
default:
$code = $this->generateNumericCode();
}
$width = 180;
$height = 40;
$image = imagecreatetruecolor($width, $height);
imagealphablending($image, false);
imagesavealpha($image, true);
$background_color = imagecolorallocatealpha($image, 255, 255, 255, 127);
imagefill($image, 0, 0, $background_color);
for ($i = 0; $i < 100; $i++) {
$noise_color = imagecolorallocate($image, rand(100, 255), rand(100, 255), rand(100, 255));
imagesetpixel($image, rand(0, $width), rand(0, $height), $noise_color);
}
for ($i = 0; $i < 5; $i++) {
$line_color = imagecolorallocate($image, rand(100, 255), rand(100, 255), rand(100, 255));
imageline($image, rand(0, $width), rand(0, $height), rand(0, $width), rand(0, $height), $line_color);
}
$x = 10;
$font_size = 20;
$font_path = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'hylxjt.ttf'; // 确保字体文件路径正确
// 将问题拆分为单个字符(包括=和?)
$question = implode('', $code['question']);
$chars = preg_split('//u', $question, -1, PREG_SPLIT_NO_EMPTY);
// 计算总宽度
$totalWidth = 0;
foreach ($chars as $char) {
$textbox = imagettfbbox($font_size, 0, $font_path, $char);
$totalWidth += ($textbox[2] - $textbox[0]) + rand(5, 10);
}
$x = ($width - $totalWidth) / 2;
$y = ($height + $font_size) / 2;
// 绘制问题字符串,每个字符随机颜色
foreach ($chars as $char) {
$char_color = imagecolorallocate($image, rand(0, 255), rand(0, 255), rand(0, 255));
imagettftext($image, $font_size, rand(-30, 30), $x, $y, $char_color, $font_path, $char);
$textbox = imagettfbbox($font_size, 0, $font_path, $char);
$x += ($textbox[2] - $textbox[0]) + rand(5, 10); // 字符之间的随机间隙
}
return array('image' => $image, 'code' => $code['code']);
}
// 生成纯字母验证码
private function generateAlphabetCode(): array
{
$chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
$code = substr(str_shuffle($chars), 0, 4);
return array('question' => str_split($code), 'code' => $code);
}
// 生成纯数字加法验证码
private function generateNumericCode(): array
{
$num1 = rand(0, 9);
$num2 = rand(0, 9);
$code = $num1 + $num2;
$question = "$num1 + $num2";
return array('question' => str_split($question), 'code' => $code);
}
// 生成数字和汉字加法混合验证码
private function generateMixedCode(): array
{
$num1 = rand(0, 9);
$num2 = rand(0, 9);
$code = $num1 + $num2;
$num1Chinese = $this->numberToChinese($num1);
$num2Chinese = $this->numberToChinese($num2);
$rand = rand(0, 2);
if ($rand == 0) {
$question = "$num1 + $num2Chinese";
} elseif ($rand == 1) {
$question = "$num1Chinese + $num2";
} else {
$question = "$num1Chinese + $num2Chinese";
}
return array('question' => str_split($question), 'code' => $code);
}
// 将数字转换为中文
private function numberToChinese($num)
{
$chineseNumbers = array('零', '壹', '贰', '叁', '肆', '伍', '陆', '柒', '捌', '玖');
return $chineseNumbers[$num];
}
public function action()
{
// 空
}
}
?>

增加了验证码类型选项,基本测试通过。
字体文件
hylxjt.zip