Discuz 用户多次登录失败 要求填写验证码

目标

现在几乎所有的网站都会有验证码功能,有些是图文的,有些是填字符。在某个网站上看到过类似的功能,就想做一个。现在公司最近要求使用Discuz没办法,准备研究它的错误登录三次,就会要求等待15分钟时间的功能,并尝试修改它。


分析Discuz

第一步分析discuz 登录入口 member.php 文件
  • 登录的地址为:member.php?mod=logging&action=login& loginsubmit=yes& infloat=yes& lssubmit=yes& inajax=1

在这个文件里,系统会初始化框架,并判断mod是否存在,存在则加载对应文件。这里加载了./source/module/member/member_logging.php 文件

第二步分析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 文件
  • 这里才是 登录的重点地方 分析on_login 方法

这个 方法里面写了大量的 用户登录业务逻辑,其他的暂时不管,只需要跟踪它的登录。由于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个小时以前的失败记录。


上面是所有的登录失败操作。

那么问题出现了,在登录的时候是如何验证用户已经 连续多次登录失败过了呢?

Dicuz的验证用户是否多次登录失败

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 的登录失败 处理,都分析完了。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值