typecho极验验证插件

刚开始本地搭建好typecho的博客后,发现后台登录竟然没有验证码,太不安全了,加上typecho支持插件式开发,就自己动手做个吧。

  1. 采用比较流行的极验验证作为验证码,先下载sdk,放到tpecho插件目录下:
/
/usr/plugins/Geetest
/usr/plugins/Geetest/Plugin.php
/usr/plugins/Geetest/lib/class.geetestlib.php
  1. Plugin.php:
<?php
/**
 * 极客登录验证码插件 for Typecho
 *
 * @package 极客登录验证码插件
 * @author 没那么简单
 * @version 1.0.0
 * @link http://nsimple.top
 */

if (!defined('__TYPECHO_ROOT_DIR__')) exit;

require 'lib/class.geetestlib.php';

class Geetest_Plugin implements Typecho_Plugin_Interface
{
    /**
     * 激活插件方法,如果激活失败,直接抛出异常
     *
     * @access public
     * @return void
     * @throws Typecho_Plugin_Exception
     */
    public static function activate()
    {
        Typecho_Plugin::factory('gt')->render = array('Geetest_Plugin', 'render');
        Typecho_Plugin::factory('gt')->server = array('Geetest_Plugin', 'server');
        Typecho_Plugin::factory('gt')->verify= array('Geetest_Plugin', 'verify');
    }

    /**
     * 禁用插件方法,如果禁用失败,直接抛出异常
     *
     * @static
     * @access public
     * @return void
     * @throws Typecho_Plugin_Exception
     */
    public static function deactivate(){}

    /**
     * 获取插件配置面板
     *
     * @access public
     * @param Typecho_Widget_Helper_Form $form 配置面板
     * @return void
     */
    public static function config(Typecho_Widget_Helper_Form $form)
    {
        /** 极验验证配置 */
        $geetest_id = new Typecho_Widget_Helper_Form_Element_Text('geetest_id', NULL, '', _t('极验验证ID'));
        $geetest_key = new Typecho_Widget_Helper_Form_Element_Text('geetest_key', NULL, '', _t('极验验证Key'));
        $types = array(
            'float' => '浮动式',
            'embed' => '嵌入式'
        );
        $geetest_type = new Typecho_Widget_Helper_Form_Element_Select('geetest_type', $types, 'float', _t('极验验证类型'));
        $form->addInput($geetest_id);
        $form->addInput($geetest_key);
        $form->addInput($geetest_type);
    }

    /**
     * 个人用户的配置面板
     *
     * @access public
     * @param Typecho_Widget_Helper_Form $form
     * @return void
     */
    public static function personalConfig(Typecho_Widget_Helper_Form $form){}

    /**
     * 插件实现方法
     *
     * @access public
     * @return void
     */
    public static function render()
    {
        $config = Typecho_Widget::widget('Widget_Options')->plugin('Geetest');
        echo <<<EOT
            <div id="captcha"></div>
            <script src="http://static.geetest.com/static/tools/gt.js"></script>
            <script> var type = '{$config->geetest_type}';</script>
EOT;
    }

    /**
     * 输出验证geetest服务器响应字符串
     */
    public static function server()
    {
        $config = Typecho_Widget::widget('Widget_Options')->plugin('Geetest');
        $GtSdk = new GeetestLib($config->geetest_id, $config->geetest_key);
        @session_start();
        $status = $GtSdk->pre_process();
        $_SESSION['gtServer'] = $status;
        echo $GtSdk->get_response_str();
    }

    /**
     * 验证行为是否合法
     *
     * @param array $data
     * @return string
     */
    public function verify($data = array())
    {
        @session_start();
        $config = Typecho_Widget::widget('Widget_Options')->plugin('Geetest');
        $GtSdk = new GeetestLib($config->geetest_id, $config->geetest_key);
        if (empty($data['geetest_challenge']) || empty($data['geetest_validate']) && empty($data['geetest_seccode'])) {
            return 'empty';
        }
        if ($_SESSION['gtServer'] == 1) {
            $result = $GtSdk->success_validate($data['geetest_challenge'], $data['geetest_validate'], $data['geetest_seccode']);
            if ($result) {
                return 'success';
            } else {
                return 'failed';
            }
        }else{
            if ($GtSdk->fail_validate($data['geetest_challenge'], $data['geetest_validate'], $data['geetest_seccode'])) {
                return 'success';
            } else {
                return 'down';
            }
        }
    }
}

  1. lib/class.geetestlib.php这个文件是官网下载的,不需要修改:
<?php
/**
 * 极验行为式验证安全平台,php 网站主后台包含的库文件
 *
 * @author Tanxu
 */
class GeetestLib {
    const GT_SDK_VERSION = 'php_3.2.0';
    public static $connectTimeout = 1;
    public static $socketTimeout  = 1;
    private $response;
    public function __construct($captcha_id, $private_key) {
        $this->captcha_id  = $captcha_id;
        $this->private_key = $private_key;
    }
    /**
     * 判断极验服务器是否down机
     *
     * @param null $user_id
     * @return int
     */
    public function pre_process($user_id = null) {
        $url = "http://api.geetest.com/register.php?gt=" . $this->captcha_id;
        if (($user_id != null) and (is_string($user_id))) {
            $url = $url . "&user_id=" . $user_id;
        }
        $challenge = $this->send_request($url);
        if (strlen($challenge) != 32) {
            $this->failback_process();
            return 0;
        }
        $this->success_process($challenge);
        return 1;
    }
    /**
     * @param $challenge
     */
    private function success_process($challenge) {
        $challenge      = md5($challenge . $this->private_key);
        $result         = array(
            'success'   => 1,
            'gt'        => $this->captcha_id,
            'challenge' => $challenge
        );
        $this->response = $result;
    }
    /**
     *
     */
    private function failback_process() {
        $rnd1           = md5(rand(0, 100));
        $rnd2           = md5(rand(0, 100));
        $challenge      = $rnd1 . substr($rnd2, 0, 2);
        $result         = array(
            'success'   => 0,
            'gt'        => $this->captcha_id,
            'challenge' => $challenge
        );
        $this->response = $result;
    }
    /**
     * @return mixed
     */
    public function get_response_str() {
        return json_encode($this->response);
    }
    /**
     * 返回数组方便扩展
     *
     * @return mixed
     */
    public function get_response() {
        return $this->response;
    }
    /**
     * 正常模式获取验证结果
     *
     * @param      $challenge
     * @param      $validate
     * @param      $seccode
     * @param null $user_id
     * @return int
     */
    public function success_validate($challenge, $validate, $seccode, $user_id = null) {
        if (!$this->check_validate($challenge, $validate)) {
            return 0;
        }
        $data = array(
            "seccode" => $seccode,
            "sdk"     => self::GT_SDK_VERSION,
        );
        if (($user_id != null) and (is_string($user_id))) {
            $data["user_id"] = $user_id;
        }
        $url          = "http://api.geetest.com/validate.php";
        $codevalidate = $this->post_request($url, $data);
        if ($codevalidate == md5($seccode)) {
            return 1;
        } else {
            if ($codevalidate == "false") {
                return 0;
            } else {
                return 0;
            }
        }
    }
    /**
     * 宕机模式获取验证结果
     *
     * @param $challenge
     * @param $validate
     * @param $seccode
     * @return int
     */
    public function fail_validate($challenge, $validate, $seccode) {
        if ($validate) {
            $value   = explode("_", $validate);
            $ans     = $this->decode_response($challenge, $value['0']);
            $bg_idx  = $this->decode_response($challenge, $value['1']);
            $grp_idx = $this->decode_response($challenge, $value['2']);
            $x_pos   = $this->get_failback_pic_ans($bg_idx, $grp_idx);
            $answer  = abs($ans - $x_pos);
            if ($answer < 4) {
                return 1;
            } else {
                return 0;
            }
        } else {
            return 0;
        }
    }
    /**
     * @param $challenge
     * @param $validate
     * @return bool
     */
    private function check_validate($challenge, $validate) {
        if (strlen($validate) != 32) {
            return false;
        }
        if (md5($this->private_key . 'geetest' . $challenge) != $validate) {
            return false;
        }
        return true;
    }
    /**
     * GET 请求
     *
     * @param $url
     * @return mixed|string
     */
    private function send_request($url) {
        if (function_exists('curl_exec')) {
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::$connectTimeout);
            curl_setopt($ch, CURLOPT_TIMEOUT, self::$socketTimeout);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            $data = curl_exec($ch);
            if (curl_errno($ch)) {
                $err = sprintf("curl[%s] error[%s]", $url, curl_errno($ch) . ':' . curl_error($ch));
                $this->triggerError($err);
            }
            curl_close($ch);
        } else {
            $opts    = array(
                'http' => array(
                    'method'  => "GET",
                    'timeout' => self::$connectTimeout + self::$socketTimeout,
                )
            );
            $context = stream_context_create($opts);
            $data    = file_get_contents($url, false, $context);
        }
        return $data;
    }
    /**
     *
     * @param       $url
     * @param array $postdata
     * @return mixed|string
     */
    private function post_request($url, $postdata = '') {
        if (!$postdata) {
            return false;
        }
        $data = http_build_query($postdata);
        if (function_exists('curl_exec')) {
            $ch = curl_init();
            curl_setopt($ch, CURLOPT_URL, $url);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, self::$connectTimeout);
            curl_setopt($ch, CURLOPT_TIMEOUT, self::$socketTimeout);
            //不可能执行到的代码
            if (!$postdata) {
                curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
            } else {
                curl_setopt($ch, CURLOPT_POST, 1);
                curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
            }
            $data = curl_exec($ch);
            if (curl_errno($ch)) {
                $err = sprintf("curl[%s] error[%s]", $url, curl_errno($ch) . ':' . curl_error($ch));
                $this->triggerError($err);
            }
            curl_close($ch);
        } else {
            if ($postdata) {
                $opts    = array(
                    'http' => array(
                        'method'  => 'POST',
                        'header'  => "Content-type: application/x-www-form-urlencoded\r\n" . "Content-Length: " . strlen($data) . "\r\n",
                        'content' => $data,
                        'timeout' => self::$connectTimeout + self::$socketTimeout
                    )
                );
                $context = stream_context_create($opts);
                $data    = file_get_contents($url, false, $context);
            }
        }
        return $data;
    }
    /**
     * 解码随机参数
     *
     * @param $challenge
     * @param $string
     * @return int
     */
    private function decode_response($challenge, $string) {
        if (strlen($string) > 100) {
            return 0;
        }
        $key             = array();
        $chongfu         = array();
        $shuzi           = array("0" => 1, "1" => 2, "2" => 5, "3" => 10, "4" => 50);
        $count           = 0;
        $res             = 0;
        $array_challenge = str_split($challenge);
        $array_value     = str_split($string);
        for ($i = 0; $i < strlen($challenge); $i++) {
            $item = $array_challenge[$i];
            if (in_array($item, $chongfu)) {
                continue;
            } else {
                $value = $shuzi[$count % 5];
                array_push($chongfu, $item);
                $count++;
                $key[$item] = $value;
            }
        }
        for ($j = 0; $j < strlen($string); $j++) {
            $res += $key[$array_value[$j]];
        }
        $res = $res - $this->decodeRandBase($challenge);
        return $res;
    }
    /**
     * @param $x_str
     * @return int
     */
    private function get_x_pos_from_str($x_str) {
        if (strlen($x_str) != 5) {
            return 0;
        }
        $sum_val   = 0;
        $x_pos_sup = 200;
        $sum_val   = base_convert($x_str, 16, 10);
        $result    = $sum_val % $x_pos_sup;
        $result    = ($result < 40) ? 40 : $result;
        return $result;
    }
    /**
     * @param $full_bg_index
     * @param $img_grp_index
     * @return int
     */
    private function get_failback_pic_ans($full_bg_index, $img_grp_index) {
        $full_bg_name = substr(md5($full_bg_index), 0, 9);
        $bg_name      = substr(md5($img_grp_index), 10, 9);
        $answer_decode = "";
        // 通过两个字符串奇数和偶数位拼接产生答案位
        for ($i = 0; $i < 9; $i++) {
            if ($i % 2 == 0) {
                $answer_decode = $answer_decode . $full_bg_name[$i];
            } elseif ($i % 2 == 1) {
                $answer_decode = $answer_decode . $bg_name[$i];
            }
        }
        $x_decode = substr($answer_decode, 4, 5);
        $x_pos    = $this->get_x_pos_from_str($x_decode);
        return $x_pos;
    }
    /**
     * 输入的两位的随机数字,解码出偏移量
     *
     * @param $challenge
     * @return mixed
     */
    private function decodeRandBase($challenge) {
        $base      = substr($challenge, 32, 2);
        $tempArray = array();
        for ($i = 0; $i < strlen($base); $i++) {
            $tempAscii = ord($base[$i]);
            $result    = ($tempAscii > 57) ? ($tempAscii - 87) : ($tempAscii - 48);
            array_push($tempArray, $result);
        }
        $decodeRes = $tempArray['0'] * 36 + $tempArray['1'];
        return $decodeRes;
    }
    /**
     * @param $err
     */
    private function triggerError($err) {
        trigger_error($err);
    }
}
  1. sdk包中其他文件不需要了,配置文件已做成后台插件配置形式.
  2. 增加/admin/geetest-code.php文件,用来返回极验服务响应数据:
<?php
if (!defined('__DIR__')) {
    define('__DIR__', dirname(__FILE__));
}

define('__TYPECHO_ADMIN__', true);

/** 载入配置文件 */
if (!defined('__TYPECHO_ROOT_DIR__') && !@include_once __DIR__ . '/../config.inc.php') {
    file_exists(__DIR__ . '/../install.php') ? header('Location: ../install.php') : print('Missing Config File');
    exit;
}

/** 初始化组件 */
Typecho_Widget::widget('Widget_Init');

/** 如果插件已启用, 则输出极验服务器响应数据 */
/** 之前旧版本写法(已废弃)
$exists = Typecho_Plugin::exists('Geetest');
if(false !== $exists) {
    Typecho_Plugin::factory('gt')->server();
}
**/
if(!empty(Helper::options()->plugins['activated']['Geetest'])) {
    Typecho_Plugin::factory('gt')->server();
}

  1. 把插件嵌入到登录页面(放到密码框和提交按钮代码中间)login.php:
<?php
include 'common.php';

if ($user->hasLogin()) {
    $response->redirect($options->adminUrl);
}
$rememberName = htmlspecialchars(Typecho_Cookie::get('__typecho_remember_name'));
Typecho_Cookie::delete('__typecho_remember_name');

$bodyClass = 'body-100';

include 'header.php';
?>

<div class="typecho-login-wrap">
    <div class="typecho-login">
        <h1><a href="http://typecho.org" class="i-logo">Typecho</a></h1>
        <form action="<?php $options->loginAction(); ?>" method="post" name="login" role="form">
            <p>
                <label for="name" class="sr-only"><?php _e('用户名'); ?></label>
                <input type="text" id="name" name="name" value="<?php echo $rememberName; ?>" placeholder="<?php _e('用户名'); ?>" class="text-l w-100" autofocus />
            </p>
            <p>
                <label for="password" class="sr-only"><?php _e('密码'); ?></label>
                <input type="password" id="password" name="password" class="text-l w-100" placeholder="<?php _e('密码'); ?>" />
            </p>
            <?php Typecho_Plugin::factory('gt')->render(); ?>
            <p class="submit">
                <button type="submit" class="btn btn-l w-100 primary"><?php _e('登录'); ?></button>
                <input type="hidden" name="referer" value="<?php echo htmlspecialchars($request->get('referer')); ?>" />
            </p>
            <p>
                <label for="remember"><input type="checkbox" name="remember" class="checkbox" value="1" id="remember" /> <?php _e('下次自动登录'); ?></label>
            </p>
        </form>
        
        <p class="more-link">
            <a href="<?php $options->siteUrl(); ?>"><?php _e('返回首页'); ?></a>
            <?php if($options->allowRegister): ?>
            &bull;
            <a href="<?php $options->registerUrl(); ?>"><?php _e('用户注册'); ?></a>
            <?php endif; ?>
        </p>
    </div>
</div>
<?php 
include 'common-js.php';
/** 如果插件已启用, 则输出极验服务器响应数据 */
$geePluginEnable = !empty(Helper::options()->plugins['activated']['Geetest']) ? true : false;
?>
<script>
$(document).ready(function () {
    $('#name').focus();

    <?php if($geePluginEnable):?>
    //极客验证码验证
    (function(){
        var handler = function (captchaObj) {
            // 将验证码加到id为captcha的元素里
            captchaObj.appendTo("#captcha");
        };
        $.ajax({
            // 获取id,challenge,success(是否启用failback)
            url: "geetest-code.php?rand="+Math.random()*100,
            type: "get",
            dataType: "json", // 使用jsonp格式
            success: function (data) {
                // 使用initGeetest接口
                // 参数1:配置参数,与创建Geetest实例时接受的参数一致
                // 参数2:回调,回调的第一个参数验证码对象,之后可以使用它做appendTo之类的事件
                initGeetest({
                    gt: data.gt,
                    challenge: data.challenge,
                    product: type, // 产品形式float-浮动式 embed-嵌入式
                    offline: !data.success //支持本地验证
                }, handler);
            }
        });
    })();
    <?php endif;?>
});
</script>
<?php
include 'footer.php';
?>

  1. 后台控制台-》插件-》极客登录验证码-》启用
    极客登录验证码插件
  2. 填写官网申请的极验验证ID极验验证Key
    插件配置
  3. 处理后端登录验证逻辑/var/Widget/Login.php:
<?php
if (!defined('__TYPECHO_ROOT_DIR__')) exit;


/**
 * 登录组件
 *
 * @category typecho
 * @package Widget
 * @copyright Copyright (c) 2008 Typecho team (http://www.typecho.org)
 * @license GNU General Public License 2.0
 */
class Widget_Login extends Widget_Abstract_Users implements Widget_Interface_Do
{
    /**
     * 初始化函数
     *
     * @access public
     * @return void
     */
    public function action()
    {
        // protect
        $this->security->protect();

        /** 如果已经登录 */
        if ($this->user->hasLogin()) {
            /** 直接返回 */
            $this->response->redirect($this->options->index);
        }

        /** 初始化验证类 */
        $validator = new Typecho_Validate();
        $validator->addRule('name', 'required', _t('请输入用户名'));
        $validator->addRule('password', 'required', _t('请输入密码'));

        /** 截获验证异常 */
        if ($error = $validator->run($this->request->from('name', 'password'))) {
            Typecho_Cookie::set('__typecho_remember_name', $this->request->name);

            /** 设置提示信息 */
            $this->widget('Widget_Notice')->set($error);
            $this->response->goBack();
        }

        /** 极客验证码校验 **/
        $this->verifyGeeTest();

        /** 开始验证用户 **/
        $valid = $this->user->login($this->request->name, $this->request->password,
        false, 1 == $this->request->remember ? $this->options->gmtTime + $this->options->timezone + 30*24*3600 : 0);

        /** 比对密码 */
        if (!$valid) {
            /** 防止穷举,休眠3秒 */
            sleep(3);

            $this->pluginHandle()->loginFail($this->user, $this->request->name,
            $this->request->password, 1 == $this->request->remember);

            Typecho_Cookie::set('__typecho_remember_name', $this->request->name);
            $this->widget('Widget_Notice')->set(_t('用户名或密码无效'), 'error');
            $this->response->goBack('?referer=' . urlencode($this->request->referer));
        }

        $this->pluginHandle()->loginSucceed($this->user, $this->request->name,
        $this->request->password, 1 == $this->request->remember);

        /** 跳转验证后地址 */
        if (NULL != $this->request->referer) {
            $this->response->redirect($this->request->referer);
        } else if (!$this->user->pass('contributor', true)) {
            /** 不允许普通用户直接跳转后台 */
            $this->response->redirect($this->options->profileUrl);
        } else {
            $this->response->redirect($this->options->adminUrl);
        }
    }

    /**
     * 开始验证用户行为
     */
    private function verifyGeeTest()
    {
        $status = array(
            'empty'   => '请进行拼图验证',
            'failed'  => '验证失败',
            'success' => '验证通过',
            'down'    => '请求超时,请重试',
            'error'   => '服务器异常,请重试'
        );
        /** 如果插件已启用, 则进行验证 */
        if(!empty(Helper::options()->plugins['activated']['Geetest'])) {
            $data = $this->request->from('geetest_challenge', 'geetest_validate', 'geetest_seccode');
            $response = Typecho_Plugin::factory('gt')->verify($data);
            if($response == 'success') {

            } else {
                $error  = !empty($status[$response]) ? $status[$response] : $status['error'];
                $this->widget('Widget_Notice')->set($error);
                $this->response->goBack();
            }
        }
    }
}

  1. 演示后台登录吧~
    演示后台登录

标签: php, typecho, geetest, plugin, 验证码, 原创

已有 10 条评论

  1. T_T试了一下好像没有成功

    1. 什么原因呢 有错误提示吗

  2. RYAN RYAN

    你好,十分感谢你的教程。
    使用过程中发现一个问题,“Typecho_Plugin::exists('Geetest');” 的地方都出错,显示 “Non-static method Typecho_Plugin::exists() should not be called statically”,不知道博主你的为什么不会出错?请问这里有什么好的解决方法吗?试了将 exists() 改为 static 仍不成功。
    感谢

    1. RYAN RYAN

      噢,查询了一下,Typecho 通过 “isset(Helper::options()->plugins['activated']['Geetest'])” 判断成功,亦即修改 /admin/login.php、/admin/geetest-code.php、/var/Widget/Login.php 相应位置成功。

      再次感谢。

      1. 嗯 你这个方法也是可行的 。我之所以能成功 好像改了静态方法 源码地址:https://github.com/github-zjh/blog-typecho/blob/master/var/Typecho/Plugin.php 互相学习

        1. 文章已更新,用Helper::options()->plugins['activated']['Geetest']比较好!感谢@RYAN

  3. 小七 小七

    博主有考虑把最新的 极客验证码3.0 加入到评论功能里面吗?每天好多垃圾评论呀~!!

    1. 我这里欢迎垃圾评论 :) 访问量小

  4. 楼主的服务器是自己搭建的还是在哪里买的?

    1. 阿里云的ecs, web环境自己搭的

添加新评论