zabbix前端代码浅析之登录验证全过程

 

      最近比较忙,工作刚走上正轨,所以你懂的,接着前面的,简单的说下zabbix的整个登录验证过程

 

   1.意义:

         

          这部分其实普通用户是不需要特别关心的,但如果你想将zabbix接入cas、ladp等认证系统的时候就需要大致了解下了。我们公司这边是接入了cas验证,所以我才有机会去研究这个整个过程。

 

 

2.index的开始:

 

         众所周知,index是一个网站的起点,zabbix的前端其实也是一个网站,当然也不例外了。前面(http://www.furion.info/453.html)我们已经大致说了下index中涉及的验证部分,主要就是读取cookie、读取用户,然后调用:

    

$login = CUser::authenticate(array('user'=>$name, 'password'=>$passwd, 'auth_type'=>$authentication_type));

authenticate是实际的校验函数。

 

 

 

3.class.cuser.php的验证

 

      authenticate 在api/classes/class.cuser.php中定义,是登录过程中非常核心的一个函数。之前也大致说个这个函数,这里我们再详细看下究竟都做了些什么操作。

 

            $name = $user['user'];
            $passwd = $user['password'];
            $auth_type = $user['auth_type'];

            if ($auth_type == ZBX_AUTH_HTTP) {
                // if PHP_AUTH_USER is not set, it means that HTTP authentication is not enabled
                if (!isset($_SERVER['PHP_AUTH_USER'])) {
                    throw new APIException(ZBX_API_ERROR_INTERNAL, S_CUSER_ERROR_CANNOT_LOGIN);
                }
                // check if the user name used when calling the API matches the one used for HTTP authentication
                elseif ($name !== $_SERVER['PHP_AUTH_USER']) {
                    throw new APIException(ZBX_API_ERROR_INTERNAL, S_CUSER_ERROR_USER_DOES_NOT_MATCH_HTTP_LOGIN);
                }

            }

     这部分没啥说的,读取用户名、密码,然后判断如果是http验证,进一步判断是否设置了相关的参数,否则直接报异常退出,一般不用该方式验证,所以不用太关心了。

 

            $password = md5($passwd);

            $sql = 'SELECT u.userid,u.attempt_failed, u.attempt_clock, u.attempt_ip '.
                    ' FROM users u '.
                    ' WHERE u.alias='.zbx_dbstr($name);

            $login = $attempt = DBfetch(DBselect($sql));

 

     这里使用了md5对读取的密码进行加密,然后调用DBselect函数去查询数据库,DBfetch是封装的一个函数,在数据库类型为Mysql的时候其实就是mysql_fetch_assoc函数。

     

     这里需要注意的是查询数据库不是直接查找用户,查找用户的sql语句其实还在后面,而是查询的是当前用户的登录失败记录。对应的是数据库的user表中的用户登录失败的记录。我们动手去查询下就知道了:

 

 

                                          image

          

    可以知道,查询的结果是用户的id、尝试的失败次数、上次的失败登录并且被锁定的时间,以及当时登录的IP。

if($login){
                if($login['attempt_failed'] >= ZBX_LOGIN_ATTEMPTS){
                    if((time() - $login['attempt_clock']) < ZBX_LOGIN_BLOCK){
                        throw new APIException(ZBX_API_ERROR_INTERNAL, S_CUSER_ERROR_ACCOUNT_IS_BLOCKED_FOR_XX_SECONDS_FIRST_PART.' '.(ZBX_LOGIN_BLOCK - (time() - $login['attempt_clock'])).' '.S_CUSER_ERROR_ACCOUNT_IS_BLOCKED_FOR_XX_SECONDS_SECOND_PART);
                    }
                    else{
                        DBexecute('UPDATE users SET attempt_clock='.time().' WHERE alias='.zbx_dbstr($name));
                    }
                }

                if($auth_type != ZBX_AUTH_HTTP){
                    switch(get_user_auth($login['userid'])){
                        case GROUP_GUI_ACCESS_INTERNAL:
                            $auth_type = ZBX_AUTH_INTERNAL;
                            break;
                        case GROUP_GUI_ACCESS_SYSTEM:
                        case GROUP_GUI_ACCESS_DISABLED:
                        default:
                            break;
                    }
                }

                switch($auth_type){
                    case ZBX_AUTH_LDAP:
                        $login = self::ldapLogin($user);
                        break;
                    case ZBX_AUTH_HTTP:
                        $login = true;
                        break;
                    case ZBX_AUTH_INTERNAL:
                    default:
                        $login = true;
                }
            }

    

     这部分主要是防止暴露破解攻击、判断验证类型从而重置login变量。当当前的登录失败次数大于ZBX_LOGIN_ATTEMPTS(在include/defines.inc.php中定义为5)次数时,判断当前时间和上次锁定的时间差是否大于ZBX_LOGIN_BLOCK,如果小于直接报异常,否者刷新锁定时间。

 

     如果验证类型是ladp验证,则调用ladpLogin函数去校验,其他方式则直接重置login 为true。

if($login){
                $sql = 'SELECT u.* '.
                        ' FROM users u'.
                        ' WHERE u.alias='.zbx_dbstr($name).
                            ((ZBX_AUTH_INTERNAL==$auth_type)? ' AND u.passwd='.zbx_dbstr($password):'').
                            ' AND '.DBin_node('u.userid', $ZBX_LOCALNODEID);

                $login = $user = DBfetch(DBselect($sql));
            }

            if($login){
                $login = (check_perm2login($user['userid']) && check_perm2system($user['userid']));
            }

    根据前面重置的login结果,进行数据库查询。如果查询到结果,则进行相应的权限校验同时再次重置login变量。权限这块很复杂,后续我们再慢慢研究吧。

 

if($login){
                $sessionid = zbx_session_start($user['userid'], $name, $password);

                // we assign $USER_DETAILS first time, so that it could be used in Cprofile::get
                $USER_DETAILS = $user;

                add_audit(AUDIT_ACTION_LOGIN,AUDIT_RESOURCE_USER, 'Correct login ['.$name.']');
                if(empty($user['url'])){
                    $user['url'] = CProfile::get('web.menu.view.last','index.php');

                    // we assign $USER_DETAILS second time, to update 'url'
                    $USER_DETAILS['url'] = $user['url'];
                }

                $login = $sessionid;
            }
            else{
                $user = NULL;

                $_REQUEST['message'] = S_CUSER_ERROR_LOGIN_OR_PASSWORD_INCORRECT;
                add_audit(AUDIT_ACTION_LOGIN,AUDIT_RESOURCE_USER,'Login failed ['.$name.']');

                if($attempt){
                    $ip = (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !empty($_SERVER['HTTP_X_FORWARDED_FOR']))?$_SERVER['HTTP_X_FORWARDED_FOR']:$_SERVER['REMOTE_ADDR'];
                    $attempt['attempt_failed']++;
                    $sql = 'UPDATE users '.
                            ' SET attempt_failed='.$attempt['attempt_failed'].','.
                                ' attempt_clock='.time().','.
                                ' attempt_ip='.zbx_dbstr($ip).
                            ' WHERE userid='.zbx_dbstr($attempt['userid']);
                    DBexecute($sql);
                }
            }

  根据前面的权限检查结果,如果longin为空则在登录界面显示错误提示信息,同时调用add_audit 写入审计日志、DBexecute刷新当前的登录失败次数、尝试的IP等信息。

           

    如果login不为空,则调用zbx_session_start 获取session、重置login变量,同时add_audi写入审计日志、获取用户的默认url等。

    我们先看下zbx_session_start,这个是实际的获取session的地方。

3.perm.inc.php的回话开始

                   

      zbx_session_start 在include/perm.inc.php中定义:

 

function zbx_session_start($userid, $name, $password){
    $sessionid = md5(time().$password.$name.rand(0,10000000));
    zbx_setcookie('zbx_sessionid',$sessionid);

    DBexecute('INSERT INTO sessions (sessionid,userid,lastaccess,status) VALUES ('.zbx_dbstr($sessionid).','.zbx_dbstr($userid).','.time().','.ZBX_SESSION_ACTIVE.')');

return $sessionid;

     这部分比较简单,主要是使用了md5将当前的时间、密码、帐号、还有一个随机数进行加密,然后调用zbx_setcookie函数进行写cookie,zbx_setcookie其实就是简单的封装了setcookie函数。

function zbx_setcookie($name, $value, $time=null){
    setcookie($name, $value, isset($time) ? $time : (0));
    $_COOKIE[$name] = $value;
} 

       随后将sessionid写入数据库中的sessions表中去,用作下次的登录查询使用。

    返回来我们再次看下add_audit函数。

4.audit.inc.php的审计

       

        add_audit函数就是用来写审计日志的函数,在include/audit.inc.php中,这个函数出现的很频繁,实际上大多数的用户操作都会调用该函数写审计日志。所以其实这部分对系统审计有很大的意思,但同时势必也会对系统的性能有一定的损失,我个人是想去掉的,当然提前是zabbix系统完全接入公司内网、vpn、cas等其他的防火措施到位的情况下。其实我现在接入的公司的cas验证系统后,就将审计日志写入了另外的一张表中去了。

 

function add_audit($action,$resourcetype,$details){
        global $USER_DETAILS;

        if(!isset($USER_DETAILS['userid'])) return false;

        $auditid = get_dbid('auditlog','auditid');

        if(zbx_strlen($details) > 128)
            $details = zbx_substr($details, 0, 125).'...';

        $ip = (isset($_SERVER['HTTP_X_FORWARDED_FOR']) && !empty($_SERVER['HTTP_X_FORWARDED_FOR']))?$_SERVER['HTTP_X_FORWARDED_FOR']:$_SERVER['REMOTE_ADDR'];

        if(($result = DBexecute('INSERT INTO auditlog (auditid,userid,clock,action,resourcetype,details,ip) '.
            ' VALUES ('.$auditid.','.$USER_DETAILS['userid'].','.time().','.
                        zbx_dbstr($action).','.zbx_dbstr($resourcetype).','.zbx_dbstr($details).','.
                        zbx_dbstr($ip).')')))
        {
            $result = $auditid;
        }

        return $result;
    }

     首先检查当前用户名(alias),不存在直接退出。然后调用get_dbid函数获取ids表中的nextid。这个函数大致的目的就是维护ids表中的nextid值,保证唯一、累加,ids表保存了当前数据库各个表的下一个id值,简单的理解就是每张表的唯一自增主键。只是自增主键是mysql自身维护的,这是zabbix通过get_dbid来维护的。

    剩下的部分就是将IP、用户操作、操作的内容等加入auditlog表中去,我们详细看下get_dbid函数。

5.db.inc.php的幕后支持

     get_dbid函数定义在include/db.inc.php中,这个函数很重要,用来维护zabbix库中各个表的id值的唯一、自增,代码如下:

function get_dbid($table,$field){
// PGSQL on transaction failure on all queries returns false..
        global $DB, $ZBX_LOCALNODEID;
        if(($DB['TYPE'] == 'POSTGRESQL') && $DB['TRANSACTIONS'] && !$DB['TRANSACTION_STATE']) return 0;
//------
        $nodeid = get_current_nodeid(false);

        $found = false;

        $min = bcadd(bcmul($nodeid, '100000000000000', 0), bcmul($ZBX_LOCALNODEID, '100000000000', 0), 0);
        $max = bcadd(bcadd(bcmul($nodeid, '100000000000000', 0), bcmul($ZBX_LOCALNODEID, '100000000000', 0), 0), '99999999999', 0);

        do{


            $db_select = DBselect('SELECT nextid FROM ids WHERE nodeid='.$nodeid .' AND table_name='.zbx_dbstr($table).' AND field_name='.zbx_dbstr($field));
            if(!is_resource($db_select)) return false;
            $row = DBfetch($db_select);

            if(!$row){
                $row = DBfetch(DBselect('SELECT max('.$field.') AS id FROM '.$table.' WHERE '.$field.'>='.$min.' AND '.$field.'<='.$max));
                if(!$row || is_null($row['id'])){
                    DBexecute("INSERT INTO ids (nodeid,table_name,field_name,nextid) VALUES ($nodeid,'$table','$field',$min)");
                }
                else{
                    DBexecute("INSERT INTO ids (nodeid,table_name,field_name,nextid) VALUES ($nodeid,'$table','$field',".$row['id'].')');
                }
                continue;
            }
            else{
                $ret1 = $row['nextid'];
                if((bccomp($ret1, $min, 0) < 0) || !(bccomp($ret1, $max, 0) < 0)) {
                    DBexecute('DELETE FROM ids WHERE nodeid='.$nodeid.' AND table_name='.zbx_dbstr($table).' AND field_name='.zbx_dbstr($field));
                    continue;
                }

                $sql = 'UPDATE ids SET nextid=nextid+1 WHERE nodeid='.$nodeid.' AND table_name='.zbx_dbstr($table).' AND field_name='.zbx_dbstr($field);
                DBexecute($sql);

                $row = DBfetch(DBselect('SELECT nextid FROM ids WHERE nodeid='.$nodeid.' AND table_name='.zbx_dbstr($table).' AND field_name='.zbx_dbstr($field)));
                if(!$row || is_null($row["nextid"])){
// Should never be here
                    continue;
                }
                else{
                    $ret2 = $row["nextid"];
                    if(bccomp(bcadd($ret1, 1, 0), $ret2, 0) == 0){
                        $found = true;
                    }
                }
            }
        }
        while(false == $found);

    return $ret2;
    }

      这部分结合数据库表结构来看比较清晰:

mysql> desc ids;
+------------+---------------------+------+-----+---------+-------+
| Field      | Type                | Null | Key | Default | Extra |
+------------+---------------------+------+-----+---------+-------+
| nodeid     | int(11)             | NO   | PRI | 0       |       |
| table_name | varchar(64)         | NO   | PRI |         |       |
| field_name | varchar(64)         | NO   | PRI |         |       |
| nextid     | bigint(20) unsigned | NO   |     | 0       |       |
+------------+---------------------+------+-----+---------+-------+
4 rows in set (0.01 sec)

    ids表中记录了各张表的第一个字段(id)的值,我们动手查询下就明白了:

mysql> select * from profiles  order by profileid desc limit 1;
+-----------+--------+------------------+------+----------+-----------+-----------+--------+------+
| profileid | userid | idx              | idx2 | value_id | value_int | value_str | source | type |
+-----------+--------+------------------+------+----------+-----------+-----------+--------+------+
|     81486 |    225 | web.paging.start |    0 |        0 |         0 |           |        |    2 |
+-----------+--------+------------------+------+----------+-----------+-----------+--------+------+
1 row in set (0.00 sec)

mysql> select * from ids where table_name='profiles';
+--------+------------+------------+--------+
| nodeid | table_name | field_name | nextid |
+--------+------------+------------+--------+
|      0 | profiles   | profileid  |  81486 |
+--------+------------+------------+--------+
1 row in set (0.00 sec)

 

      看到了吧,profileid 就是通过存在在ids中的nextid字段,同样类似的有users表中的userid等,这部分均是通过get_dbid来维护的。

     看懂了表结构、目的,那函数自然就很容易了,当然这里还调用了get_current_nodeid函数去获取当前的节点号,我们这里都手动置为0了,具体有兴趣的自行研究吧。主要的逻辑就是先查询当前的nextid值,然后去进行加1 的更新,随后再次进行检测,如果失败则一直continue重试,成功的话将found重置为true。

   

     唯一需要注意的是,这部分的sql语句开启了一个事务,当时进行与cas接入的时候,由于事务的提交代码不在这块,所以导致了nextid的update语句死锁,最后超时退出,从而整个登录过程都卡死这里了。在DBA的帮助下才得以解决,直接在这部分的sql代码后添加了一行事务的提交代码。

6.index的结束

 

    

         在获取到session、成功写入审计日志之后,authenticate函数开始落幕,退出到index.php中。随后调用了redirect进行重定向到url,url为user表中查询到的用户默认的url。

      

    if($login){
            $url = is_null($request) ? $USER_DETAILS['url'] : $request;
            redirect($url);
            exit();
        }
    }

         整个的登录验证到这里结束。

 

7.总结

      

        看完整个的登录过程,其实还是非常有收获的。在与cas、ladp等系统对接的时候,最怕的就是搞不清登录流程,不知道卡死或者失败在那个阶段。清楚了大致流程,故障的时候才会心中有数,一路摸索过来,可谓艰辛,希望对有耐心看到这里的各位有所帮助吧。

Leave a Reply

Your email address will not be published. Required fields are marked *


To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax