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! 的源码,大致画出它的登录流程


image

CAS登录流程

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


image

这里可以根据自己的业务场景进行调整,大致的流程是一样的。可能在是否本地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,问题解决。 **

    image

  • 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魔改消灭它。

image

登出处理

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更改用户。

  1. 用户登录行为记录

登录行为记录可以考虑放在cas-server端,否则需要通过其他方式判断用户登录行为,比如额外的参数(或者curl?)

  1. 管理后台登录和用户管理

discuz管理员需要原生的discuz账号密码登录,cas用户的账号密码由另一个数据库进行统一管理,因此需要同步两个系统的用户账号和密码。
事实上,当CAS连接了N个系统的时候,如何同步管理这多个系统的账号和权限是个非常棘手的问题。

  1. 文章参考

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,099评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,828评论 3 387
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,540评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,848评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,971评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,132评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,193评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,934评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,376评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,687评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,846评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,537评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,175评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,887评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,134评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,674评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,741评论 2 351

推荐阅读更多精彩内容

  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,220评论 11 349
  • Php:脚本语言,网站建设,服务器端运行 PHP定义:一种服务器端的HTML脚本/编程语言,是一种简单的、面向对象...
    廖马儿阅读 2,126评论 2 38
  • 1. CAS 简介 1.1. What is CAS ? CAS ( Central Authenti...
    人在码途阅读 9,807评论 3 51
  • 在网站使用nginx+php做负载均衡情况下,同一个IP访问同一个页面会被分配到不同的服务器上,如果session...
    dreamer_lk阅读 1,023评论 2 9
  • 我也只有在夜深人静的时候,才会大着胆子扭亮台灯,铺开信纸,动笔写下你所在的那个城市的名字。 那是怎样的一座城市呢?...
    不见得阅读 304评论 3 4