刚开始本地搭建好typecho的博客后,发现后台登录竟然没有验证码,太不安全了,加上typecho支持插件式开发,就自己动手做个吧。
/
/usr/plugins/Geetest
/usr/plugins/Geetest/Plugin.php
/usr/plugins/Geetest/lib/class.geetestlib.php
- 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';
}
}
}
}
- 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);
}
}
- sdk包中其他文件不需要了,配置文件已做成后台插件配置形式.
- 增加/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();
}
- 把插件嵌入到登录页面(放到密码框和提交按钮代码中间)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): ?>
•
<a href="<?php $options->registerUrl(); ?>"><?php _e('用户注册'); ?></a>
<?php endif; ?>
</p>
</div>
</div>
<?php
include 'common-js.php';
/** 如果插件已启用, 则输出极验服务器响应数据 */
$geePluginEnable = Typecho_Plugin::exists('Geetest');
?>
<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';
?>
- 后台控制台-》插件-》极客登录验证码-》启用
[ - 填写官网申请的极验验证ID和极验验证Key
[ - 处理后端登录验证逻辑/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' => '服务器异常,请重试'
);
/** 如果插件已启用, 则进行验证 */
$exists = Typecho_Plugin::exists('Geetest');
if(false !== $exists) {
$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();
}
}
}
}
- 演示后台登录吧~
[