内部邀请码:C8E245J (不写邀请码,没有现金送)
国内私募机构九鼎控股打造,九鼎投资是在全国股份转让系统挂牌的公众公司,股票代码为430719,为“中国PE第一股”,市值超1000亿元。
单点登录系统CAS搭建及取得更多用户信息的实现
一、 单点登录简介
单点登录(Single sign-on,简称为 SSO),是目前比较流行的企业业务整合的解决方案之一。其简单定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。类似的,用户只需要执行一次退出操作就可以终止对所有相关应用系统的访问。
本文主旨在介绍如何使用CAS实现单点登录时取得更多的用户信息,单点登录的原理将不作阐述。
二、 CAS简介
CAS是CentralAuthentication Service即中央认证服务的简称,它是由耶鲁大学发起的开源项目,其目的是为Web系统提供可靠的单点登录机制。2004年12月CAS正式成为JASIG(jasig.org)的一个项目。
CAS的Server需要作为独立Web应用部署,并且当前Server仅支持Java,但其Client端则支持Java、.Net、PHP、Perl、Apache、uPortal、Ruby等多种开发语言。
各版本的CAS可以分别从以下地址下载。
CAS Server的下载地址:http://downloads.jasig.org/cas/
CAS Client的下载地址:http://downloads.jasig.org/cas-clients/
本文验证环境使用CASServer 3.5.2、CAS Client 3.2.1(Java Client端)、CAS1.3.2(PHP Client端)。当前网上可查找到的大部分资料并不适用于CAS Server 3.5.2,因此如果使用其它版本本文可能也存在不适用的情况。
三、 CAS部署、基本配置与使用
1. Server端部署
将下载的CASServer压缩包解压,可以发现这个包中已经包含了Server侧的全部源代码及编译完成的Jar及war包。将modules目录下的cas-server-webapp-3.5.2.war拷贝至Tomcat的webapps下后启动Tomcat即可完成部署。
上述war包中包含了Server的版本号,因此部署完成后的应用访问URL中也会包含版本信息。如果期望为Client端提供更多的伸缩性和兼容性,可以在启动Tomcat前将war包的名称修改为cas.war。这样如果以后Server端进行升级,Client端也不需要做任何修改。
部署完成并启动Tomcat以后,就可以在浏览器中输入对应的URL(例如:http://192.168.202.176:8080/cas)访问CAS了。CAS Server提供的默认界面如下图所示。
在Username与Password输入框中输入相同的字符即可成功登录,登录成功后的界面如下图。
2. Server端配置
上述部署完成的Server存在两个问题,一是通过HTTP协议访问服务安全性较差,通常应当使用HTTPS协议以提供较高的安全性;另一方面登录账号与密码其实与系统不存在关联,不能在实际应用中加以使用,因此需要进一步做一些配置解决上述问题。
2.1 配置Tomcat的HTTPS协议
为Tomcat配置HTTPS协议分为两步,一是为Tomcat生成服务证书并将对应CA根证书导入keystore的可信列表中,二是修改Tomcat的配置文件。
关于证书生成,不同的应用场景与需求其具体过程会有一些差异。如果对此没有特殊要求,可以采用最为简单的自签名证书。其具体生成过程简述如下:
创建一个用于保存证书的目录,然后通过cmd命令进入命令提示符窗口,并将当前工作目录切换至刚刚创建的用于保存证书的目录(Linux系统过程类似)。随后执行以下命令,其中红色部分可以根据实际需要加以替换:
keytool-genkey -aliascas_key -keyalg RSA -storepass yanzhijun -keystorecas.keystore-validity 3600
注意:如果上述命令执行时提示找不到keytool,请检查环境变量JAVA_HOME和PATH是否已正确配置。
随后,keytool会提示输入一些与证书相关的信息,例如可以依次输入下图中所示值。其中特别要注意的是要输入的第一项“passport.yanzhijun.net”必须要输入CAS Server的实际域名或IP,否则在后面建立HTTPS连接时将不能建立可信连接。其它项可以根据实际情况输入或随意输入合法值即可。
上述命令成功执行以后,将生成名为cas.keystore的文件,Tomcat将使用它来建立HTTPS连接。
通常CAS Client端与Server端进行通信时也使用HTTPS协议并通过证书对Server进行身份确认,因此还需要将上述步骤中生成的证书导出备用。以下命令将证书导出为名为cas.cer的文件:
keytool-export -trustcacerts -alias cas_key -file cas.cer -keystore cas.keystore-storepass yanzhijun
注意:上述命令中的参数要与生成证书的命令中的参数保持对应一致。
完成证书生成以后,还需要对Tomcat的配置文件conf/server.xml进行修改。首先将标签Connectorport="8443"前后的注释删除,然后将其修改为类似以下内容:
<Connectorport="8443"protocol="org.apache.coyote.http11.Http11NioProtocol"SSLEnabled="true"
maxThreads="150" scheme="https"secure="true"
clientAuth="false" sslProtocol="TLS"
keystoreFile="C:/cert/cas.keystore"
keystorePass="yanzhijun"/>
注意:keystoreFile与keystorePass要与生成证书时输入的参数保持一致。
至此,Tomcat的HTTPS协议配置完成,重新启动Tomcat,就可以使用HTTPS协议访问cas了(例如https://passport.yanzhijun.net:8443/cas)。如果必要,可以将前述生成的cas.cer导入系统或浏览器的证书库中,以便访问CAS Server时浏览器不再显示警告信息。
2.2 配置CAS从数据库中取得数据验证登录的有效性
CASServer提供了多种身份验证方法,可以配置文件WEB-INF/deployerConfigContext.xml中对其加以修改。
在deployerConfigContext.xml中查找“<property name="authenticationHandlers">”,其中以下配置表明登录时Username与Password一致则通过验证。
<bean class="org.jasig.cas.authentication.handler.support.SimpleTestUsernamePasswordAuthenticationHandler"/>
要使用数据库中的信息对登录进行验证,需要将上述bean配置项注释或删除掉,然后在对应位置添加以下配置:
<beanclass="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
<property name="sql" value="selectpassword fromuserinfowhereuserName=?" />
<property name="dataSource" ref="dataSource"/>
</bean>
其中userinfo是保存用户信息的表名,userName是表userinfo中存储用户名的字段名称,password则是表userinfo中存储用户密码(或加密后的密码)的字段名称。
如果数据库中的密码是加密存储的(通常都应加密存储),则上述配置中需要增加名为“passwordEncoder”的property指定加密方法。例如以下配置指定了以SHA加密密码。
<propertyname="passwordEncoder">
<beanclass="org.jasig.cas.authentication.handler.DefaultPasswordEncoder">
<constructor-argvalue="SHA"/>
</bean>
</property>
在指定从数据库中查询用户信息进行验证时,同时指定了要使用数据源dataSource,因此还需要对数据源进行配置。以下数据源配置使用位于本机的MySQL数据库,可以将其配置在节点beans下的任意位置。
<beanid="dataSource"class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName"value="com.mysql.jdbc.Driver"></property>
<property name="url"value="jdbc:mysql://localhost:3306/cas_usr"></property>
<property name="username"value="cas_usr"></property>
<property name="password"value="123"></property>
</bean>
注意:使用数据库时需要将与之对应的数据库Java驱动拷入WEB-INF/lib目录下。
至此,CAS Server全部搭建完成。
3. Java Client端配置与使用
首先需要将下载的JavaClient压缩包解压并在解压后将modules目录下的cas-client-core-3.2.1.jar及commons-logging-1.1.jar拷入准备集成CAS的Web应用的WEB-INF/lib下。
随后需要在Web应用的web.xml文件中添加CAS的相关配置,具体配置项如下,请注意修改其中CAS Server的地址、端口信息及当前应用的地址、端口信息。
<filter>
<filter-name>CASAuthentication Filter</filter-name>
<filter-class>
org.jasig.cas.client.authentication.AuthenticationFilter
</filter-class>
<!-- CAS验证服务器地址-->
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>
https://passport.yanzhijun.net:8443/cas/login
</param-value>
</init-param>
<init-param>
<param-name>renew</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>gateway</param-name>
<param-value>false</param-value>
</init-param>
<!--客户端应用服务器地址-->
<init-param>
<param-name>serverName</param-name>
<param-value>http://192.168.202.176:8080</param-value>
</init-param>
</filter>
<!--负责Ticket校验-->
<filter>
<filter-name>CASValidation Filter</filter-name>
<filter-class>
org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter
</filter-class>
<!-- CAS验证服务器地址(上下文) -->
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>https://passport.yanzhijun.net:8443/cas/</param-value>
</init-param>
<!--客户端应用服务器地址-->
<init-param>
<param-name>serverName</param-name>
<param-value>http://192.168.202.176:8080</param-value>
</init-param>
<init-param>
<param-name>useSession</param-name>
<param-value>true</param-value>
</init-param>
<init-param>
<param-name>redirectAfterValidation</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter>
<filter-name>CASHttpServletRequest Wrapper Filter</filter-name>
<filter-class>
org.jasig.cas.client.util.HttpServletRequestWrapperFilter
</filter-class>
</filter>
<filter>
<filter-name>CASAssertion Thread Local Filter</filter-name>
<filter-class>
org.jasig.cas.client.util.AssertionThreadLocalFilter
</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS AuthenticationFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>CASValidation Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>CAS HttpServletRequestWrapper Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>CASAssertion Thread Local Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
至此,CAS机制已经可以发挥作用了,如果尝试打开Web应用,可以发现页面跳转到了CAS Server的登录页面,输入正确的Username与Password并提交后页面又跳转回当前Web应用。不过通常Web应用程序也需要知道当前登录的用户名称,以下示例JSP页面显示了在Web应用中获取当前登录用户名的几种方法。
<%@pageimport="org.jasig.cas.client.authentication.AttributePrincipal" %>
<%@page import="org.jasig.cas.client.validation.Assertion"%>
<%
String loginName1 = request.getRemoteUser();
%>
request.getRemoteUser(): <%=loginName1%><br/>
<%
AttributePrincipal principal = (AttributePrincipal)request.getUserPrincipal();
String loginName2 = principal.getName();
%>
request.getUserPrincipal().getName():<%=loginName2%><br/>
<%
Object object =request.getSession().getAttribute("_const_cas_assertion_");
Assertion assertion =(Assertion)object;
String loginName3 =assertion.getPrincipal().getName();
%>
request.getSession().getAttribute("_const_cas_assertion_").getPrincipal().getName():<%=loginName3%><br/>
查看上述代码运行后输出的结果,可以看到三种方式取得的用户称是一样的,均与登录时的名称保持一致。
4. PHP Client端使用
解压PHP Client压缩包,将文件夹CAS及文件CAS.php拷至需要集成CAS的PHP应用的适当位置。修改docs/examples目录下的文件example_simple.php如下,并令example_simple.php与文件夹CAS及文件CAS.php同一目录,在浏览器中访问example_simple.php,页面将显示当前登录的用户名称。
<?php
require_once'CAS.php';
phpCAS::client(CAS_VERSION_2_0,'passport.yanzhijun.net',8443, '/cas');
phpCAS::setNoCasServerValidation();
phpCAS::forceAuthentication();
if(isset($_REQUEST['logout'])) {
phpCAS::logout();
}
// forthis test, simply print that the authentication was successfull
?>
<html>
<head>
<title>phpCAS simpleclient</title>
</head>
<body>
<h1>Successfull Authentication!</h1>
<p>the user's login is<b><?php echo phpCAS::getUser(); ?></b>.</p>
<p>phpCAS version is<b><?php echo phpCAS::getVersion(); ?></b>.</p>
<p><ahref="?logout=">Logout</a></p>
</body>
</html>
四、 CAS单点登录取得更多用户信息
通过上述部署与配置,多个Web应用已经可以共用一个登录服务。但是,上述过程中作为CAS Client端的Web应用只取得了用户登录名称信息,而在实际应用中,Web应用往往需要获得登录用户更多的信息,例如会员等级、性别、住址等。要达到此目的,只需对Server端稍做修改即可实现。
1. 服务端配置及修改
假定上述存储用户信息的数据表userinfo中还包含一个名为address的用于存储用户地址的字段,而Web应用程序希望能够从CAS Server处获得当前登录用户的地址信息,则Server端需要按以下内容修改deployerConfigContext.xml。部分配置说明请参见注释。
<!--将原有attributeRepository配置注释 -->
<!--
<beanid="attributeRepository"
class="org.jasig.services.persondir.support.StubPersonAttributeDao">
<propertyname="backingMap">
<map>
<entrykey="uid" value="uid" />
<entrykey="eduPersonAffiliation" value="eduPersonAffiliation"/>
<entrykey="groupMembership" value="groupMembership" />
</map>
</property>
</bean>
-->
<!--新增attributeRepository配置(开始) -->
<bean class="org.jasig.services.persondir.support.jdbc.SingleRowJdbcPersonAttributeDao"id="attributeRepository">
<!-- 指定使用的数据源,此处dataSource是已配置好的数据源 -->
<constructor-arg index="0"ref="dataSource"/>
<!-- 从数据库中查询信息的SQL语句,通常只需要修改表名即可 -->
<constructor-arg index="1" value="select * fromuserinfo where {0}"/>
<propertyname="queryAttributeMapping">
<map>
<!-- 上述查询的参数,将userName替换为表中表示用户名的字段名称 -->
<entrykey="username" value="userName"/>
</map>
</property>
<propertyname="resultAttributeMapping">
<map>
<!-- 需要返回给Web应用的其它信息,多个信息时可继续增加entry节点-->
<!--key值为数据表中的字段名称,value值为Client端取值时的名称标识-->
<entry key="address" value="address"/>
</map>
</property>
</bean>
<!--新增attributeRepository配置(结束) -->
<bean
id="serviceRegistryDao"
class="org.jasig.cas.services.InMemoryServiceRegistryDaoImpl">
<propertyname="registeredServices">
<list>
<beanclass="org.jasig.cas.services.RegexRegisteredService">
<propertyname="id" value="0" />
<propertyname="name" value="HTTP and IMAP" />
<propertyname="description" value="Allows HTTP(S) and IMAP(S)protocols" />
<propertyname="serviceId" value="^(https?|imaps?)://.*" />
<propertyname="evaluationOrder" value="10000001" />
<!--增加此项配置 -->
<property name="ignoreAttributes" value="true"/>
</bean>
… …
</list>
</property>
</bean>
CASServer要将额外的信息传递至Client端,还需要修改完成信息组装的文件WEB-INF/view/jsp/protocol/2.0/casServiceValidationSuccess.jsp。casServiceValidationSuccess.jsp负责组装包含用户信息的XML,因此修改部分是将需要传递的额外信息加入到它最终生成的XML文件之中。具体修改如下:
<cas:serviceResponsexmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess> <cas:user>${fn:escapeXml(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.id)}</cas:user>
<!-- 新增额外信息(开始) -->
<c:iftest="${fn:length(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes)> 0}">
<cas:attributes>
<c:forEachvar="attr"items="${assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes}">
<!--注意此行的正确写法,网上资料基本都是错误的--> <cas:${fn:escapeXml(attr.key)}>${fn:escapeXml(attr.value)}</cas:${fn:escapeXml(attr.key)}>
</c:forEach>
</cas:attributes>
</c:if>
<!-- 新增额外信息(结束) -->
<c:if test="${not emptypgtIou}">
<cas:proxyGrantingTicket>${pgtIou}</cas:proxyGrantingTicket>
</c:if>
<c:if test="${fn:length(assertion.chainedAuthentications)> 1}">
<cas:proxies>
<c:forEachvar="proxy" items="${assertion.chainedAuthentications}"varStatus="loopStatus" begin="0"end="${fn:length(assertion.chainedAuthentications)-2}"step="1">
<cas:proxy>${fn:escapeXml(proxy.principal.id)}</cas:proxy>
</c:forEach>
</cas:proxies>
</c:if>
</cas:authenticationSuccess>
</cas:serviceResponse>
2. Java Client端取得更多用户信息
Java Client端不需要做任何修改就可以继续正常使用CAS服务,如果需要取得用户更多信息,可以通过AttributePrincipal对象取得Attribute列表(一个Map对象)后进行查询。
修改前述Java Client的示例代码,在最后追加取得address信息的代码,重启服务并重新访问页面,可以看到页面上显示了当前用户的address信息。
<%@pageimport="org.jasig.cas.client.authentication.AttributePrincipal" %>
<%@pageimport="org.jasig.cas.client.validation.Assertion" %>
<%@page import="java.util.*" %>
<%
String loginName1 = request.getRemoteUser();
%>
request.getRemoteUser(): <%=loginName1%><br/>
<%
AttributePrincipal principal = (AttributePrincipal)request.getUserPrincipal();
String loginName2 = principal.getName();
%>
request.getUserPrincipal().getName():<%=loginName2%><br/>
<%
Object object =request.getSession().getAttribute("_const_cas_assertion_");
Assertion assertion =(Assertion)object;
String loginName3 =assertion.getPrincipal().getName();
%>
request.getSession().getAttribute("_const_cas_assertion_").getPrincipal().getName():<%=loginName3%><br/>
<br/>
<%
// 以下代码取得address信息
Map<String, Object> attributes = principal.getAttributes();
Iterator it = attributes.keySet().iterator();
String address = "NoAddress";
Object oaddress=attributes.get("address");
if(oaddress != null)
{
address =oaddress.toString();
}
%>
address:<%=address%><br/>
3. PHP Client端取得更多用户信息
PHP Client要取得额外的用户信息只需要直接调用phpCAS::getAttribute即可,例如要取得上述配置中增加的address信息,只需要在适当位置增加以下代码:
phpCAS::getAttribute('address');