现在几乎所有的网站都会有验证码功能,有些是图文的,有些是填字符。在某个网站上看到过类似的功能,就想做一个。现在公司最近要求使用Discuz没办法,准备研究它的错误登录三次,就会要求等待15分钟时间的功能,并尝试修改它。
member.php
文件member.php?mod=logging&action=login& loginsubmit=yes& infloat=yes& lssubmit=yes& inajax=1
在这个文件里,系统会初始化框架,并判断mod是否存在,存在则加载对应文件。这里加载了./source/module/member/member_logging.php
文件
......
$ctl_obj = new logging_ctl();
$ctl_obj->setting = $_G['setting'];
$method = 'on_'.$_GET['action'];
$ctl_obj->template = 'member/login';
$ctl_obj->$method();
//这里直接实例化了class_member.php 的类,并调用 on_login 方法
class_member.php
文件这个 方法里面写了大量的 用户登录业务逻辑,其他的暂时不管,只需要跟踪它的登录。由于Discuz做个Ucenter 这么一个用户中心,所以现在有两处登录的地方。
第一处
.....
//这里用户登录用户的
$result = userlogin(
$_GET['username'],
$_GET['password'],
$_GET['questionid'],
$_GET['answer'],
$this->setting['autoidselect'] ? 'auto' :$_GET['loginfield'], $_G['clientip']);
$uid = $result['ucresult']['uid'];
......
第二处
function_member.php
文件分析
......
//这里实现用户 登录
if($isuid == 3) {
if(!strcmp(dintval($username), $username) && getglobal('setting/uidlogin')) {
$return['ucresult'] = uc_user_login($username, $password, 1, 1, $questionid, $answer, $ip);
} elseif(isemail($username)) {
$return['ucresult'] = uc_user_login($username, $password, 2, 1, $questionid, $answer, $ip);
}
if($return['ucresult'][0] <= 0 && $return['ucresult'][0] != -3) {
$return['ucresult'] = uc_user_login(addslashes($username), $password, 0, 1, $questionid, $answer, $ip);
}
} else {
$return['ucresult'] = uc_user_login(addslashes($username), $password, $isuid, 1, $questionid, $answer, $ip);
}
......
登录的到这里,就剩成功和失败了。今天不分析成功的情况,只分析处理登录失败的情况。
......
//登录失败
$password = preg_replace("/^(.{".round(strlen($_GET['password']) / 4)."})(.+?)(.{".round(strlen($_GET['password']) / 6)."})$/s", "\\1***\\3", $_GET['password']);
$errorlog = dhtmlspecialchars(
TIMESTAMP."\t".
($result['ucresult']['username'] ? $result['ucresult']['username'] : $_GET['username'])."\t".
$password."\t".
"Ques #".intval($_GET['questionid'])."\t".
$_G['clientip']);
//1、写入 日文件,暂时不研究
writelog('illegallog', $errorlog);
//2、登录失败 处理
loginfailed($_GET['username']);
//3、失败IP处理
failedip();
$fmsg = $result['ucresult']['uid'] == '-3' ? (empty($_GET['questionid']) || $answer == '' ? 'login_question_empty' : 'login_question_invalid') : 'login_invalid';
if($_G['member_loginperm'] > 1) {
showmessage($fmsg, '', array('loginperm' => $_G['member_loginperm'] - 1));
} elseif($_G['member_loginperm'] == -1) {
showmessage('login_password_invalid');
} else {
showmessage('login_strike');
}
2、登录失败处理
数据库文件 table_common_failedlogin.php
文件
public function update_failed($ip, $username) {
DB::query("UPDATE %t SET count=count+1, lastupdate=%d WHERE ip=%s", array($this->_table, TIMESTAMP, $ip));
}
//从上面可以看出,这里直接更新了 xx_common_failedlogin.php表,在对应username字段下的 count 字段+1 ,比更新最后登录 时间 - lastupdate 字段
//%d 就是 TIMESTAMP
//找到上面的定义 define('TIMESTAMP', time());
3、ip失败处理
function failedip() {
global $_G;
list($ip1, $ip2) = explode('.', $_G['clientip']);
$ip = $ip1.'.'.$ip2;
//这里直接记录了 ip ,具体的需要分析 table_common_failedip.php 文件。
C::t('common_failedip')->insert_ip($ip);
}
table_common_fialedip.php
文件
public function insert_ip($ip) {
if(DB::result_first("SELECT COUNT(*) FROM %t WHERE ip=%s AND lastupdate=%d", array($this->_table, $ip, TIMESTAMP))) {
DB::query("UPDATE %t SET `count`=`count`+1 WHERE ip=%s AND lastupdate=%d", array($this->_table, $ip, TIMESTAMP));
} else {
DB::query("INSERT INTO %t VALUES (%s, %d, 1)", array($this->_table, $ip, TIMESTAMP));
}
DB::query("DELETE FROM %t WHERE lastupdate<%d", array($this->_table, TIMESTAMP - 3600));
}
分析上面的代码:
这里首先判断,该IP下的登录失败是否存在,存在则更新登录失败的次数。不存在,则直接添加一条在该IP下的 登录失败数据。可以看到在最后 还一个删除 1个小时以前的失败记录。
上面是所有的登录失败操作。
那么问题出现了,在登录的时候是如何验证用户已经 连续多次登录失败过了呢?
在class_member.php
文件中
//检查 被锁定,不能登录
if(!($_G['member_loginperm'] = logincheck($_GET['username']))) {
captcha::report($_G['clientip']);
showmessage('login_strike');
}
//logincheck() 这里面做的登录是否锁定验证
//logincheck 方法,可以看到,这里调用了 uc_user_logincheck方法。
function logincheck($username) {
global $_G;
$return = 0;
$username = trim($username);
loaducenter();
if(function_exists('uc_user_logincheck')) {
$return = uc_user_logincheck(addslashes($username), $_G['clientip']);
}
.......
//查看到 uc_user_logincheck方法这里 调用了 /control/user.php文件 里面的 logincheck方法
function uc_user_logincheck($username, $ip) {
return call_user_func(UC_API_FUNC, 'user', 'logincheck', array('username' => $username, 'ip' => $ip));
}
//找到这个方法,发现是下面这样写的。
function onlogincheck() {
$this->init_input();
$username = $this->input('username');
$ip = $this->input('ip');
return $_ENV['user']->can_do_login($username, $ip);
}
//$_ENV['user']->can_do_login($username, $ip); 分析这句话。$_ENV 为PHP全局变量 。可以知道,调用的是 user
分析和查找$_ENV['user']
代表的意义,根据Discuz 的尿性来说,这个一定是一个数据库对象,下面采用中断的方式开始分析这个$_ENV
折出现的时机。
在client.php
文件中下面位置
if(empty($uc_controls[$model])) {
if(function_exists("mysql_connect")) {
include_once UC_ROOT.'./lib/db.class.php';
} else {
include_once UC_ROOT.'./lib/dbi.class.php';
}
include_once UC_ROOT.'./model/base.php';
include_once UC_ROOT."./control/$model.php";
var_dump($_ENV['user']);exit;//没有结果
eval("\$uc_controls['$model'] = new {$model}control();");
var_dump($_ENV['user']);exit; //有输出
}
开始 分析 uc_client\./control/user.php
这个文件 。
//在这个文件中,看到下面这段
class usercontrol extends base {
function __construct() {
$this->usercontrol();
}
function usercontrol() {
parent::__construct();
$this->load('user'); //猜测是这里 加载了$_ENV 的东西。
$this->app = $this->cache['apps'][UC_APPID];
}
......
......
//找到了 这个load 方法,果然,在这里调用了
function load($model, $base = NULL) {
$base = $base ? $base : $this;
if(empty($_ENV[$model])) {
require_once UC_ROOT."./model/$model.php"; //这里加载了 /model/user.php 文件,去瞧一瞧。
eval('$_ENV[$model] = new '.$model.'model($base);');
}
return $_ENV[$model];
}
文件 uc_client\model\user.php
文件
//哇咔咔,终于找到这个方法,开始分析这个方法。
.......
function can_do_login($username, $ip = '') {
$check_times = $this->base->settings['login_failedtime'] < 1 ? 5 : $this->base->settings['login_failedtime']; //获取默认的最多失败次数
$username = substr(md5($username), 8, 15);
$expire = 15 * 60; //这个是单次验证有效时间,单位秒
if(!$ip) {
$ip = $this->base->onlineip;
}
$ip_check = $user_check = array();
$query = $this->db->query("SELECT * FROM ".UC_DBTABLEPRE."failedlogins WHERE ip='".$ip."' OR ip='$username'"); //根据IP,或者username 查询数据 ,根据这条sql可以知道这里要查询出的信息
while($row = $this->db->fetch_array($query)) {
if($row['ip'] === $username) {
$user_check = $row;
} elseif($row['ip'] === $ip) {
$ip_check = $row;
}
}
//判断,是 名称验证,还是 ip验证 ,默认以 username 验证为准
if(empty($ip_check) || ($this->base->time - $ip_check['lastupdate'] > $expire)) {
$ip_check = array();
$this->db->query("REPLACE INTO ".UC_DBTABLEPRE."failedlogins (ip, count, lastupdate) VALUES ('{$ip}', '0', '{$this->base->time}')");
}
if(empty($user_check) || ($this->base->time - $user_check['lastupdate'] > $expire)) {
$user_check = array();
$this->db->query("REPLACE INTO ".UC_DBTABLEPRE."failedlogins (ip, count, lastupdate) VALUES ('{$username}', '0', '{$this->base->time}')");
}
if ($ip_check || $user_check) {
$time_left = min(($check_times - $ip_check['count']), ($check_times - $user_check['count']));
return $time_left;
}
//上面看出,返回值为,最后还有登陆次数的机会。
$this->db->query("DELETE FROM ".UC_DBTABLEPRE."failedlogins WHERE lastupdate<".($this->base->time - ($expire + 1)), 'UNBUFFERED');
return $check_times;
}
最后 回归到 class_member.php
中下面
//检查 被锁定,不能登录
if(!($_G['member_loginperm'] = logincheck($_GET['username']))) {
captcha::report($_G['clientip']);
showmessage('login_strike');
}
//返回结果为 还可以错误的次数,若果返回 0 则直接 返回错误哦提示了。
到此 Discuz 的登录失败 处理,都分析完了。