1. 程式人生 > >【試水CAS-4.0.3】第07節_CAS客戶端配置單點登入

【試水CAS-4.0.3】第07節_CAS客戶端配置單點登入

完整版見https://jadyer.github.io/2015/07/26/sso-cas-client-login/

/**
 * @see CAS客戶端配置
 * @see ------------------------------------------------------------------------------------------------------------------------
 * @see 這裡用的是cas-client-core-3.4.0.jar(這是2015-07-21釋出的)
 * @see 下載地址http://mvnrepository.com/artifact/org.jasig.cas.client/cas-client-core/3.4.0
 * @see 另外為了使客戶端在HTTP協議下單點成功,可以修改以下兩處配置使其不開啟HTTPS驗證
 * @see 1.\WEB-INF\deployerConfigContext.xml
 * @see   <bean class="org.jasig...support.HttpBasedServiceCredentialsAuthenticationHandler">新增p:requireSecure="false"
 * @see 2.\WEB-INF\spring-configuration\ticketGrantingTicketCookieGenerator.xml和\WEB-INF\spring-configuration\warnCookieGenerator.xml
 * @see   p:cookieSecure="true"改為p:cookieSecure="false"
 * @see 下面介紹兩種配置方法,一種是純web.xml配置,一種是藉助Spring來配置,相關的官方文件如下所示
 * @see https://wiki.jasig.org/display/CASC/Configuring+the+Jasig+CAS+Client+for+Java+in+the+web.xml
 * @see https://wiki.jasig.org/display/CASC/Configuring+the+JA-SIG+CAS+Client+for+Java+using+Spring
 * @see ------------------------------------------------------------------------------------------------------------------------
 * @see 純web.xml
 * @see web.xml中需配置四個順序固定的Filter,而且出於認證考慮,最好配置在其他Filter之前,它們的先後順序如下
 * @see AuthenticationFilter
 * @see TicketValidationFilter(或其它AbstractTicketValidationFilter實現,比如Cas20ProxyReceivingTicketValidationFilter)
 * @see HttpServletRequestWrapperFilter
 * @see AssertionThreadLocalFilter
 * @see 另外各個Filter的<init-param>優先順序都比<context-param>要高,通常<context-param>用來配置公用的引數
 * @see 1.AuthenticationFilter
 * @see   用來攔截請求,判斷是否需要CASServer認證,需要則跳轉到CASServer登入頁,否則放行請求
 * @see   有兩個必須引數,一個是指定CASServer登入地址的casServerLoginUrl,另一個是指定認證成功後跳轉地址的serverName或service
 * @see   service和serverName設定一個即可,二者都設定時service的優先順序更高,即會以service為準
 * @see   service指的是一個確切的URL,而serverName是用來指定客戶端的主機名的,格式為{protocol}:{hostName}:{port}
 * @see   指定serverName時,該Filter會把它附加上當前請求的URI及對應的查詢引數來構造一個確切的URL作為認證成功後的跳轉地址
 * @see   比如serverName為"http://gg.cn",當前請求的URI為"/oa",查詢引數為"aa=bb",則認證成功後跳轉地址為http://gg.cn/oa?aa=bb
 * @see   casServerLoginUrl--去哪登入,serverName--我是誰
 * @see 2.TicketValidationFilter
 * @see   請求通過AuthenticationFilter認證後,若請求中攜帶了ticket引數,則會由該類Filter對攜帶的ticket進行校驗
 * @see   驗證ticket的時候,要訪問CAS服務的/serviceValidate介面,使用的url就是${casServerUrlPrefix}/serviceValidate
 * @see   所以它也有兩個引數是必須指定的,casServerUrlPrefix(CASServer對應URL地址的字首)和serverName或service
 * @see   實際上,TicketValidationFilter只是對驗證ticket的這一類Filter的統稱,其並不對應CASClient中的具體型別
 * @see   CASClient中有多種驗證ticket的Filter,都繼承自AbstractTicketValidationFilter
 * @see   常見的有Cas10TicketValidationFilter/Cas20ProxyReceivingTicketValidationFilter/Saml11TicketValidationFilter
 * @see   它們的驗證邏輯都是一致的,都有AbstractTicketValidationFilter實現,只是使用的TicketValidator不一樣而已
 * @see   如果要從伺服器獲取使用者名稱之外的更多資訊應該採用CAS20這個2.0協議的代理
 * @see 3.HttpServletRequestWrapperFilter
 * @see   用於封裝每個請求的HttpServletRequest為其內部定義的CasHttpServletRequestWrapper
 * @see   它會將儲存在Session或request中的Assertion物件重寫HttpServletRequest的getUserPrincipal()、getRemoteUser()、isUserInRole()
 * @see   這樣在我們的應用中就可以非常方便的從HttpServletRequest中獲取到使用者的相關資訊
 * @see 4.AssertionThreadLocalFilter
 * @see   為了方便使用者在應用的其它地方獲取Assertion物件,其會將當前的Assertion物件存放到當前的執行緒變數中
 * @see   以後使用者在程式的任何地方都可以從執行緒變數中獲取當前的Assertion,而無需從Session或request中解析
 * @see   該執行緒變數是由AssertionHolder持有的,我們在獲取當前的Assertion時也只需Assertion assertion = AssertionHolder.getAssertion()
 * @see ------------------------------------------------------------------------------------------------------------------------
 * @see 藉助Spring
 * @see 與上述web.xml配置四個Filter方式不同的是,可以使用Spring的四個DelegatingFilterProxy來代理需要配置的四個Filter
 * @see 此時這四個Filter就應該配置為Spring的Bean物件,並且web.xml中的<filter-name>就應該對應SpringBean名稱
 * @see 但是SingleSignOutFilter/HttpServletRequestWrapperFilter/AssertionThreadLocalFilter等Filter不含配置引數
 * @see 所以實際上只需要配置AuthenticationFilter和Cas20ProxyReceivingTicketValidationFilter兩個Filter交由Spring代理就可以了
 * @see ------------------------------------------------------------------------------------------------------------------------
 * @see 注意
 * @see 1.CAS1.0提供的介面有/validate,CAS2.0提供的介面有/serviceValidate,/proxyValidate,/proxy
 * @see 2.四個Filter太多了,有時間的話考慮參考org.springframework.web.filter.CompositeFilter寫一個Filter來實現
 * @see 3.web.xml的好處是可以配置匿名訪問的資源,配置引數參考AuthenticationFilter中的ignoreUrlPatternMatcherStrategyClass
 * @see   起碼cas-client-core-3.4.0.jar中的Spring配置還不支援ignorePattern(該引數預設正則驗證,此外還有contains和equals驗證)
 * @see 4.javax.net.ssl.SSLHandshakeException: java.security.cert.CertificateException: No name matching casserver found
 * @see   這是由於建立證書的域名和應用中配置的CAS服務域名不一致導致出錯(說白了就是指客戶端匯入的CRT證書與CAS服務端的域名不同)
 * @see ------------------------------------------------------------------------------------------------------------------------
 * @create 2015-7-26 下午1:00:14
 * @author 玄玉<http://blog.csdn.net/jadyer>
 */

下面是web.xml的配置

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" 
	xmlns="http://java.sun.com/xml/ns/javaee" 
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
	http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath:applicationContext.xml</param-value>
	</context-param>
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
	
	<servlet>
		<servlet-name>SpringMVC</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>classpath:applicationContext.xml</param-value>
		</init-param>
	</servlet>
	<servlet-mapping>
		<servlet-name>SpringMVC</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>
	
	<filter>
		<filter-name>CharacterEncodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
		<init-param>
			<param-name>forceEncoding</param-name>
			<param-value>true</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>CharacterEncodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

	<!-- SSO -->
	<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>
	<!-- 
	<context-param>
		<param-name>serverName</param-name>
		<param-value>http://boss.jadyer.com: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>http://sso.jadyer.com:8080/cas-server-web/login</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>casAuthenticationFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	<filter>
		<filter-name>casTicketValidationFilter</filter-name>
		<filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
		<init-param>
			<param-name>casServerUrlPrefix</param-name>
			<param-value>http://sso.jadyer.com:8080/cas-server-web</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>casTicketValidationFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	 -->
	<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>
	<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>
</web-app>
下面是//src//applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xsi:schemaLocation="http://www.springframework.org/schema/beans 
						http://www.springframework.org/schema/beans/spring-beans-3.2.xsd 
						http://www.springframework.org/schema/mvc
						http://www.springframework.org/schema/mvc/spring-mvc-3.2.xsd">
	<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
		<property name="systemPropertiesModeName" value="SYSTEM_PROPERTIES_MODE_OVERRIDE"/>
		<property name="ignoreResourceNotFound" value="false"/>
		<property name="locations">
			<list>
				<value>classpath:config.properties</value>
			</list>
		</property>
	</bean>
	<mvc:resources mapping="/index.jsp" location="/index.jsp"/>
	
	<!-- cas -->
	<bean name="casAuthenticationFilter" class="org.jasig.cas.client.authentication.AuthenticationFilter">
		<property name="serverName" value="${casClientServerName}"/>
		<property name="casServerLoginUrl" value="${casServerLoginUrl}"/>
	</bean>
	<bean name="casTicketValidationFilter" class="org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter">
		<property name="serverName" value="${casClientServerName}"/>
		<property name="ticketValidator">
			<bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
				<constructor-arg index="0" value="${casServerUrlPrefix}"/>
			</bean>
		</property>
	</bean>
</beans>
下面是//src//config.properties
#<<Central Authentication Service>>
#where to login
casServerLoginUrl=http://sso.jadyer.com:8080/cas-server-web/login
#login server root
casServerUrlPrefix=http://sso.jadyer.com:8080/cas-server-web
#who am i
#casClientServerName=http://boss.jadyer.com:8180
casClientServerName=http://risk.jadyer.com:8280
最後是//WebRoot//index.jsp
<%@ page pageEncoding="UTF-8"%>
<%@ page import="java.util.Map"%>
<%@ page import="java.net.URLDecoder"%>
<%@ page import="org.jasig.cas.client.util.AssertionHolder"%>
<%@ page import="org.jasig.cas.client.authentication.AttributePrincipal"%>

<body style="background-color:#CBE0C9;">
	<span style="color:red; font-size:32px; font-weight:bold;">客戶端登入成功</span>
</body>

<hr size="2">

<%
	AttributePrincipal principal = (AttributePrincipal)request.getUserPrincipal();
	Map<String, Object> attributes = principal.getAttributes();
	out.print("principal.getName()=" + principal.getName() + "<br/>");
	out.print("request.getRemoteUser()=" + request.getRemoteUser() + "<br/>");
	out.print("登入使用者:" + attributes.get("userId") + "<br/>");
	out.print("登入時間:" + AssertionHolder.getAssertion().getAuthenticationDate() + "<br/>");
	out.print("-----------------------------------------------------------------------<br/>");
	for(Map.Entry<String,Object> entry : attributes.entrySet()){
		//服務端返回中文時需要encode,客戶端接收顯示中文時需要decode,否則會亂碼
		out.print(entry.getKey() + "=" + URLDecoder.decode(entry.getValue().toString(), "UTF-8") + "<br/>");
	}
	out.print("-----------------------------------------------------------------------<br/>");
	Map<String, Object> attributes22 = AssertionHolder.getAssertion().getAttributes();
	for(Map.Entry<String,Object> entry : attributes22.entrySet()){
		out.print(entry.getKey() + "=" + entry.getValue() + "<br/>");
	}
	out.print("-----------------------------------------------------------------------<br/>");
	Map<String, Object> attributes33 = AssertionHolder.getAssertion().getPrincipal().getAttributes();
	for(Map.Entry<String,Object> entry : attributes33.entrySet()){
		out.print(entry.getKey() + "=" + entry.getValue() + "<br/>");
	}
%>
接下來就可以測試了,測試之前先修改幾處配置,模擬單點環境
/**
 * @see ------------------------------------------------------------------------------------------------------------------------
 * @see 測試時在C:\Windows\System32\drivers\etc\hosts中新增以下三個配置
 * @see 127.0.0.1 sso.jadyer.com
 * @see 127.0.0.1 boss.jadyer.com
 * @see 127.0.0.1 risk.jadyer.com
 * @see ------------------------------------------------------------------------------------------------------------------------
 * @see 然後拷貝三個Tomcat,分別用作sso伺服器和兩個sso客戶端
 * @see 修改兩個sso客戶端的\Tomcat\conf\server.xml的以下三個埠,保證啟動監聽埠不重複
 * @see <Server port="8105" shutdown="SHUTDOWN">
 * @see <Connector port="8180" protocol="HTTP/1.1"......>
 * @see <Connector port="8109" protocol="AJP/1.3" redirectPort="8443" />
 * @see <Server port="8205" shutdown="SHUTDOWN">
 * @see <Connector port="8280" protocol="HTTP/1.1"......>
 * @see <Connector port="8209" protocol="AJP/1.3" redirectPort="8443" />
 * @see ------------------------------------------------------------------------------------------------------------------------
 * @see 最後修改兩個sso客戶端的\Tomcat\webapps\cas-client\WEB-INF\classes\config.properties的casClientServerName值
 * @see casClientServerName=http://boss.jadyer.com:8180
 * @see casClientServerName=http://risk.jadyer.com:8280
 * @see ------------------------------------------------------------------------------------------------------------------------
 * @create 2015-7-26 下午1:08:35
 * @author 玄玉<http://blog.csdn.net/jadyer>
 */

現在開始測試

先訪問http://boss.jadyer.com:8180/cas-client,發現沒登入會自動跳轉到單點登入頁
首次訪問其中一個SSO客戶端


輸入密碼後登入成功

SSO客戶端首次登入成功後的頁面效果


再訪問http://risk.jadyer.com:8280/cas-client,會發現自動登入成功,不用再登入了

第二個SSO客戶端自動登入成功頁面