phpcas的几个遗留问题
在之前的博客《企业CAS单点登录的架构思路》介绍过了CAS单点登录的主要思路,在后续的实践过程中遇到了几个不大不小的问题,记录下来也许会帮助到大家。
无法获取attributes
CAS服务端可以配置将LDAP中的属性映射到CAS中的attributes属性,这样在客户端验证ST票据的时候除了可以得到username,还可以得到attributes,也就是LDAP中描述用户信息的一些属性。
当CAS服务端配置正确后,在客户端验证TS时可以读到如下的XML报文应答:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
// "Jasig Style" Attributes: // // <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'> // <cas:authenticationSuccess> // <cas:user>jsmith</cas:user> // <cas:attributes> // <cas:attraStyle>RubyCAS</cas:attraStyle> // <cas:surname>Smith</cas:surname> // <cas:givenName>John</cas:givenName> // <cas:memberOf>CN=Staff,OU=Groups,DC=example,DC=edu</cas:memberOf> // <cas:memberOf>CN=Spanish Department,OU=Departments,OU=Groups,DC=example,DC=edu</cas:memberOf> // </cas:attributes> // <cas:proxyGrantingTicket>PGTIOU-84678-8a9d2sfa23casd</cas:proxyGrantingTicket> // </cas:authenticationSuccess> // </cas:serviceResponse> // |
其中<cas:user>是默认会返回的CAS用户名,而<cas:attributes>只有在CAS配置过LDAP映射属性后才会出现,其中的属性从格式上来看就是LDAP风格。
想要获取attributes属性,要求客户端按照CAS 3.0协议与CAS服务端通讯,这是CAS服务端的要求。
对于PHP来说,需要在创建客户端时声明这一点:
1 |
phpCAS::client(CAS_VERSION_3_0, self::CAS_HOST, self::CAS_PORT, self::CAS_CONTEXT); |
我一开始使用了2.0协议,结果导致服务端始终不返回<cas:attributes>标签。
统一登出
背景
此前一直将关注点放在CAS统一登录的流程上,但是忽略了如何实现统一登出。
因为CAS一旦完成ST校验,应用就只会访问自己的登录会话了。
默认CAS登出流程是A应用重定向浏览器到CAS服务的/cas/logout注销单点会话,然后重定向回应用注销本地会话。然而假设B应用此前也有自己的本地会话,那么B应用仍旧处于登录状态,对于企业内的多个应用来说,这个感觉就比较奇怪。
所以希望实现统一登出,也就是一旦CAS服务端单点会话注销,那么所有应用的本地会话也被注销。
原理
CAS是支持这一点的,也就是在CAS服务端单点会话注销时,会通过HTTP调用通知所有的应用注销会话,相当于一个通知机制。
CAS服务端因为维护了所有曾经校验过的ST票据以及票据所属的应用地址,所以可以将每个票据都回调给应用,让应用注销ST对应的会话。
为了实现这一点,CAS客户端默认实现都是用ST票据作为本地登录会话的key,所以当CAS通知注销时应用只需要根据传来的ST删除对应会话即可,对应的注销通知报文如下:
1 2 3 4 5 6 |
<samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="LR-1-VM1PfgJD6VEDtCc4NnIWaVLqFs0PktY6Ej9" Version="2.0" IssueInstant="2017-07-20T10:45:39Z"> <saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion"> @NOT_USED@ </saml:NameID> <samlp:SessionIndex>ST-2-HtrBiWrgRD9DFgL25GI9-passport.edu</samlp:SessionIndex> </samlp:LogoutRequest> |
其中<samlp:SessionIndex>就是对应的ST票据,也是应用本地会话的key。
需要CAS服务端为每个CAS应用,分别配置注销回调接口的地址。
一旦某个用户在CAS单点登出,那么该用户所有在此期间使用过的ST(每次登录一个应用会有一个ST),均会被回调给对应的应用,完成本地会话注销。
作为PHPCAS客户端来说,它已经提供了现成的函数来完成本地会话注销,我们只需要在回调接口里调用一下即可完成$_SESSION的注销:
1 2 3 4 5 6 7 8 9 10 11 |
/** * This method handles logout requests. * * @param bool $check_client true to check the client bofore handling * the request, false not to perform any access control. True by default. * @param bool $allowed_clients an array of host names allowed to send * logout requests. * * @return void */ public function handleLogoutRequests($check_client=true, $allowed_clients=false) |
其中check_client=true的话,则会根据调用请求来源IP进行DNS反解到HOSTNAME,判断HOSTNAME是否在白名单allowed_clients里。
这种安全考虑非常自然,否则恶意攻击者随便调用一下注销接口,我们就把会话删掉,这肯定是不行的,所以只能信任来自CAS服务端的请求。
但是从IP反解HOSTNAME对于很多企业是没有配置的,所以建议公司内系统放弃安全检测,也就是check_client=false。
我们可以在CAS服务端配置每个service的注销回调地址,当我们在CAS单点登出后,所有你登录过的应用会收到回调报文如下:
1 2 3 4 5 6 7 8 9 10 |
2FD6 .START (2018-03-14 09:51:18) phpCAS-1.3.5 ****************** [CAS.php:468] 2FD6 .=> CAS_Client::handleLogoutRequests(false, false) [CAS.php:1276] 2FD6 .| Logout requested [Client.php:1750] 2FD6 .| SAML REQUEST: <samlp:LogoutRequest xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol" ID="LR-2--UsUae8Vv7qwTi5g5iy6DFvb" Version="2.0" IssueInstant="2018-03-14T09:51:19Z"><saml:NameID xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">@NOT_USED@</saml:NameID><samlp:SessionIndex>ST-2-Jp5gUGaxEvQC5YwfQlVvX88JN6Q-localhost</samlp:SessionIndex></samlp:LogoutRequest> [Client.php:1752] 2FD6 .| No access control set [Client.php:1778] 2FD6 .| Logout command allowed [Client.php:1783] 2FD6 .| Ticket to logout: ST-2-Jp5gUGaxEvQC5YwfQlVvX88JN6Q-localhost [Client.php:1799] 2FD6 .| Session id: ST-2-Jp5gUGaxEvQC5YwfQlVvX88JN6Q-localhost [Client.php:1812] 2FD6 .| Session ST-2-Jp5gUGaxEvQC5YwfQlVvX88JN6Q-localhost destroyed [Client.php:1828] 2FD6 .| exit() |
handleLogoutRequests函数会取出其中的<samlp:SessionIndex>,也就是当初登录时用的票据ST,它同时也是本地SESSION的key。
只要把key对应的SESSION删除即可,我这里配置的是redis session,相关代码如下供大家参考:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
// 初始化SESSION private function initSession() { // 设置COOKIE名称 session_name(self::SESSION_COOKIE_NAME); // 设置COOKIE生命期 session_set_cookie_params(self::SESSION_LIFETIME, '/', '.mycompany.com', FALSE, TRUE); // 使用REDIS SESSION ini_set('session.save_handler','Redis'); ini_set('session.save_path',self::SESSION_SAVE_PATH); // 设置SESSION生命期 ini_set("session.gc_maxlifetime", self::SESSION_LIFETIME); // 启动SESSION session_start(); } // 因为CAS CLIENT涉及session初始化, 所以延迟到使用时创建 private function initCasClientOnce() { if (empty($this->casInit)) { // 初始化SESSION配置 $this->initSession(); // 创建CAS客户端 phpCAS::client(CAS_VERSION_3_0, self::CAS_HOST, self::CAS_PORT, self::CAS_CONTEXT); phpCAS::setNoCasServerValidation(); // For production use set the CA certificate that is the issuer of the cert // on the CAS server and uncomment the line below // phpCAS::setCasServerCACert($cas_server_ca_cert_path); // 如果调试, 打开这两行, 日志输出在/tmp/cas.log文件 // phpCAS::setDebug("/tmp/cas.log"); // phpCAS::setVerbose(true); $this->casInit = true; } } |
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~
