有好幾個系統需要接入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.php的on_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 () )
// ) );
}
}
問題小結
-
本地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. ### 文章參考 ###
