CAS客户端整合(三) Otrs
OTRS 是用Perl寫(xiě)的一個(gè)工單郵件系統(tǒng),非常強(qiáng)大。
登錄流程
流程圖略過(guò)
otrs沒(méi)有像 discuz 和 zabbix 類(lèi)似的游客登錄狀態(tài),這樣處理起來(lái)邏輯分支少一些。
不過(guò)還是考慮用 otrs 的 session 機(jī)制,這樣可以在用戶(hù)已登錄的時(shí)候減少想 CAS-server 認(rèn)證的次數(shù)。
因此,登錄的流程基本跟Zabbix一致
Perl-cas 客戶(hù)端
perl 沒(méi)有官方的 cas 客戶(hù)端代碼。網(wǎng)上找到一個(gè)比較新的 perlcashttps://subversion.renater.fr/perlcas/trunk/
直接用的話有點(diǎn)問(wèn)題,需要做點(diǎn)修改。
cas-server 默認(rèn)使用https協(xié)議,因此需要修改 perlcas 的 curl 操作,這里是 get_https2() 方法, 改成 LWP::UserAgent
sub get_https2 {
my $host = shift;
my $port = shift;
my $path = shift;
unless ( eval "require LWP::UserAgent" ) {
$errors = sprintf
"Unable to use LWP library, LWP::UserAgent required, install LWP (CPAN) first
";
return undef;
}
require LWP::UserAgent;
# user LWP::UserAgent
my $ua = LWP::UserAgent->new(
protocols_allowed => ['http', 'https'],
timeout => 30,
ssl_opt => {
verify_hostname => 0
}
);
$ua->default_header('cookie'=>'');
my $url = 'http://'.$host.':'.$port.$path;
my $response = $ua->get($url);
return $response->{_content};
}
改之前返回的是一個(gè)xml列表,改之后變成了一整個(gè)xml字符串,因此在callCAS()作以下修改:
sub callCAS {
my $self = shift;
my $url = shift;
my ( $host, $port, $path ) = &_parse_url($url);
my $xml = get_https2(
$host, $port, $path,
{
'cafile' => $self->{'CAFile'},
'capath' => $self->{'CAPath'},
'SSL_version' => $self->{'SSL_version'}
}
);
# use Data::Dumper; die ''.Dumper($xml);
# unless ($xml && $#$xml >= 0) {
# warn $errors;
# return undef;
# }
# ## Skip HTTP header fields
# my $line = shift @$xml;
# while ( $line !~ /^s*$/ ) {
# $line = shift @$xml;
# }
return &_parse_xml( $xml );
}
修改登錄過(guò)程
OTRS 的用戶(hù)登錄驗(yàn)證都在 Kernel/System/Web/InterfaceAgent.pm, 根據(jù) OTRS 開(kāi)發(fā)建議,我們復(fù)制原文件到 `Custom/Kernel/System/Web/InterfaceAgent.pm',然后再進(jìn)行修改。
為了方便調(diào)用, 增加兩個(gè)自定義方法:
# logout cas.
# 2017-11-14 by Carl
sub _logoutCAS {
my $Self = shift;
my $ConfigObject = $Kernel::OM->Get('Kernel::Config');
my $ParamObject = $Kernel::OM->Get('Kernel::System::Web::Request');
my $wsCAS = new AuthCAS(
casUrl => "http://cas.cloud.com:8088/cas-server",
);
my $app = 'http://'.$ENV{SERVER_NAME}.$ENV{REQUEST_URI};
# 非常關(guān)鍵,
$app =~ s/?$ENV{QUERY_STRING}//;
## Redirect the User for login at CAS
## This step is not required if we already have a PGT (Proxy Granting Ticket)
my $login_url = $wsCAS->getServerLogoutURL($app);
my $cookie = $ParamObject->SetCookie(
Key => $ConfigObject->Get('SessionName') || 'SessionID',
Value => '',
Expires => '-1y',
Path => $ConfigObject->Get('ScriptAlias'),
Secure => 0,
HTTPOnly => 1,
);
printf "Set-Cookie: $cookie
Content-Type: text/html; charset=UTF-8;
Status: 302 Found
Location: %s
", $login_url;
exit 0;
return 1;
}
# login cas.
# 2017-11-14 by Carl
sub _loginCAS {
my $Self = shift;
my $wsCAS = new AuthCAS(
casUrl => "http://cas.cloud.com:8088/cas-server",
);
my $app = 'http://'.$ENV{SERVER_NAME}.$ENV{REQUEST_URI};
my $login_url = $wsCAS->getServerLoginURL($app);
unless ($ENV{'QUERY_STRING'} =~ /ticket=/) {
## Redirect the User for login at CAS
## This step is not required if we already have a PGT (Proxy Granting Ticket)
my $login_url = $wsCAS->getServerLoginURL($app);
printf "Location: $login_url
";
exit 0;
}
my $ST;
# 非常重要,否則會(huì)導(dǎo)致cas-token反復(fù)認(rèn)證失敗
$ENV{'QUERY_STRING'} =~ /ticket=([^&]+)/;
$ST = $1;
# very important!
# Remove ST-ticket from query string
$app =~ s/&ticket(=[^&]*)?|?ticket(=[^&]*)?&?//;
my $User = $wsCAS->validateST($app, $ST);
return $User;
}
首先處理登出請(qǐng)求
# Handle CAS-Server logout request.
# 2017-11-13 by Carl.
if ( $ParamObject->GetParam(Param => 'logoutRequest') || $Param{Action} eq 'Logout' ) {
# logout
# elsif ( $Param{Action} eq 'Logout' ) {
my $LayoutObject = $Kernel::OM->Get('Kernel::Output::HTML::Layout');
# check session id
if ( !$SessionObject->CheckSessionID( SessionID => $Param{SessionID} ) ) {
$Self->_logoutCAS();
return 1;
#... if 分支結(jié)束后也要掉用一次,這里省略不表
考慮沒(méi)有系統(tǒng)session的情況:
# show login site
elsif ( !$Param{SessionID} ) {
# use CAS Auth login
# 2017-11-13 by Carl.
my $User = $Self->_loginCAS();
# login is successful
my %UserData = $UserObject->GetUserData(
User => $User,
Valid => 1
);
這樣保證無(wú)session的時(shí)候必先驗(yàn)證 cas,驗(yàn)證通過(guò)才有session。退出系統(tǒng)清除session
然而,這里出現(xiàn)一個(gè)bug。因?yàn)閛trs的session也是寫(xiě)在本地cookie,而調(diào)用logout的時(shí)候直接重定向到了cas-server。此時(shí)本地cookie未被清除。這就是為什么logout方法中需要輸出 Set-cookie文件頭的原因。(注:此處表現(xiàn)了對(duì)http協(xié)議還不夠熟悉。服務(wù)端的logout請(qǐng)求只與cas-client端的web服務(wù)器交互,無(wú)法影響瀏覽器客戶(hù)端的cookie!)
小結(jié)
代碼仍舊很簡(jiǎn)單。難點(diǎn)在于除了要分析otrs的登錄流程,還需要自己構(gòu)造perl客戶(hù)端。
不過(guò)正由于如此,反而對(duì)cas認(rèn)證機(jī)制有了更清晰的認(rèn)識(shí)。之前雖然修改了幾個(gè)系統(tǒng),但是對(duì)cas的過(guò)程并沒(méi)有認(rèn)真分析。
客戶(hù)端驗(yàn)證cas的時(shí)候,先向服務(wù)器驗(yàn)證本地isAuthenticated,未成功則發(fā)起登錄請(qǐng)求。這是第一次302
服務(wù)端發(fā)現(xiàn)已有用戶(hù)登錄,直接根據(jù)客戶(hù)端請(qǐng)求中的service參數(shù),獲取重定向地址,并附帶 Server-token 。這是第二次302
客戶(hù)端收到 server-token 之后,拿這個(gè)ST 向服務(wù)端發(fā)起 curl 請(qǐng)求獲取xml用戶(hù)信息。此時(shí)需要一并傳入之前發(fā)起認(rèn)證時(shí)的客戶(hù)端url(未帶st參數(shù))。
這時(shí)如果直接使用 REQUEST_URI (攜帶了ST參數(shù)) 將導(dǎo)致認(rèn)證失敗。
成功的xml:
失敗的xml:
php-client 的做法是獲取用戶(hù)信息后寫(xiě)入session, 然后直接再次重定向到?jīng)]有ST參數(shù)的頁(yè)面. 這是第三次302
如何才能保留cookie機(jī)制的同時(shí)認(rèn)證cas?
我的想法是在接受服務(wù)端的 logoutRequest的時(shí)候清楚數(shù)據(jù)庫(kù)中的session(或使之過(guò)期)。
但是目前發(fā)現(xiàn),在其他客戶(hù)端推出的時(shí)候,好像收不到服務(wù)端的登出通知?
日志中也無(wú)法得知
繼續(xù)探索
============================================================================
【2017-11-17】修復(fù)客戶(hù)端同步退出問(wèn)題。
查看Apache的access_log發(fā)現(xiàn),其實(shí)cas-server有發(fā)送 logoutRequest:
經(jīng)過(guò)一個(gè)下午的調(diào)試研究,終于解決了同步退出的問(wèn)題,下面是過(guò)程:
Otrs的session機(jī)制
用戶(hù)登錄后,會(huì)在后臺(tái)sessions表創(chuàng)建大量session變量,當(dāng)用戶(hù)推出后,會(huì)根據(jù)session_id 清空所有變量。
解決方案
既然不能清除瀏覽器的cookie,那么我們可以清除sessions表里的session_id,從而使從瀏覽器讀取的cookie無(wú)法驗(yàn)證通過(guò),達(dá)到退出登錄的目的。
研究cas-server的POST請(qǐng)求發(fā)現(xiàn),只有一個(gè)參數(shù):
logoutRequest: <samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="LR-2105-Kha1YoKgOCpxoCHuPf7qrdYVMTHvH4HYRVK" Version="2.0" IssueInstant="2017-11-17T16:20:03Z"><saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">@NOT_USED@</saml:NameID><samlp:SessionIndex>ST-2105-6fyJS6MtEhRUKsBqECQG-cas.cloud.com</samlp:SessionIndex></samlp:LogoutRequest>
唯一有用的就是里面的ST token。這個(gè)在登錄的時(shí)候也能獲取到。
然后需要做的就是吧這個(gè)ST 與 Otrs產(chǎn)生的 SessionID 綁定。
我們?cè)诘卿洺晒螅瑒?chuàng)建一個(gè) CasServerToken的 session 變量。然后在退出的時(shí)候根據(jù)這個(gè)變量值查找對(duì)應(yīng)的session_id, 刪除這個(gè) session_id.
代碼過(guò)程
1) 登錄的時(shí)候保存ST my ($User, $CASST) = $Self->_loginCAS();,在后面創(chuàng)建了SessionID的位置加上:
# login cas. save cas-ST
# 2017-11-17 by Carl.
$SessionObject->UpdateSessionID(
SessionID => $NewSessionID,
Key => 'CasServerToken',
Value => $CASST,
);
2) 處理同步登出請(qǐng)求:
# Handle CAS-Server logout request.
# 2017-11-13 by Carl.
if ( $ParamObject->GetParam(Param => 'logoutRequest') ){
# handle logout request
# read CAS ST ticket from logout request.
my $CasServerToken = $ParamObject->GetParam(Param => 'logoutRequest');
$CasServerToken =~ s/^.*<samlp:SessionIndex>(ST-.*)</samlp:SessionIndex>.*$/$1/;
# find current session ID.
my $DBObject = $Kernel::OM->Get('Kernel::System::DB');
$DBObject->Prepare(
SQL => "
SELECT session_id
FROM sessions
WHERE data_key = 'CasServerToken' AND data_value = ?
LIMIT 1 ",
Bind => [ $CasServerToken ],
);
my @Row = $DBObject->FetchrowArray();
if (my $CasSessionID = $Row[0]) {
# Remove session ID.
$SessionObject->RemoveSessionID( SessionID => $CasSessionID );
}
exit 0;
}
=============================================================
至此,otrs 的cas接入完成。
總結(jié)
以上是生活随笔為你收集整理的CAS客户端整合(三) Otrs的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: MySQL 授权指定ip访问
- 下一篇: Python使用heapq实现小顶堆(T