建立使用Cas進行單點登錄的應用
目錄
1.1加入cas-client-core-xxx.jar到classpath
1.2配置Filter
1.2.1AuthenticationFilter
1.2.3HttpServletRequestWrapperFilter
1.2.4AssertionThreadLocalFilter
1.2.5基於Spring的Filter配置
1.3添加證書到信任庫
根據之前的描述我們知道,Cas由兩部分組成,Cas Server和Cas Client。Cas Server是Cas自己的服務端,而Cas Client是Cas客戶端,其需要與我們自己的應用進行集成。
1.1 加入cas-client-core-xxx.jar到classpath
在我們下載的Cas Client壓縮包的modules目錄下可以找到一個名爲cas-client-core-xxx.jar的jar文件,首先需要將該jar包加入我們應用的類路徑下,筆者這裏使用的是cas-client-core-3.1.11.jar。如果用戶的應用是使用Maven構造的,則可以在應用的pom.xml文件中加入如下依賴。
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.1.11</version>
</dependency>
1.2 配置Filter
然後需要我們在應用的web.xml文件中配置四個Filter,這四個Filter必須按照固定的順序來進行配置,而且它們必須配置在應用的其它Filter之前。它們的先後順序要求如下:
l AuthenticationFilter
l TicketValidationFilter
l HttpServletRequestWrapperFilter
l AssertionThreadLocalFilter
這些Filter有的必須指定某些參數,有的可以指定某些參數,這些參數可以通過context-param來指定,也可以通過init-param來指定。Cas Client默認會先從init-param取,沒取到則從context-param取,所以當init-param和context-param都指定了某個參數時,init-param指定的將擁有更高的優先級。所以當多個Filter需要共用一個參數時,我們可以把它定義爲context-param。
1.2.1 AuthenticationFilter
AuthenticationFilter用來攔截所有的請求,用以判斷用戶是否需要通過Cas Server進行認證,如果需要則將跳轉到Cas Server的登錄頁面。如果不需要進行登錄認證,則請求會繼續往下執行。
AuthenticationFilter有兩個用戶必須指定的參數,一個是用來指定Cas Server登錄地址的casServerLoginUrl,另一個是用來指定認證成功後需要跳轉地址的serverName或service。service和serverName只需要指定一個就可以了。當兩者都指定了,參數service將具有更高的優先級,即將以service指定的參數值爲準。service和serverName的區別在於service指定的是一個確定的URL,認證成功後就會確切的跳轉到service指定的URL;而serverName則是用來指定主機名,其格式爲{protocol}:{hostName}:{port},如:https://localhost:8443,當指定的是serverName時,AuthenticationFilter將會把它附加上當前請求的URI,以及對應的查詢參數來構造一個確定的URL,如指定serverName爲“http://localhost”,而當前請求的URI爲“/app”,查詢參數爲“a=b&b=c”,則對應認證成功後的跳轉地址將爲“http://localhost/app?a=b&b=c”。
除了上述必須指定的參數外,AuthenticationFilter還可以指定如下可選參數:
l renew:當指定renew爲true時,在請Cas Server時將帶上參數“renew=true”,默認爲false。
l gateway:指定gateway爲true時,在請求Cas Server時將帶上參數“gateway=true”,默認爲false。
l artifactParameterName:指定ticket對應的請求參數名稱,默認爲ticket。
l serviceParameterName:指定service對應的請求參數名稱,默認爲service。
如下是一個配置AuthenticationFilter的示例,serverName由於在接下來配置的Filter中還要用,所以利用context-param將其配置爲一個公用的參數。“elim”對應我的電腦名。
<context-param>
<param-name>serverName</param-name>
<param-value>http://elim:8080</param-value>
</context-param>
<filter>
<filter-name>casAuthenticationFilter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>https://elim:8443/cas/login</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>casAuthenticationFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
1.2.2 TicketValidationFilter
在請求通過AuthenticationFilter的認證之後,如果請求中攜帶了參數ticket則將會由TicketValidationFilter來對攜帶的ticket進行校驗。TicketValidationFilter只是對驗證ticket的這一類Filter的統稱,其並不對應Cas Client中的一個具體類型。Cas Client中有多種驗證ticket的Filter,都繼承自AbstractTicketValidationFilter,它們的驗證邏輯都是一致的,都有AbstractTicketValidationFilter實現,所不同的是使用的TicketValidator不一樣。筆者這裏將以Cas10TicketValidationFilter爲例,其它還有Cas20ProxyReceivingTicketValidationFilter和Saml11TicketValidationFilter。
<filter>
<filter-name>casTicketValidationFilter</filter-name>
<filter-class>org.jasig.cas.client.validation.Cas10TicketValidationFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>https://elim:8443/cas</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>casTicketValidationFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
必須指定的參數:
l casServerUrlPrefix:用來指定Cas Server對應URL地址的前綴,如上面示例的“https://elim:8443/cas”。
l serverName或service:語義跟前面介紹的一致。
可選參數:
l redirectAfterValidation :表示是否驗證通過後重新跳轉到該URL,但是不帶參數ticket,默認爲true。
l useSession :在驗證ticket成功後會生成一個Assertion對象,如果useSession爲true,則會將該對象存放到Session中。如果爲false,則要求每次請求都需要攜帶ticket進行驗證,顯然useSession爲false跟redirectAfterValidation爲true是衝突的。默認爲true。
l exceptionOnValidationFailure :表示ticket驗證失敗後是否需要拋出異常,默認爲true。
l renew:當值爲true時將發送“renew=true”到Cas Server,默認爲false。
1.2.3 HttpServletRequestWrapperFilter
HttpServletRequestWrapperFilter用於將每一個請求對應的HttpServletRequest封裝爲其內部定義的CasHttpServletRequestWrapper,該封裝類將利用之前保存在Session或request中的Assertion對象重寫HttpServletRequest的getUserPrincipal()、getRemoteUser()和isUserInRole()方法。這樣在我們的應用中就可以非常方便的從HttpServletRequest中獲取到用戶的相關信息。以下是一個配置HttpServletRequestWrapperFilter的示例:
<filter>
<filter-name>casHttpServletRequestWrapperFilter</filter-name>
<filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>casHttpServletRequestWrapperFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
1.2.4 AssertionThreadLocalFilter
AssertionThreadLocalFilter是爲了方便用戶在應用的其它地方獲取Assertion對象,其會將當前的Assertion對象存放到當前的線程變量中,那麼以後用戶在程序的任何地方都可以從線程變量中獲取當前Assertion,無需再從Session或request中進行解析。該線程變量是由AssertionHolder持有的,我們在獲取當前的Assertion時也只需要通過AssertionHolder的getAssertion()方法獲取即可,如:
Assertion assertion = AssertionHolder.getAssertion();
像AssertionThreadLocalFilter這種設計理念是非常好的,實際應用中使用的也比較多,Spring Security中也有用到這種理念。爲了便於大家瞭解,特貼出AssertionHolder的源碼如下:
public class AssertionHolder {
/**
* ThreadLocal to hold the Assertion for Threads to access.
*/
private static final ThreadLocal threadLocal = new ThreadLocal();
/**
* Retrieve the assertion from the ThreadLocal.
*/
public static Assertion getAssertion() {
return (Assertion) threadLocal.get();
}
/**
* Add the Assertion to the ThreadLocal.
*/
public static void setAssertion(final Assertion assertion) {
threadLocal.set(assertion);
}
/**
* Clear the ThreadLocal.
*/
public static void clear() {
threadLocal.set(null);
}
}
以下是配置AssertionThreadLocalFilter的示例:
<filter>
<filter-name>casAssertionThreadLocalFilter</filter-name>
<filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>casAssertionThreadLocalFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
1.2.5 基於Spring的Filter配置
使用Cas單點登錄的應用需要我們在應用的web.xml文件中配置上述介紹的四個Filter,但如果用戶的應用是使用Spring開發的,則我們可以只在web.xml文件中配置四個Spring的DelegatingFilterProxy用來代理需要配置的四個Filter,對應的Filter名稱對應我們需要代理的Spring ApplicationContext中bean的名稱,此時我們需要將對應的Filter配置爲Spring ApplicationContext中的一個bean對象。所以此時對應的web.xml文件的定義應該是這樣的:
<filter>
<filter-name>casAuthenticationFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>casAuthenticationFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>casTicketValidationFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>casTicketValidationFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>casHttpServletRequestWrapperFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>casHttpServletRequestWrapperFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
<filter-name>casAssertionThreadLocalFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>casAssertionThreadLocalFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
而對應的Filter應該都以對應的名稱定義爲Spring ApplicationContext中的一個bean。
<bean name="casAuthenticationFilter"
class="org.jasig.cas.client.authentication.AuthenticationFilter"
p:casServerLoginUrl="https://elim:8443/cas/login" p:renew="false"
p:gateway="false" p:serverName="http://elim:8080" />
<bean name="casTicketValidationFilter"
class="org.jasig.cas.client.validation.Cas10TicketValidationFilter"
p:serverName="http://elim:8080" p:redirectAfterValidation="true">
<property name="ticketValidator">
<bean class="org.jasig.cas.client.validation.Cas10TicketValidator">
<!-- 對應於casServerUrlPrefix -->
<constructor-arg index="0" value="https://elim:8443/cas" />
</bean>
</property>
</bean>
<bean id="casHttpServletRequestWrapperFilter" class="org.jasig.cas.client.util.HttpServletRequestWrapperFilter"/>
<bean id="casAssertionThreadLocalFilter" class="org.jasig.cas.client.util.AssertionThreadLocalFilter"/>
1.3 添加證書到信任庫
在ticket驗證成功後,還需要驗證證書,這需要我們將之前建立的證書導出並添加到當前JRE的證書信任庫中,否則將驗證失敗。JRE在尋找證書時將根據當前使用的host來尋找,且會用該host匹配之前創建證書時指定的用戶名稱,如果匹配則表示找到。這也就意味着我們在創建證書時指定的用戶名稱需要是我們的host。我的機器名稱爲“elim”,我就把它作爲我的host,那麼對應的證書應該這樣創建。
keytool -genkey -keyalg RSA -alias tomcat -dname "cn=elim" -storepass changeit
該語句是對我們之前介紹的keytool -genkey -alias tomcat -keyalg RSA的精寫,它已經通過相應的參數指定了對應的參數值,而不需要再與用戶交互了。如果還用之前的語句生成證書的話,那麼對應的值應該這樣填:
之後會在用戶的對應目錄下生成一個.keystore文件。之後需要將該文件導出爲一個證書到%JAVA_HOME%/jre/lib/security目錄下,對應指令爲:
keytool -export -alias tomcat -file %JAVA_HOME%/jre/lib/security/tomcat.crt -storepass changeit
之後需要將導出的tomcat.crt證書添加到運行時使用的JRE的受信任證書庫中,此時如果出現異常可將原本%JAVA_HOME%/jre/lib/security目錄下的cacerts刪除後繼續執行以下指令。
keytool -import -alias tomcat -file %JAVA_HOME%/jre/lib/security/tomcat.crt -keystore %JAVA_HOME%/jre/lib/security/cacerts -storepass changeit
經過以上幾步後就可以啓用我們自己的Cas Client應用了,然後初次訪問該應用時就會跳轉到Cas Server進行登錄認證。認證成功後將跳轉到我們自己的Client應用進行ticket的驗證,驗證通過後就可以自由的訪問我們的Client應用了。
(注:本文是基於Cas Server3.5.2和Cas Client3.1.11所寫)
(注:原創文章,轉載請註明出處。原文地址:http://haohaoxuexi.iteye.com/blog/2142631)