zoukankan      html  css  js  c++  java
  • CAS客户端整合(一) Discuz!

    有好几个系统需要接入CAS,所以登录模块统统需要重构

    版本

    • CAS服务端是Java的 Cas-server-4.0
    • CAS的php客户端 是 phpCAS-1.2.0
    • 论坛版本是 Discuz!X3.3

    Discuz! 登录流程

    因为discuz原来的流程是验证自己的一套用户密码体系,现在我们需要将这个验证过程放在 CAS-server ,然后通过绑定的 php-cas-client 来获取登录状态。由这个登录状态来决定需不需要初始化用户。

    原流程

    简(jian)单(nan)研究 Discuz! 的源码,大致画出它的登录流程

    CAS登录流程

    我们修改后的登录流程,橙色为需要修改的部分

    这里可以根据自己的业务场景进行调整,大致的流程是一样的。可能在是否本地cookie自动登录这点会有区别,在文末总结里会对这一点谈谈我个人的分析。

    整合 Discuz! 与 phpCAS

    画好了流程图,然后可以开始动手写代码了。

    phpCAS接入

    根目录下引入整个phpCAS客户端/cas/,新建一个CasClient.php用来做一些初始化工作。

    /*
     * path : /cas/CasClient.php
     */
    define ( 'CAS_SERVER_HOST', 'localhost' );
    define ( 'CAS_SERVER_PORT', 34382 );
    define ( 'CAS_SERVER_PATH', "/cas-server" );
    
    include_once (dirname ( __FILE__ ) . '/CAS.php'); 
    
    // debug logfile name
    phpCAS::setDebug ('./cas.log');
    
    // initialize phpCAS
    phpCAS::client ( CAS_VERSION_2_0, CAS_SERVER_HOST, CAS_SERVER_PORT, CAS_SERVER_PATH );
    
    // no SSL validation for the CAS server
    phpCAS::setNoCasServerValidation ();
    
    // sync logout requests
    phpCAS::handleLogoutRequests();
    
    //var_dump(phpCAS::isAuthenticated());
    

    然后在discuz中包含它,在 /source/class/class_core.php添加一行:

    error_reporting(E_ALL);
    
    define('IN_DISCUZ', true);
    define('DISCUZ_ROOT', substr(dirname(__FILE__), 0, -12));
    define('DISCUZ_CORE_DEBUG', false);
    define('DISCUZ_TABLE_EXTENDABLE', false);
    // include phpCAS
    require_once DISCUZ_ROOT."cas/CasClient.php";
    
    set_exception_handler(array('core', 'handleException'));
    
    • **我引入的phpCAS版本是用 global 变量来存放 PHPCAS_CLIENT,Discuz在程序初始化的时候会清空全局变量(discuz_application->_init_env()),因此导致在后面无法获取到PHPCAS_CLIENT 变量,出现phpCAS error: phpCAS::isAuthenticated(): this method should not be called before phpCAS::client() or phpCAS::proxy()错误。我的解决方案是把/cas/CAS.php里的 global 变量改成了 static,问题解决。 **

    • phpCAS 客户端是通过 session 来记录isAuthenticated()状态,引入phpCAS的地方可以获取正确的session, 在后面的业务代码中就为null。聪明的你已经想到因为跟上面相同的原因,全局的 session 变量在 Discuz 初始化的时候被清空了。因此对/source/class/discuz/discuz_application.php进行如下修改

        private function _init_env() {
        ...
        foreach ($GLOBALS as $key => $value) {
            // if session of phpCAS, keep it.
        	if (!isset($this->superglobal[$key])) {
        		if ($key == '_SESSION') {
        			$temp_phpCAS = $_SESSION['phpCAS'];
        			$GLOBALS[$key] = null; unset($GLOBALS[$key]);
        			$_SESSION['phpCAS'] = $temp_phpCAS;
        			$temp_phpCAS = null; unset($temp_phpCAS);
        			continue;
        		}
        		$GLOBALS[$key] = null; unset($GLOBALS[$key]);
        	}
        }
        ...
      

    这样我们就可以在discuz的其他地方正确获取到phpCAS的client对象。

    Discuz 的登录过程

    登录过程中有2个地方需要修改。第一是页面初始化的时候,检查cas是否已登录,如果已登录直接初始化用户登录信息;第二处是如果页面发起了登录请求,我们需要将请求引导到cas-server端登录,验证完成后返回。

    页面初始化

    修改位于/source/class/discuz/discuz_appliation.php,大致是 455 行 _init_user() 方法:

    	if($this->init_user) {
    		/**
    		 * login via cas
    		 */
    		if (phpCAS::isAuthenticated()) {
                // 根据自己实际情况获取用户字段,这里论坛账号为中文名
                // 因此用这个中文名到数据库中查找用户uid进行初始化
    			$username = phpCAS::getAttribute('cname');
    			$db_user_info = DB::fetch_first("SELECT `uid`,`username`,`password`,`email` FROM ". DB::table('ucenter_members') ." WHERE `username`='$username' ");
    			$discuz_pw = $db_user_info['password'];
    			$discuz_uid = $db_user_info['uid'];
                // 下面这部分跟原来的验证过程一致
    			if ($db_user_info) {
    				$user = getuserbyuid($discuz_uid, 1);
    				if(isset($user['_inarchive'])) {
    					C::t('common_member_archive')->move_to_master($discuz_uid);
    				}
    				$this->var['member'] = $user;
    			} else {
    				$user = array();
    				$this->_init_guest();
    			}
    		} else {
                // original discuz auth via cookie
    

    以上修改的意思是,页面初始化的时候会检查cas-server的用户登录状态,如果已有用户登录,获取用户名,在discuz初始化这个用户的登录状态。

    登录跳转

    接下来要处理的问题是,当用户没有登录的时候,如何从discuz登录到 cas-server。
    discuz的所有登录(管理后台除外)都会由/source/class/class_member.phpon_login()方法进行处理。

    	/*
    	 * discuz 原先的逻辑
    	if(!submitcheck('loginsubmit', 1, $seccodestatus)) {
    		登录界面
    	}
    	else {
    		验证账号密码
    	}
    	*/
    	if ( TRUE ) {
                
    		$username='';
    		if (!phpCAS::isAuthenticated()) {
                // 非常重要
                // 构造登录返回的url
    			$url = phpCAS::$_PHPCAS_CLIENT->getServerLoginURL();
    			$url = substr($url, 0, strpos($url, 'login?service='));
    			$url = $url . 'login?service=' . urlencode(dreferer());
                //phpCAS::setServerLoginURL($url);
                //phpCAS::forceAuthentication ();
    			showmessage ( '尚未登录,<a href="'.$url.'" >前去登录</a><script type="text/javascript">window.top.location.href="'.$url.'";</script>' );
    		}
            // 获取用户
    		$username = phpCAS::getAttribute('cname');
    		$password = '';
    		$email = phpCAS::getAttribute('email');
    		// $result = userlogin($_GET['username'], $_GET['password'], $_GET['questionid'], $_GET['answer'], $this->setting['autoidselect'] ? 'auto' : $_GET['loginfield'], $_G['clientip']);
    		// 采用自己的登陆方法
                        $result = userloginCas ( $username, $email );
    		$uid = $result ['ucresult'] ['uid'];
            // 后面继续按照discuz的流程即可
    

    然后在/source/function/function_member.php添加自己的userloginCas()方法:

    /**
     * Login via Cas
     * @param $member
     * @param $cookietime
     */
    function userloginCas($username, $email) {
    $return = array ();
    $merge = 0;
        // 根据用户名获取用户信息
    $db_user_info = DB::fetch_first("SELECT `uid`,`username`,`password`,`email` FROM ". DB::table('ucenter_members') ." WHERE `username`='$username' ");
    if ($db_user_info) {
    	$return ['ucresult'] = array(
    		$db_user_info['uid'],
    		$db_user_info['username'],
    		$db_user_info['password'],
    		$db_user_info['email'],
    		$merge,
    	);
    } else {
    	$return ['ucresult'] = array(0, '', '', '', 0);
    }
        
    if ($merge && $return ['ucresult'] ['uid'] > 0 || $return ['ucresult'] ['uid'] <= 0) {
    	$return ['status'] = 0;
    	return $return;
    }
        
    $member = getuserbyuid ( $return ['ucresult'] ['uid'], 1 );
    if (! $member || empty ( $member ['uid'] )) {
    	$return ['status'] = - 1;
    	return $return;
    }
    $return ['member'] = $member;
    $return ['status'] = 1;
    if ($member ['_inarchive']) {
    	C::t ( 'common_member_archive' )->move_to_master ( $member ['uid'] );
    }
    if ($member ['email'] != $return ['ucresult'] ['email']) {
    	C::t ( 'common_member' )->update ( $return ['ucresult'] ['uid'], array (
    		'email' => $return ['ucresult'] ['email']
    	) );
    }
        
    return $return;
    }
    

    上述代码的作用是,当用户请求登录时,若用户未在cas服务器登录,直接引导用户跳转至cas-server进行登录,并记录当前页面url在验证成功之后返回。否则直接初始化当前cas登录的用户。

    隐藏右上角的登录框

    登录全部交给cas-server,这里就不再需要登录框了,直接找到/template/default/member/login_simple.htm魔改消灭它。

    登出处理

    function on_logout() {
    	global $_G;
                
    	$ucsynlogout = $this->setting ['allowsynlogin'] ? uc_user_synlogout () : '';
                
    	if ($_GET ['formhash'] != $_G ['formhash']) {
    		$service =  dreferer ()  ;
    		phpCAS::logoutWithRedirectService ( $service );
    		showmessage ( 'logout_succeed', dreferer (), array (
    			'formhash' => FORMHASH,
    			'ucsynlogout' => $ucsynlogout,
    			'referer' => rawurlencode ( dreferer () )
    		) );
    	}
                
    	clearcookies ();
    	$_G ['groupid'] = $_G ['member'] ['groupid'] = 7;
    	$_G ['uid'] = $_G ['member'] ['uid'] = 0;
    	$_G ['username'] = $_G ['member'] ['username'] = $_G ['member'] ['password'] = '';
    	$_G ['setting'] ['styleid'] = $this->setting ['styleid'];
                
    	if (defined ( 'IN_MOBILE' )) {
    		$service =  dreferer ()  ;
    		phpCAS::logoutWithRedirectService ( $service );
    		showmessage ( 'location_logout_succeed_mobile', dreferer (), array (
    			'formhash' => FORMHASH,
    			'referer' => rawurlencode ( dreferer () )
    		) );
    	} else {
    		// 退出
    		$service =  dreferer ()  ;
    		phpCAS::logoutWithRedirectService ( $service );
    		// 后面这个showmessage提示实际上已经不会执行了
            // showmessage ( 'logout_succeed', dreferer (), array (
            // 'formhash' => FORMHASH,
            // 'ucsynlogout' => $ucsynlogout,
            // 'referer' => rawurlencode ( dreferer () )
            // ) );
    	}
    }
    

    问题小结

    1. 本地cookie登录

    我这里登录全部交给cas-server进行处理,每次web请求都会向cas-server进行认证,本地cookie事实上已经没有关系了。如果想把流程改为 先尝试本地cookie登录,再认证cas-server,看起来可以减少中间的会话过程(一般cas-server会在另一台服务器上),提升页面响应速度。这种方案的流程为,在验证cas之前先尝试cookie登录,然后尝试cas认证,认证成功则写入cookie,然后初始化用户登录状态。麻烦的地方在于用户在其他cas更改了用户之后,如何同步响应修改cookie更改用户。
    2. ### 用户登录行为记录 ###
    登录行为记录可以考虑放在cas-server端,否则需要通过其他方式判断用户登录行为,比如额外的参数(或者curl?)
    3. ### 管理后台登录和用户管理 ###
    discuz管理员需要原生的discuz账号密码登录,cas用户的账号密码由另一个数据库进行统一管理,因此需要同步两个系统的用户账号和密码。
    事实上,当CAS连接了N个系统的时候,如何同步管理这多个系统的账号和权限是个非常棘手的问题。
    4. ### 文章参考 ###

  • 相关阅读:
    2020年,初级、中级 Android 工程师可能因离职而失业吗?
    Android 后台运行白名单,优雅实现保活
    使用DataBinding还在为数据处理头疼?这篇文章帮你解决问题
    Android 7.x Toast BadTokenException处理
    2017-2020历年字节跳动Android面试真题解析(累计下载1082万次,持续更新中)
    Flutter 尺寸限制类容器总结:这可能是全网解析最全的一篇文章,没有之一!
    并行收集器
    高性能索引策略
    Innodb简介
    索引的优点
  • 原文地址:https://www.cnblogs.com/dapianzi/p/7799851.html
Copyright © 2011-2022 走看看