OTRS 是用Perl写的一个工单邮件系统,非常强大。

登录流程

流程图略过

otrs没有像 discuz 和 zabbix 类似的游客登录状态,这样处理起来逻辑分支少一些。

不过还是考虑用 otrs 的 session 机制,这样可以在用户已登录的时候减少想 CAS-server 认证的次数。

因此,登录的流程基本跟Zabbix一致

Perl-cas 客户端

perl 没有官方的 cas 客户端代码。网上找到一个比较新的 perlcashttps://subversion.renater.fr/perlcas/trunk/

直接用的话有点问题,需要做点修改。

cas-server 默认使用https协议,因此需要修改 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\n";
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};
}

改之前返回的是一个xml列表,改之后变成了一整个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 );
}

修改登录过程

OTRS 的用户登录验证都在 Kernel/System/Web/InterfaceAgent.pm, 根据 OTRS 开发建议,我们复制原文件到 `Custom/Kernel/System/Web/InterfaceAgent.pm',然后再进行修改。

为了方便调用, 增加两个自定义方法:

# 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};
# 非常关键,
$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\nContent-Type: text/html; charset=UTF-8;\nStatus: 302 Found\nLocation: %s\n\n", $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\n\n";
exit 0;
} my $ST;
# 非常重要,否则会导致cas-token反复认证失败
$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;
}

首先处理登出请求

# 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 分支结束后也要掉用一次,这里省略不表

考虑没有系统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
);

这样保证无session的时候必先验证 cas,验证通过才有session。退出系统清除session

然而,这里出现一个bug。因为otrs的session也是写在本地cookie,而调用logout的时候直接重定向到了cas-server。此时本地cookie未被清除。这就是为什么logout方法中需要输出 Set-cookie文件头的原因。(注:此处表现了对http协议还不够熟悉。服务端的logout请求只与cas-client端的web服务器交互,无法影响浏览器客户端的cookie!)

小结

代码仍旧很简单。难点在于除了要分析otrs的登录流程,还需要自己构造perl客户端。

不过正由于如此,反而对cas认证机制有了更清晰的认识。之前虽然修改了几个系统,但是对cas的过程并没有认真分析。

客户端验证cas的时候,先向服务器验证本地isAuthenticated,未成功则发起登录请求。这是第一次302

服务端发现已有用户登录,直接根据客户端请求中的service参数,获取重定向地址,并附带 Server-token 。这是第二次302

客户端收到 server-token 之后,拿这个ST 向服务端发起 curl 请求获取xml用户信息。此时需要一并传入之前发起认证时的客户端url(未带st参数)。

这时如果直接使用 REQUEST_URI (携带了ST参数) 将导致认证失败。

成功的xml:

失败的xml:

php-client 的做法是获取用户信息后写入session, 然后直接再次重定向到没有ST参数的页面. 这是第三次302

如何才能保留cookie机制的同时认证cas?

我的想法是在接受服务端的 logoutRequest的时候清楚数据库中的session(或使之过期)。

但是目前发现,在其他客户端推出的时候,好像收不到服务端的登出通知?

日志中也无法得知

继续探索

============================================================================

【2017-11-17】修复客户端同步退出问题。

查看Apache的access_log发现,其实cas-server有发送 logoutRequest:

经过一个下午的调试研究,终于解决了同步退出的问题,下面是过程:

Otrs的session机制

用户登录后,会在后台sessions表创建大量session变量,当用户推出后,会根据session_id 清空所有变量。

解决方案

既然不能清除浏览器的cookie,那么我们可以清除sessions表里的session_id,从而使从浏览器读取的cookie无法验证通过,达到退出登录的目的。

研究cas-server的POST请求发现,只有一个参数:

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。这个在登录的时候也能获取到。

然后需要做的就是吧这个ST 与 Otrs产生的 SessionID 绑定。

我们在登录成功后,创建一个 CasServerToken的 session 变量。然后在退出的时候根据这个变量值查找对应的session_id, 删除这个 session_id.

代码过程

1) 登录的时候保存ST my ($User, $CASST) = $Self->_loginCAS();,在后面创建了SessionID的位置加上:

    # login cas. save cas-ST
# 2017-11-17 by Carl.
$SessionObject->UpdateSessionID(
SessionID => $NewSessionID,
Key => 'CasServerToken',
Value => $CASST,
);

2) 处理同步登出请求:

# 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接入完成。

CAS客户端整合(三) Otrs的更多相关文章

  1. CAS客户端整合(二) Zabbix

    Zabbix是一个强大的服务器/交换机监控应用,有zabbix-server, zabbix-client, zabbix-web 三部分.zabbix-web管理端是用php写的. 前文参考:CAS ...

  2. CAS客户端整合(四)-- Cacti

    Cacti 是一套纯 lnmp 搭建的服务器监控系统,用 SNMP 抓取数据,RRDTool 绘制表格 登录流程 Cacti 的登录同样是先判断session,再尝试从 cookie 读取 sessi ...

  3. CAS客户端整合(一) Discuz!

    有好几个系统需要接入CAS,所以登录模块统统需要重构 版本 CAS服务端是Java的 Cas-server-4.0 CAS的php客户端 是 phpCAS-1.2.0 论坛版本是 Discuz!X3. ...

  4. CAS学习笔记三:SpringBoot自动配置与手动配置过滤器方式集成CAS客户端

    本文目标 基于SpringBoot + Maven 分别使用自动配置与手动配置过滤器方式集成CAS客户端. 需要提前搭建 CAS 服务端,参考 https://www.cnblogs.com/hell ...

  5. 品优购商城项目(六)CAS客户端与SpringSecurity集成

    cas单点登录旨在解决传统登录模式session在分布式项目中共享登录信息的问题. 本文cas服务器使用 4.0版本,仅供学习参考.把 cas.war 直接部署在tomcat即可,这里有个固定的用户名 ...

  6. CAS5.3服务器搭建与客户端整合SpringBoot以及踩坑笔记

    CAS5.3服务器搭建与客户端整合SpringBoot以及踩坑笔记 cas服务器的搭建 导出证书(1和2步骤是找了课程,随便写了一下存记录,不过对于自己测试不投入使用应该不影响) C:\Users\D ...

  7. cas sso单点登录系列2:cas客户端和cas服务端交互原理动画图解,cas协议终极分析

    转:http://blog.csdn.net/ae6623/article/details/8848107 1)PPT流程图:ppt下载:http://pan.baidu.com/s/1o7KIlom ...

  8. springboot之cas客户端

    一.CAS Client 与受保护的客户端应用部署在一起,以 Filter 方式保护受保护的资源.对于访问受保护资源的每个 Web 请求,CAS Client 会分析该请求的 Http 请求中是否包含 ...

  9. 【Tech】单点登录系统CAS客户端demo

    服务器端配置请参考: http://www.cnblogs.com/sunshineatnoon/p/4064632.html 工具:myeclipse或者javaee-eclipse 1.启动jav ...

随机推荐

  1. 一张图理解is_nll isset empty

    isset 判断变量是否已存在,如果变量存在则返回 TRUE,否则返回 FALSE. empty 判断变量是否为空,如果变量是非空 或非零 的值,则 empty() 返回 FALSE.换句话说,&qu ...

  2. java:Map借口及其子类HashMap四

    java:Map借口及其子类HashMap四 使用非系统对象作为key,使用匿名对象获取数据 在Map中可以使用匿名对象找到一个key对应的value. person: public class Ha ...

  3. 单机版 RedisUtils({基本操作封装工具类})【三】

    <!--集成的RedisJAR--> <!--引入jedis需的jar包--> <dependency> <groupId>redis.clients& ...

  4. 解编码框架的比较(protobuf,thrift,Marshalling,xml)

    1.ProtoBuf 特点: 1.结构化数据存储格式 2.高效的解编码性能. 3.语言无关,平台无关,扩展性好. 4.官方支持java,c++,python三种语言. 5.性能比较好 (与之对比xml ...

  5. tensorflow CUDA 9.0安装成功

    berli@berli-dev:~/tensorflow$ bazel-bin/tensorflow/examples/label_image/label_image 2017-12-18 00:04 ...

  6. 【遍历二叉树】02二叉树的中序遍历【Binary Tree Inorder Traversal】

    ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 给定一个二叉树,返回他的中序遍历的 ...

  7. [原]NYOJ-组合数-32

    大学生程序代写 http://acm.nyist.net/JudgeOnline/problem.php?pid=32 /*组合数 时间限制:3000 ms  |  内存限制:65535 KB 难度: ...

  8. LOJ2719 「NOI2018」冒泡排序

    「NOI2018」冒泡排序 题目描述 最近,小S 对冒泡排序产生了浓厚的兴趣.为了问题简单,小 S 只研究对 1 到n 的排列的冒泡排序. 下面是对冒泡排序的算法描述. 输入:一个长度为n 的排列p[ ...

  9. 【VisualStudio】软件安装中出现的问题

    针对2017版本安装 1. 安装windows通用平台工具出错 报错信息:15605 FQ安装. 2.  LINK : fatal error LNK1104: 无法打开文件“gdi32.lib” 在 ...

  10. IOS的设计模式

    对象创建 原型(Prototype) 使用原型实例指定创建对象的种类,并通过复制这个原型创建新的对象. NSArray *array = [[NSArray alloc] initWithObject ...