1. 程式人生 > >CAS SSO學習筆記

CAS SSO學習筆記

CAS的結構:

    從結構上看,CAS 包含兩個部分: CAS Server 和 CAS Client。CAS Server 需要獨立部署,主要負責對使用者的認證工作;CAS Client 負責處理對客戶端受保護資源的訪問請求,需要登入時,重定向到 CAS Server。

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傳輸需要https協議,那麼就要為應用環境配置相應的SSL協議用來支援https協議(tomcat伺服器需要配置SSL協議)。在生成證書的過程中,會有需要用到主機名的地方,CAS 建議不要使用 IP 地址,而要使用機器名或域名。


CAS是一個web框架,所以需要將其部署到伺服器(tomcat)下,釋出啟動。


CAS部署成功後,要對其進行拓展和定製:

    1.拓展認證介面:CAS Server 負責完成對使用者的認證工作,它會處理登入時的使用者憑證 (Credentials) 資訊,使用者名稱/密碼對是最常見的憑證資訊。CAS Server 可能需要到資料庫檢索一條使用者帳號資訊,也可能在 XML 檔案中檢索使用者名稱/密碼,還可能通過 LDAP Server 獲取等,在這種情況下,CAS 提供了一種靈活但統一的介面和實現分離的方式,實際使用中 CAS 採用哪種方式認證是與 CAS 的基本協議分離開的,使用者可以根據認證的介面去定製和擴充套件。CAS 提供擴充套件認證的核心是 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() 方法前後執行某些其他操作,那麼可以實現下面介面:

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 提供了針對該方式的實現:

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。


JDBC認證方法:

    使用者的認證資訊通常儲存在資料庫中,因此本文就選用這種情況來介紹。將前面下載的 cas-server-3.1.1-release.zip 包解開後,在 modules 目錄下可以找到包 cas-server-support-jdbc-3.1.1.jar,其提供了通過 JDBC 連線資料庫進行驗證的預設實現,基於該包的支援,我們只需要做一些配置工作即可實現 JDBC 認證。

    JDBC 認證方法支援多種資料庫,DB2, Oracle, MySql, Microsoft SQL Server 等均可,這裡以 DB2 作為例子介紹。並且假設DB2資料庫名: CASTest,資料庫登入使用者名稱: db2user,資料庫登入密碼: db2password,使用者資訊表為: userTable,該表包含使用者名稱和密碼的兩個資料項分別為 userName 和 password。

    1.配置CAS的資料庫配置檔案:

    開啟檔案 %CATALINA_HOME%/webapps/cas/WEB-INF/deployerConfigContext.xml,新增一個新的 bean 標籤,對於 DB2,內容如清單 4 所示:

<bean id="casDataSource" class="org.apache.commons.dbcp.BasicDataSource">
     <property name="driverClassName">
          <value>com.ibm.db2.jcc.DB2Driver</value>
     </property>
     <property name="url">
          <value>jdbc:db2://9.125.65.134:50000/CASTest</value>
     </property>
     <property name="username">
          <value>db2user</value>
     </property>
     <property name="password">
          <value>db2password</value>
     </property>
</bean>

    其中 id 屬性為該 DataStore 的標識,在後面配置 AuthenticationHandler 會被引用,另外,需要提供 DataStore 所必需的資料庫驅動程式、連線地址、資料庫登入使用者名稱以及登入密碼。

    2.配置AuthenticationHandler:

    在 cas-server-support-jdbc-3.1.1.jar 包中,提供了 3 個基於 JDBC 的 AuthenticationHandler,分別為 BindModeSearchDatabaseAuthenticationHandler, QueryDatabaseAuthenticationHandler, SearchModeSearchDatabaseAuthenticationHandler。其中 BindModeSearchDatabaseAuthenticationHandler 是用所給的使用者名稱和密碼去建立資料庫連線,根據連線建立是否成功來判斷驗證成功與否;QueryDatabaseAuthenticationHandler 通過配置一個 SQL 語句查出密碼,與所給密碼匹配;SearchModeSearchDatabaseAuthenticationHandler 通過配置存放使用者驗證資訊的表、使用者名稱欄位和密碼欄位,構造查詢語句來驗證。

    使用哪個 AuthenticationHandler,需要在 deployerConfigContext.xml 中設定,預設情況下,CAS 使用一個簡單的 username=password 的 AuthenticationHandler,在檔案中可以找到如下一行:<bean class="org.jasig.cas.authentication.handler.support.SimpleTestUsernamePassword
AuthenticationHandler" />,我們可以將其註釋掉,換成我們希望的一個 AuthenticationHandler

案例一:

<bean class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
 <property name="dataSource" ref=" casDataSource " />
 <property name="sql" 
       value="select password from userTable where lower(userName) = lower(?)" />
</bean>

案例二:

<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:

<bean class="org.jasig.cas.adaptors.jdbc.QueryDatabaseAuthenticationHandler">
  <property name="dataSource" ref=" casDataSource " />
  <property name="sql" 
           value="select password from userTable where lower(userName) = lower(?)" />
  <property  name="passwordEncoder"  ref="myPasswordEncoder"/>
</bean>
<bean id="myPasswordEncoder" 
            class="org.jasig.cas.authentication.handler.MyPasswordEncoder"/>


部署依賴包:

    在以上配置完成以後,需要拷貝幾個依賴的包到 cas 應用下,包括:
    1.將 cas-server-support-jdbc-3.1.1.jar 拷貝到 %CATALINA_HOME%/webapps/cas/ WEB-INF/lib 目錄。
    2.資料庫驅動,由於這裡使用 DB2,將 %DB2_HOME%/java 目錄下的 db2java.zip (更名為 db2java.jar), db2jcc.jar, db2jcc_license_cu.jar 拷貝到 %CATALINA_HOME%/webapps/cas/WEB-INF/lib 目錄。對於其他資料庫,同樣將相應資料庫驅動程式拷貝到該目錄。
    3.DataStore 依賴於 commons-collections-3.2.jar, commons-dbcp-1.2.1.jar, commons-pool-1.3.jar,需要到 apache 網站的 Commons 專案下載以上 3 個包放進 %CATALINA_HOME%/webapps/cas/WEB-INF/lib 目錄。


拓展CAS Server介面:

    CAS 提供了 2 套預設的頁面,分別為“ default ”和“ simple ”,分別在目錄“ cas/WEB-INF/view/jsp/default ”和“ cas/WEB-INF/view/jsp/simple ”下。其中 default 是一個稍微複雜一些的頁面,使用 CSS,而 simple 則是能讓 CAS 正常工作的最簡化的頁面。
    在部署 CAS 之前,我們可能需要定製一套新的 CAS Server 頁面,新增一些個性化的內容。最簡單的方法就是拷貝一份 default 或 simple 檔案到“ cas/WEB-INF/view/jsp ”目錄下,比如命名為 newUI,接下來是實現和修改必要的頁面,有 4 個頁面是必須的:
    1.casConfirmView.jsp: 當用戶選擇了“ warn ”時會看到的確認介面
    2.casGenericSuccess.jsp: 在使用者成功通過認證而沒有目的Service時會看到的介面
    3.casLoginView.jsp: 當需要使用者提供認證資訊時會出現的介面
    4.casLogoutView.jsp: 當用戶結束 CAS 單點登入系統會話時出現的介面
    頁面定製完過後,還需要做一些配置從而讓 CAS 找到新的頁面,拷貝“ cas/WEB-INF/classes/default_views.properties ”,重新命名為“ cas/WEB-INF/classes/ newUI_views.properties ”,並修改其中所有的值到相應新頁面。最後是更新“ cas/WEB-INF/cas-servlet.xml ”檔案中的 viewResolver:

<bean id="viewResolver" 
     class="org.springframework.web.servlet.view.ResourceBundleViewResolver" p:order="0">
    <property name="basenames">
        <list>
            <value>${cas.viewResolver.basename}</value>
            <value> newUI_views</value>
        </list>
    </property>
</bean>


部署客戶端應用:

    1.與 CAS Server 建立信任關係
    假設 CAS Server 單獨部署在一臺機器 A,而客戶端應用部署在機器 B 上,由於客戶端應用與 CAS Server 的通訊採用 SSL,因此,需要在 A 與 B 的 JRE 之間建立信任關係。
首先與 A 機器一樣,要生成 B 機器上的證書,配置 Tomcat 的 SSL 協議。其次,下載http://blogs.sun.com/andreas/entry/no_more_unable_to_find 的 InstallCert.java,執行“ java InstallCert compA:8443 ”命令,並且在接下來出現的詢問中輸入 1。這樣,就將 A 新增到了 B 的 trust store 中。如果多個客戶端應用分別部署在不同機器上,那麼每個機器都需要與 CAS Server 所在機器建立信任關係。

    2.配置 CAS Filter
    準備好應用 casTest1(實驗案例) 和 casTest2(實驗案例) 過後,分別部署在 B 和 C 機器上,由於 casTest1 和casTest2,B 和 C 完全等同,我們以 casTest1 在 B 機器上的配置做介紹,假設 A 和 B 的域名分別為 domainA 和 domainB。
    將 cas-client-java-2.1.1.zip 改名為 cas-client-java-2.1.1.jar 並拷貝到 casTest1/WEB-INF/lib目錄下,修改 web.xml 檔案,新增 CAS Filter。

<web-app>
  ...
  <filter>
    <filter-name>CAS Filter</filter-name>
    <filter-class>edu.yale.its.tp.cas.client.filter.CASFilter</filter-class>
    <init-param>
      <param-name>edu.yale.its.tp.cas.client.filter.loginUrl</param-name>
      <param-value>https://domainA:8443/cas/login</param-value>
    </init-param>
    <init-param>
      <param-name>edu.yale.its.tp.cas.client.filter.validateUrl</param-name>
      <param-value>https://domainA:8443/cas/serviceValidate</param-value>
    </init-param>
    <init-param>
      <param-name>edu.yale.its.tp.cas.client.filter.serverName</param-name>
      <param-value>domainB:8080</param-value>
    </init-param>
  </filter>
  <filter-mapping>
    <filter-name>CAS Filter</filter-name>
    <url-pattern>/protected-pattern/*</url-pattern>
  </filter-mapping>
  ...
</web-app>

    對於所有訪問滿足 casTest1/protected-pattern/ 路徑的資源時,都要求到 CAS Server 登入,如果需要整個 casTest1 均受保護,可以將 url-pattern 指定為“/*”。

    必須引數:

edu.yale.its.tp.cas.client.filter.loginUrl 指定 CAS 提供登入頁面的 URL
edu.yale.its.tp.cas.client.filter.validateUrl 指定 CAS 提供 service ticket 或 proxy ticket 驗證服務的 URL
edu.yale.its.tp.cas.client.filter.serverName 指定客戶端的域名和埠,是指客戶端應用所在機器而不是 CAS Server 所在機器,該引數或 serviceUrl 至少有一個必須指定
edu.yale.its.tp.cas.client.filter.serviceUrl 該引數指定過後將覆蓋 serverName 引數,成為登入成功過後重定向的目的地址

   可選引數:

edu.yale.its.tp.cas.client.filter.proxyCallbackUrl 用於當前應用需要作為其他服務的代理(proxy)時獲取 Proxy Granting Ticket 的地址
edu.yale.its.tp.cas.client.filter.authorizedProxy 用於允許當前應用從代理處獲取 proxy tickets,該引數接受以空格分隔開的多個 proxy URLs,但實際使用只需要一個成功即可。當指定該引數過後,需要修改 validateUrl 到 proxyValidate,而不再是 serviceValidate
edu.yale.its.tp.cas.client.filter.renew 如果指定為 true,那麼受保護的資源每次被訪問時均要求使用者重新進行驗證,而不管之前是否已經通過
edu.yale.its.tp.cas.client.filter.wrapRequest 如果指定為 true,那麼 CASFilter 將重新包裝 HttpRequest,並且使 getRemoteUser() 方法返回當前登入使用者的使用者名稱
edu.yale.its.tp.cas.client.filter.gateway 指定 gateway 屬性

傳遞使用者名稱:

    CAS 在登入成功過後,會給瀏覽器回傳 Cookie,設定新的到的 Service Ticket。但客戶端應用擁有各自的 Session,我們要怎麼在各個應用中獲取當前登入使用者的使用者名稱呢?CAS Client 的 Filter 已經做好了處理,在登入成功後,就可以直接從 Session 的屬性中獲取。

// 以下兩者都可以
session.getAttribute(CASFilter.CAS_FILTER_USER);
session.getAttribute("edu.yale.its.tp.cas.client.filter.user");

    在 JSTL 中獲取使用者名稱

<c:out value="${sessionScope[CAS:'edu.yale.its.tp.cas.client.filter.user']}"/>

    另外,CAS 提供了一個 CASFilterRequestWrapper 類,該類繼承自HttpServletRequestWrapper,主要是重寫了 getRemoteUser() 方法,只要在前面配置 CASFilter 的時候為其設定“ edu.yale.its.tp.cas.client.filter.wrapRequest ”引數為 true,就可以通過 getRemoteUser() 方法來獲取登入使用者名稱

CASFilterRequestWrapper  reqWrapper=new CASFilterRequestWrapper(request);
out.println("The logon user:" + reqWrapper.getRemoteUser());

 

效果:

    在 casTest1 和 casTest2 中,都有一個簡單 Servlet 作為歡迎頁面 WelcomPage,且該頁面必須登入過後才能訪問:

public class WelcomePage extends HttpServlet {
  public void doGet(HttpServletRequest request, HttpServletResponse response)
  throws IOException, ServletException
  {
    response.setContentType("text/html");
    PrintWriter out = response.getWriter();
    out.println("<html>");
    out.println("<head>");
    out.println("<title>Welcome to casTest2 sample System!</title>");
    out.println("</head>");
    out.println("<body>");
    out.println("<h1>Welcome to casTest1 sample System!</h1>");
    CASFilterRequestWrapper  reqWrapper=new CASFilterRequestWrapper(request);
    out.println("<p>The logon user:" + reqWrapper.getRemoteUser() + "</p>");
    HttpSession session=request.getSession();
    out.println("<p>The logon user:" + 
                   session.getAttribute(CASFilter.CAS_FILTER_USER)  + "</p>");
    out.println("<p>The logon user:" + 
         session.getAttribute("edu.yale.its.tp.cas.client.filter.user") + "</p>");
    out.println("</body>");
    out.println("</html>");
    }
}

    在上面所有配置結束過後,分別在 A, B, C上啟動 cas, casTest1 和 casTest2。

    1.訪問 http://domainB:8080/casTest1/WelcomePage ,瀏覽器會彈出安全提示,接受後即轉到 CAS 的登入頁面:

CAS 登入頁面

    2.重定向casTest1 的 WelcomePage 頁面:

登入後訪問 casTest1 的效果

    位址列裡的地址多出了一個 ticket 引數,這就是 CAS 分配給當前應用的 ST(Service Ticket)。   

    3.同一個瀏覽器的位址列中輸入 http://domainC:8080/casTest2/WelcomePage ,系統不再提示使用者登入:

在 casTest1 中登入過後訪問 casTest2 的效果