內部邀請碼: 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');