typecho后台登录验证码
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