CAS客戶端整合(一) Discuz!

有好幾個系統須要接入CAS,因此登陸模塊通通須要重構javascript

版本

  • CAS服務端是Java的 Cas-server-4.0
  • CAS的php客戶端 是 phpCAS-1.2.0
  • 論壇版本是 Discuz!X3.3

Discuz! 登陸流程

由於discuz原來的流程是驗證本身的一套用戶密碼體系,如今咱們須要將這個驗證過程放在 CAS-server ,而後經過綁定的 php-cas-client 來獲取登陸狀態。由這個登陸狀態來決定需不須要初始化用戶。php

原流程

簡(jian)單(nan)研究 Discuz! 的源碼,大體畫出它的登陸流程
html

CAS登陸流程

咱們修改後的登陸流程,橙色爲須要修改的部分
java

這裏能夠根據本身的業務場景進行調整,大體的流程是同樣的。可能在是否本地cookie自動登陸這點會有區別,在文末總結裏會對這一點談談我我的的分析。web

整合 Discuz! 與 phpCAS

畫好了流程圖,而後能夠開始動手寫代碼了。數據庫

phpCAS接入

根目錄下引入整個phpCAS客戶端/cas/,新建一個CasClient.php用來作一些初始化工做。api

/*
 * 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,問題解決。
    cookie

  • phpCAS 客戶端是經過 session 來記錄isAuthenticated()狀態,引入phpCAS的地方能夠獲取正確的session, 在後面的業務代碼中就爲null。聰明的你已經想到由於跟上面相同的緣由,全局的 session 變量在 Discuz 初始化的時候被清空了。所以對/source/class/discuz/discuz_application.php進行以下修改session

    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. 文章參考

相關文章
相關標籤/搜索