Apache CAS部署在tomcat上實現單點登錄

單點登錄(Single Sign On , 簡稱 SSO )是目前比較流行的服務於企業業務整合的解決方案之一, SSO 使得在多個應用系統中,用戶只需要登錄一次就可以訪問所有相互信任的應用系統。CAS(Central Authentication Service)是一款不錯的針對 Web 應用的單點登錄框架,本文介紹了 CAS 的原理、協議、在 Tomcat 中的配置和使用,對於採用 CAS 實現輕量級單點登錄解決方案的入門讀者具有一定指導作用。

1.CAS介紹

CAS 是 Yale 大學發起的一個開源項目,旨在爲 Web 應用系統提供一種可靠的單點登錄方法,CAS 在 2004 年 12 月正式成爲 JA-SIG 的一個項目。

CAS 具有以下特點:

(1)開源的企業級單點登錄解決方案。

(2)CAS Server 爲需要獨立部署的 Web 應用。

(3)CAS Client 支持非常多的客戶端(這裏指單點登錄系統中的各個 Web 應用),包括 Java, .Net, PHP, Perl, Apache, uPortal, Ruby 等。

2.CAS原理和協議

從結構上看,CAS 包含兩個部分: CAS Server 和 CAS Client。CAS Server 需要獨立部署,主要負責對用戶的認證工作;CAS Client 負責處理對客戶端受保護資源的訪問請求,需要登錄時,重定向到 CAS Server。圖1 是 CAS 最基本的協議過程:













                                       圖1 CAS基礎協議

CAS Client 與受保護的客戶端應用部署在一起,以 Filter 方式保護受保護的資源。對於訪問受保護資源的每個 Web 請求,CAS Client 會分析該請求的 Http 請求中是否包含 Service Ticket,如果沒有,則說明當前用戶尚未登錄,於是將請求重定向到指定好的 CAS Server 登錄地址,並傳遞 Service (也就是要訪問的目的資源地址),以便登錄成功過後轉回該地址。用戶在第 3 步中輸入認證信息,如果登錄成功,CAS Server 隨機產生一個相當長度、唯一、不可僞造的 Service Ticket,並緩存以待將來驗證,之後系統自動重定向到 Service 所在地址,併爲客戶端瀏覽器設置一個 Ticket Granted Cookie(TGC),CAS Client 在拿到 Service 和新產生的 Ticket 過後,在第 5,6 步中與 CAS Server 進行身份合適,以確保 Service Ticket 的合法性。
在該協議中,所有與 CAS 的交互均採用 SSL 協議,確保,ST 和 TGC 的安全性。協議工作過程中會有 2 次重定向的過程,但是 CAS Client 與 CAS Server 之間進行 Ticket 驗證的過程對於用戶是透明的。
另外,CAS 協議中還提供了 Proxy (代理)模式,以適應更加高級、複雜的應用場景,具體介紹可以參考 CAS 官方網站上的相關文檔。

3.準備工作

下載CAS Server:https://www.apereo.org/projects/cas/download-cas

下載CAS Client :http://developer.jasig.org/cas-clients/

下載Tomcat : 本文用的是Tomcat 8.0,自己去官網下載即可。

4. 生成https的證書文件

爲了在Tomcat啓用SSL,使其支持https訪問,需要生產證書文件。

這裏需要使用JDK安裝包下的keytool工具生成證書文件keystore,以命令方式換到目錄%TOMCAT_HOME%,在command命令行輸入如下命令: 

keytool -genkey -alias tomcat_key -keyalg RSA -storepass changeit -keystore server.keystore -validity 36500

此時會提示用戶輸入姓名,組織等信息,如下所示,只輸入localhost作爲name,其餘回車即可


這樣就會在當前路徑下生成一個名爲server.keystore的證書文件,這個文件會在後面tomcat的server.xml配置文件中用到。

簡單解釋下這個命令的含義:

-genkey 生成證書指令

 -alias 證書的別名爲tomcat_key 

-keyalg 生成證書的算法是RSA 

-storepass 證書的密碼changeit 

-keystore 生成的證書文件是server.keystore 這裏也可以指定具體的路徑,如在Mac下:~/Document表示用戶目錄下的Document文件夾下

-validity 36500 證書的有效期,單位是天,這裏指定的是100年


5.配置 Tomcat 使用 Https 協議

在Tomcat的server.xml配置文件中進行如下配置:

 <Connector SSLEnabled="true" clientAuth="false" 
			   maxThreads="150" port="8443" 
			   protocol="org.apache.coyote.http11.Http11NioProtocol" 
			   scheme="https" secure="true" sslProtocol="TLS"
			   keystoreFile="/Users/cruise/Documents/ISG/apache-tomcat-8.0.32/server.keystore" 
   			   keystorePass="changeit" />

這裏的keystoreFile需要指定上一步生成的證書文件,keystorePass對應生成文件的時候設置的密碼。

6.部署CAS Server到Tomcat

(1)到cas官網下載cas-server http://developer.jasig.org/cas/(我下載的是4.0.0)

(2)解壓壓縮文件,在解壓後的文件夾內找到/modules/cas-server-webapp-4.0.0.war。將其複製到%Tomcat_Home%\webapps下並改名爲cas.war

(3)啓動Tomcat,並測試 https://localhost:8443/cas 看是否訪問正常(默認輸入用戶名和密碼一致就可以)。 

CAS Server 4.0.0 默認登陸驗證方式是 AcceptUsersAuthenticationHandler (老版本好像是SimpleTestUsernamePasswordAuthenticationHandler),默認用戶名/密碼爲 casuser/Mellon(cas/WEB-INF/deployerConfigContext.xml 中找到 id=primaryAuthenticationHandler 的bean查看,裏面的map也可以自己增加更多個)。我們通常需要從數據庫中取出用戶名和密碼進行驗證,所以我們需要修改 deployerConfigContext.xml,配置我們自己的服務認證方式。   

雖然 CAS Server 已經部署成功,但這只是一個缺省的實現,在實際使用的時候,還需要根據實際概況做擴展和定製,最主要的是擴展認證 (Authentication) 接口和 CAS Server 的界面。

7. 擴展認證接口

CAS Server 負責完成對用戶的認證工作,它會處理登錄時的用戶憑證 (Credentials) 信息,用戶名/密碼對是最常見的憑證信息。CAS Server 可能需要到數據庫檢索一條用戶帳號信息,也可能在 XML 文件中檢索用戶名/密碼,還可能通過 LDAP Server 獲取等,在這種情況下,CAS 提供了一種靈活但統一的接口和實現分離的方式,實際使用中 CAS 採用哪種方式認證是與CAS的基本協議分離開的,用戶可以根據認證的接口去定製和擴展。
擴展 AuthenticationHandler
CAS 提供擴展認證的核心是 AuthenticationHandler 接口,該接口定義如清單 1 下:

清單 1. AuthenticationHandler定義

public interface AuthenticationHandler {
    /**
     * Method to determine if the credentials supplied are valid.
     * @param credentials The credentials to validate.
     * @return true if valid, return false otherwise.
     * @throws AuthenticationException An AuthenticationException can contain
     * details about why a particular authentication request failed.
     */
    boolean authenticate(Credentials credentials) throws AuthenticationException;
/**
     * Method to check if the handler knows how to handle the credentials
     * provided. It may be a simple check of the Credentials class or something
     * more complicated such as scanning the information contained in the
     * Credentials object. 
     * @param credentials The credentials to check.
     * @return true if the handler supports the Credentials, false othewrise.
     */
    boolean supports(Credentials credentials);
}

該接口定義了 2 個需要實現的方法,supports ()方法用於檢查所給的包含認證信息的Credentials 是否受當前 AuthenticationHandler 支持;而 authenticate() 方法則擔當驗證認證信息的任務,這也是需要擴展的主要方法,根據情況與存儲合法認證信息的介質進行交互,返回 boolean 類型的值,true 表示驗證通過,false 表示驗證失敗。
CAS3中還提供了對AuthenticationHandler 接口的一些抽象實現,比如,可能需要在執行authenticate() 方法前後執行某些其他操作,那麼可以讓自己的認證類擴展自清單 2 中的抽象類:
清單 2. AbstractPreAndPostProcessingAuthenticationHandler定義

public abstract class AbstractPreAndPostProcessingAuthenticationHandler 
                               implements AuthenticateHandler{
    protected Log log = LogFactory.getLog(this.getClass());
    protected boolean preAuthenticate(final Credentials credentials) {
        return true;
    }
    protected boolean postAuthenticate(final Credentials credentials,
        final boolean authenticated) {
        return authenticated;
    }
    public final boolean authenticate(final Credentials credentials)
        throws AuthenticationException {
        if (!preAuthenticate(credentials)) {
            return false;
        }
        final boolean authenticated = doAuthentication(credentials);
        return postAuthenticate(credentials, authenticated);
    }
    protected abstract boolean doAuthentication(final Credentials credentials) throws AuthenticationException;
}
AbstractPreAndPostProcessingAuthenticationHandler 類新定義了 preAuthenticate() 方法和 postAuthenticate() 方法,而實際的認證工作交由 doAuthentication() 方法來執行。因此,如果需要在認證前後執行一些額外的操作,可以分別擴展 preAuthenticate()和 ppstAuthenticate() 方法,而 doAuthentication() 取代 authenticate() 成爲了子類必須要實現的方法。
由於實際運用中,最常用的是用戶名和密碼方式的認證,CAS3 提供了針對該方式的實現,如清單 3 所示:
清單 3. AbstractUsernamePasswordAuthenticationHandler 定義

public abstract class AbstractUsernamePasswordAuthenticationHandler extends 
                       AbstractPreAndPostProcessingAuthenticationHandler{
    ...
    protected final boolean doAuthentication(final Credentials credentials)
     throws AuthenticationException {
        return authenticateUsernamePasswordInternal((UsernamePasswordCredentials) credentials);
    }
    protected abstract boolean authenticateUsernamePasswordInternal(
        final UsernamePasswordCredentials credentials) throws AuthenticationException;   
    protected final PasswordEncoder getPasswordEncoder() {
         return this.passwordEncoder;
     }
    public final void setPasswordEncoder(final PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }
    ...
}


基於用戶名密碼的認證方式可直接擴展自 AbstractUsernamePasswordAuthenticationHandler,驗證用戶名密碼的具體操作通過實現 authenticateUsernamePasswordInternal() 方法達到,另外,通常情況下密碼會是加密過的,setPasswordEncoder() 方法就是用於指定適當的加密器。
從以上清單中可以看到,doAuthentication() 方法的參數是 Credentials 類型,這是包含用戶認證信息的一個接口,對於用戶名密碼類型的認證信息,可以直接使用 UsernamePasswordCredentials,如果需要擴展其他類型的認證信息,需要實現Credentials接口,並且實現相應的 CredentialsToPrincipalResolver 接口,其具體方法可以借鑑 UsernamePasswordCredentials 和 UsernamePasswordCredentialsToPrincipalResolver。

8.實現JDBC的認證

用戶的認證信息通常保存在數據庫中,因此本文就選用這種情況來介紹。將前面下載的 cas-server-{version}-release.zip 包解開後,在 modules 目錄下可以找到包 cas-server-support-jdbc-{version}.jar,其提供了通過 JDBC 連接數據庫進行驗證的缺省實現,基於該包的支持,我們只需要做一些配置工作即可實現 JDBC 認證。

JDBC 認證方法支持多種數據庫,DB2, Oracle, MySql, Microsoft SQL Server 等均可,這裏以Mysql作爲例子介紹。並且假設Mysql數據庫名:test,數據庫登錄用戶名: root,數據庫登錄密碼:password,用戶信息表爲: t_user,該表包含用戶名和密碼的兩個數據項分別爲 username 和 password。用戶表數據如下圖所示:



1)配置DataSource

打開文件 cas/WEB-INF/deployerConfigContext.xml,添加一個新的 bean 標籤,以MYSQL爲例,內容如清單 4 所示:

    <!-- Data source definition --> 
    <bean id="casDataSource" class="org.apache.commons.dbcp.BasicDataSource"> 
        <property name="driverClassName"> 
            <value>com.mysql.jdbc.Driver</value> 
        </property> 
        <property name="url"> 
            <value>jdbc:mysql://localhost:3306/test</value> 
        </property> 
        <property name="username"> 
            <value>root</value> 
        </property> 
        <property name="password"> 
            <value>passw0rd</value> 
        </property>
    </bean>
其中 casDataSource 在後面配置 AuthenticationHandler 會被引用,添加數據庫驅動程序、連接地址、數據庫登錄用戶名以及登錄密碼。

2)配置 AuthenticationHandler

CAS 4.0.0 提供了 3 個基於 JDBC 的 AuthenticationHandler,分別爲 BindModeSearchDatabaseAuthenticationHandler, QueryDatabaseAuthenticationHandler, SearchModeSearchDatabaseAuthenticationHandler。 
- BindModeSearchDatabaseAuthenticationHandler 是用所給的用戶名和密碼去建立數據庫連接,根據連接建立是否成功來判斷驗證成功與否; 
- QueryDatabaseAuthenticationHandler 通過配置一個 SQL 語句查出密碼,與所給密碼匹配; 
- SearchModeSearchDatabaseAuthenticationHandler 通過配置存放用戶驗證信息的表、用戶名字段和密碼字段,構造查詢語句來驗證。

使用哪個 AuthenticationHandler,需要在 deployerConfigContext.xml 中設置,默認情況下,CAS 4.0.0 使用 AcceptUsersAuthenticationHandler(上面已經提到,需要在 deployerConfigContext.xml 中查看)。

在 deployerConfigContext.xml 文件中可以找到如下配置:

<bean id="primaryAuthenticationHandler"
        class="org.jasig.cas.authentication.AcceptUsersAuthenticationHandler">
   <property name="users">
       <map>
          <entry key="casuser" value="Mellon"/>
       </map>
   </property>
</bean> 

這裏我們註釋掉這段配置,然後換成我們希望的一個 AuthenticationHandler,比如,使用QueryDatabaseAuthenticationHandler 或 SearchModeSearchDatabaseAuthenticationHandler 可以分別選取清單 5 或清單 6 的配置。 
清單 5. 使用 QueryDatabaseAuthenticationHandler

<bean id="primaryAuthenticationHandler" 
        class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
        <property name="dataSource" ref="casDataSource" />
        <property name="sql" value="select password from t_user where lower(username) = lower(?)" />
        <!-- 指定密碼加密器(可選) -->
        <!-- <property name="passwordEncoder" ref="passwordEncoder" /> -->
</bean>
清單 6. 使用 SearchModeSearchDatabaseAuthenticationHandler

<bean id="SearchModeSearchDatabaseAuthenticationHandler"
    class="org.jasig.cas.adaptors.jdbc.SearchModeSearchDatabaseAuthenticationHandler"
    abstract="false" singleton="true" lazy-init="default" 
    autowire="default" dependency-check="default">
    <property name="tableUsers">
        <value>userTable</value>
    </property>
    <property name="fieldUser">
        <value>userName</value>
    </property>
    <property name="fieldPassword">
        <value>password</value>
    </property>
    <property name="dataSource" ref="casDataSource " />
</bean>
另外,由於存放在數據庫中的密碼通常是加密過的,所以 AuthenticationHandler 在匹配時需要知道使用的加密方法,在 deployerConfigContext.xml 文件中我們可以爲具體的 AuthenticationHandler 類配置一個 property,指定加密器類,比如對於 QueryDatabaseAuthenticationHandler,可以修改如清單7所示:
清單 7. QueryDatabaseAuthenticationHandler配置加密器類

<!-- 密碼加密器(可以指定自己實現的加密器) -->
<bean id="passwordEncoder"
    class="org.jasig.cas.authentication.handler.DefaultPasswordEncoder" >
    <constructor-arg name="encodingAlgorithm" value="MD5"/>
    <property name="characterEncoding" value="UTF-8"/>
</bean>
本文這裏由於未對數據庫中的用戶數據進行加密,所以在清單5中將配置的加密屬性註釋掉。

注意:DataSource 依賴於 commons-collections-3.2.jar、commons-dbcp-1.2.1.jar、commons-pool-1.3.jar、數據庫驅動包、cas對jdbc的支持包cas-server-support-jdbc-4.0.0.jar,需要找到這幾個jar包放進 %TOMCAT_HOME%/webapps/cas/WEB-INF/lib 目錄。

然後便可以啓動Tomcat使用數據庫中的用戶名和密碼進行登錄測試了。
注意:DataSource 依賴於 commons-collections-3.2.jar、commons-dbcp-1.2.1.jar、commons-pool-1.3.jar、數據庫驅動包、cas對jdbc的支持包cas-server-support-jdbc-4.0.0.jar,需要找到這幾個jar包放進 %TOMCAT_HOME%/webapps/cas/WEB-INF/lib 目錄。

然後便可以啓動Tomcat使用數據庫中的用戶名和密碼進行登錄測試了。

9.部署CAS客戶端應用

單點登錄的目的是爲了讓多個相關聯的應用使用相同的登錄過程,本文在講解過程中構造 2個簡單的應用,分別以 CASClient1 和 CASClient2 來作爲示例,它們均只有一個頁面,顯示歡迎信息和當前登錄用戶名。這兩個應用使用同一套登錄信息,並且只有登錄過的用戶才能訪問,通過本文的配置,實現單點登錄,即只需登錄一次就可以訪問這兩個應用。應用的工程結構很簡單,引入必要的jar包,然後新建一個index.jsp頁面。


index.jsp代碼清單

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>CASClient1's index.jsp</title>
</head>
<body>
<% 
/*獲取登錄用戶名*/
String username = request.getRemoteUser();

%>
current user: <%=username %>
<p><a href="https://localhost:8443/cas/logout">logout</a></p>
</body>
</html>

10.配置CAS客戶端Filter

在CAS的client端的web.xml文件中配置過濾器如下:

<!-- 該過濾器用於實現單點登出功能,可選配置。 -->
<!-- 登出地址 https://casserver:8443/cas/logout -->
<filter>
	<filter-name>CAS Single Sign Out Filter</filter-name>
	<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
</filter>

<!-- 該過濾器負責用戶的認證工作,必須啓用它 -->
<filter>
	<filter-name>CAS Authentication Filter</filter-name>
	<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
	<init-param>
		<param-name>casServerLoginUrl</param-name>
		<param-value>https://localhost:8443/cas/login</param-value>
	</init-param>
	<init-param>
		<param-name>serverName</param-name>
		<param-value>http://localhost:8080</param-value>
	</init-param>
</filter>

<!-- 該過濾器負責對Ticket的校驗工作,必須啓用它 -->
<filter>
	<filter-name>CAS Validation Filter</filter-name>
	<filter-class>org.jasig.cas.client.validation.Cas10TicketValidationFilter</filter-class>
	<init-param>
		<param-name>casServerUrlPrefix</param-name>
		<param-value>https://localhost:8443/cas</param-value>
	</init-param>
	<init-param>
		<param-name>serverName</param-name>
		<param-value>http://localhost:8080</param-value>
	</init-param>
	<init-param>
		<param-name>redirectAfterValidation</param-name>
		<param-value>true</param-value>
	</init-param>
</filter>

<!-- 該過濾器負責實現HttpServletRequest請求的包裝, 比如允許開發者通過HttpServletRequest的getRemoteUser()方法獲得SSO登錄用戶的登錄名,可選配置。 -->
<filter>
	<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
	<filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
</filter>

<!-- 該過濾器使得開發者可以通過org.jasig.cas.client.util.AssertionHolder來獲取用戶的登錄名。 比如AssertionHolder.getAssertion().getPrincipal().getName()。 -->
<filter>
	<filter-name>CAS Assertion Thread Local Filter</filter-name>
	<filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
</filter>

<filter-mapping>
	<filter-name>CAS Single Sign Out Filter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
	<filter-name>CAS Authentication Filter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
	<filter-name>CAS Validation Filter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
	<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
	<filter-name>CAS Assertion Thread Local Filter</filter-name>
	<url-pattern>/*</url-pattern>
</filter-mapping>

<listener>
	<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>

運行Client工程,首次訪問任一頁面就會跳轉到https://localhost:8443/cas/login進行認證。 

把你的退出鏈接設置爲:https://localhost:8443/cas/logout 即可實現單點登出,如果需要在系統退出後返回指定頁面,我們可以修改源碼在logout後面增加參數來實現。

11.單點登錄測試同時運行CAS server、 CASClient1 和CASClient2。分別訪問下面的URL:

CASClient1: localhost:8080/CASClient1/index.jsp

CASClient2: localhost:8080/CASClient2/index.jsp

當第一次訪問上面的URL時,會重定向到下面的頁面:


這時輸入數據庫中的用戶名和對應的密碼登錄,登錄成功後會看到如下頁面:


此時訪問第二個URL發現index頁面會直接打開,而不用再次登錄,因爲訪問CASClient1的時候已經認證過了。這就實現了所謂的單點登錄。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章