CAS技術(shù)框架
CAS Server
目前,我們使用的CAS Server 3.1.1的是基于Spring Framework編寫的,因此在CAS服務(wù)器端的配置管理中,絕大多數(shù)是Spring式的Java Bean XML配置。CAS 的服務(wù)器提供了一套易于定制的用戶認(rèn)證器接口,用戶可以根據(jù)自身企業(yè)的在線系統(tǒng)的認(rèn)證方式,來定制自己的認(rèn)證邏輯。不論是傳統(tǒng)的用戶名/密碼方式,還是基于安全證書的方式;是基于關(guān)系數(shù)據(jù)庫的存儲,還是采用LDAP服務(wù)器,CAS Server給我們提供了這些常用的驗證器模板代碼,只要稍作修改,便可靈活使用了。
對于廣大的中國企業(yè)用戶而言,另一個需要定制的功能莫過于全中文、企業(yè)特色的用戶身份認(rèn)證頁面了。CAS Server提供了兩套系統(tǒng)界面,一套是默認(rèn)的CAS英文標(biāo)準(zhǔn)頁面,另一套則是專門提供給用戶來定制修改的。(PS:老外們做事情就是人性化啊~~)那么對CAS Server端的后續(xù)學(xué)習(xí),我們將圍繞著身份認(rèn)證模塊定制和界面定制這兩方面展開。
CAS Client
客戶端我們使用的是CAS Client 2.1.1。雖然在官方網(wǎng)站上已出現(xiàn)了3.1.0版本的下載,但該版本地代碼已經(jīng)完全重寫,使用的package和類名同2.1.1大相徑庭了,最關(guān)鍵的是,該版本暫時沒有對應(yīng)的API說明文檔。雖然咖啡我對程序版本懷有極大的“喜新厭舊”的心態(tài),但安全起見,還是先2.1.1吧,相信3.1.0的文檔耶魯大學(xué)的大牛們已經(jīng)在整理了,期待中……
CAS Client2.1.1.jar中的代碼是相當(dāng)精煉的,有興趣的朋友建議閱讀一下源碼。Jar包中的代碼分成三個大部分
1. edu.yale.its.tp.cas.util 包,其中只有一個工具類 SecureURL.java 用來訪問HTTPS URL
2. edu.yale.its.tp.cas.proxy包,用來處理Proxy Authentication代理認(rèn)證的3個類,其中ProxyTicketReceptor.java是 接收PGT回調(diào)的servlet,在下文中我們會提及。
3. edu.yale.its.tp.cas.client包,其中包含了CAS Filter ,Tag Library等主要的認(rèn)證客戶端工具類,我們在后面會進(jìn)行重點介紹。
針對CAS Client的學(xué)習(xí),我們的重點將放在CAS Filter 和ProxyTicketReceptor 的配置以及在Java SE環(huán)境下,直接使用 ServiceTicketValidator進(jìn)行Ticket認(rèn)證實現(xiàn)上。
CAS服務(wù)器端應(yīng)用
定制適合你的身份認(rèn)證程序
通過前面的學(xué)習(xí),我們了解了CAS具有一個良好而強大的SSO功能框架。接下來,我們要學(xué)習(xí)如何將實際企業(yè)應(yīng)用中的身份認(rèn)證同CAS進(jìn)行整合。
簡單的說,要將現(xiàn)有企業(yè)應(yīng)用中的認(rèn)證集成到CAS Server中,只要實現(xiàn)一個名為AuthenticationHandler的一個認(rèn)證處理Java接口就行。以下是該接口的源代碼:
- public interface AuthenticationHandler {
-
-
-
-
- boolean authenticate(Credentials credentials) throws AuthenticationException;
-
-
-
- boolean supports(Credentials credentials);
- }
public interface AuthenticationHandler {
/**
* 該方法決定一個受支持的credentials是否是可用的,
* 如果可用,該方法返回true,則說明身份認(rèn)證通過
*/
boolean authenticate(Credentials credentials) throws AuthenticationException;
/**
* 該方法決定一個credentials是否是當(dāng)前的handle所支持的
*/
boolean supports(Credentials credentials);
}
這里我們要說明一下Credentials這個CAS的概念。所謂Credentials是由外界提供給CAS來證明自身身份的信息,簡單的如一個用戶名/密碼對就是一個Credentials,或者一個經(jīng)過某種加密算法生成的密文證書也可以是一個Credentials。在程序的實現(xiàn)上,Credentials被聲明為一個可序列化的接口,僅僅起著標(biāo)識作用,源代碼如下:
- public interface Credentials extends Serializable {
-
- }
public interface Credentials extends Serializable {
// marker interface contains no methods
}
CAS的API中,已經(jīng)為我們提供了一個最常用的實現(xiàn)UsernamePasswordCredentials 用戶名/密碼憑證,代碼如下:
- public class UsernamePasswordCredentials implements Credentials {
-
- private static final long serialVersionUID = -8343864967200862794L;
-
- private String username;
-
- private String password;
- public final String getPassword() {
- return this.password;
- }
- public final void setPassword(final String password) {
- this.password = password;
- }
- public final String getUsername() {
- return this.username;
- }
- public final void setUsername(final String userName) {
- this.username = userName;
- }
- public String toString() {
- return this.username;
- }
- public boolean equals(final Object obj) {
- if (obj == null || !obj.getClass().equals(this.getClass())) {
- return false;
- }
- final UsernamePasswordCredentials c = (UsernamePasswordCredentials) obj;
- return this.username.equals(c.getUsername())
- && this.password.equals(c.getPassword());
- }
- public int hashCode() {
- return this.username.hashCode() ^ this.password.hashCode();
- }
- }
public class UsernamePasswordCredentials implements Credentials {
/** Unique ID for serialization. */
private static final long serialVersionUID = -8343864967200862794L;
/** The username. */
private String username;
/** The password. */
private String password;
public final String getPassword() {
return this.password;
}
public final void setPassword(final String password) {
this.password = password;
}
public final String getUsername() {
return this.username;
}
public final void setUsername(final String userName) {
this.username = userName;
}
public String toString() {
return this.username;
}
public boolean equals(final Object obj) {
if (obj == null || !obj.getClass().equals(this.getClass())) {
return false;
}
final UsernamePasswordCredentials c = (UsernamePasswordCredentials) obj;
return this.username.equals(c.getUsername())
&& this.password.equals(c.getPassword());
}
public int hashCode() {
return this.username.hashCode() ^ this.password.hashCode();
}
}
很簡單不是嗎?就是存儲一個用戶名和密碼的java bean而已。
接下來,我們將一個Credentials傳給一個AuthenticationHandler進(jìn)行認(rèn)證,首先調(diào)用boolean supports(Credentials credentials)方法察看當(dāng)前傳入的Credentials實例,AuthenticationHandler實例現(xiàn)是否支持它?如果支持,再調(diào)用boolean authenticate(Credentials credentials)方法進(jìn)行認(rèn)證。由于用戶名/密碼方式是最常用的認(rèn)證方法,因此CAS為我們提供了一個現(xiàn)成的基于該方式的抽象認(rèn)證處理類AbstractUsernamePasswordAuthenticationHandler。通常我們只需要繼承該類,并實現(xiàn)其中的 authenticateUsernamePasswordInternal方法即可。下面我們給出一個Demo的實現(xiàn)類,它的校驗邏輯很簡單——僅校驗用戶名的字符長度是否與密碼的相等(這里密碼是一個表示長度的整數(shù)),如果相等則認(rèn)為認(rèn)證通過,請看代碼:
- public class UsernameLengthAuthnHandler
- extends AbstractUsernamePasswordAuthenticationHandler {
- protected boolean authenticateUsernamePasswordInternal( UsernamePasswordCredentials credentials) throws AuthenticationException {
- /*
- * 這里我們完全可以用自己的認(rèn)證邏輯代替,比如將用戶名/密碼傳入一個SQL語句
- * 向數(shù)據(jù)庫驗證是否有對應(yīng)的用戶賬號,這不是我們最經(jīng)常干的事么?
- * 只需要將下面的程序替換掉就OK了!!So easy,so simple!
- /
- String username = credentials.getUsername();
- String password = credentials.getPassword();
- String correctPassword = Integer.toString(username.length());
- return correctPassword.equals(password);
- }
- }
public class UsernameLengthAuthnHandler
extends AbstractUsernamePasswordAuthenticationHandler {
protected boolean authenticateUsernamePasswordInternal( UsernamePasswordCredentials credentials) throws AuthenticationException {
/*
* 這里我們完全可以用自己的認(rèn)證邏輯代替,比如將用戶名/密碼傳入一個SQL語句
* 向數(shù)據(jù)庫驗證是否有對應(yīng)的用戶賬號,這不是我們最經(jīng)常干的事么?
* 只需要將下面的程序替換掉就OK了!!So easy,so simple!
/
String username = credentials.getUsername();
String password = credentials.getPassword();
String correctPassword = Integer.toString(username.length());
return correctPassword.equals(password);
}
}
介紹到這里,大家應(yīng)該清楚如何定制自己的AuthenticationHandler類了吧!這里要附帶說明的是,在CAS Server的擴(kuò)展API中已經(jīng)提供了大量常用認(rèn)證形式的實現(xiàn)類,它們同CAS Server的war包一同分發(fā):
cas-server-support-generic-3.1.1.jar ——使用Map記錄用戶認(rèn)證信息的實現(xiàn)
cas-server-support-jdbc-3.1.1.jar —— 基于Spring JDBC的數(shù)據(jù)庫實現(xiàn)(我們常用的)
cas-server-support-ldap-3.1.1.jar —— 基于LDAP的用戶認(rèn)證實現(xiàn)
更多其他形式的實現(xiàn)各位看官有興趣的,可以一一閱讀源碼。
配置你的身份認(rèn)證程序
完成了定制認(rèn)證類的代碼編寫,接下來就是要讓CAS Server來調(diào)用它了。在CAS的框架中,對程序的配置都是使用Spring Framework的xml文件,這對于熟悉Spring的程序員而言算駕輕就熟了。
配置文件位于應(yīng)用部署目錄的WEB-INF子目錄下——deployerConfigContext.xml。在bean id=authenticationManager 的 authenticationHandlers屬性中配置我們的AuthenticationHandlers:
引用
<?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:p="http://www.springframework.org/schema/p"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd">
<bean id="authenticationManager"
class="org.jasig.cas.authentication.AuthenticationManagerImpl">
。。。
。。。
<property name="authenticationHandlers">
<list>
<bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler" p:httpClient-ref="httpClient" />
<!—下面就是系統(tǒng)默認(rèn)的驗證器配置,你可以替換它,或者增加一個新的handler -->
<bean class="org.jasig.cas.authentication.handler.support.SimpleTestUsernamePasswordAuthenticationHandler" />
</list>
</property>
</bean>
。。。
。。。
</beans>
我們發(fā)現(xiàn)authenticationHandlers屬性是一個list,在這個list中可以配置多個AuthenticationHandlers。這些AuthenticationHandlers形成了一個驗證器鏈,所有提交給CAS的Credentials信息將通過這個驗證器鏈的鏈?zhǔn)竭^濾,只要這鏈中有一個驗證器通過了對Credentials的驗證,就認(rèn)為這個Credentials是合法的。這樣的設(shè)計使得我們可以很輕松的整合不同驗證體系的已有應(yīng)用到同一個CAS上,比如:A驗證器負(fù)責(zé)校驗alpha系統(tǒng)提交的Credentials,它是基于LDAP服務(wù)器的;B驗證器負(fù)責(zé)校驗beta系統(tǒng)提交的Credentials,它是一個傳統(tǒng)的RDB用戶表認(rèn)證;C驗證器負(fù)責(zé)校驗gamma系統(tǒng)提交的基于RSA證書加密的Credentials。3種完全不同的用戶身份認(rèn)證通過配置就可以統(tǒng)一在同一個CAS服務(wù)內(nèi),很好很強大,不是嗎!!
定制身份驗證登錄界面
CAS Server在顯示界面層view使用了“主題Theme”的概念。在{project.home}/webapp/WEB-INF/view/jsp/目錄下,系統(tǒng)默認(rèn)提供了兩套得UI —— default和simple 。default方案使用了CSS等相對復(fù)雜得界面元素,而simple方案提供了最簡化的界面表示方式。在整個的CAS Server服務(wù)器端,有四個界面是我們必須要實現(xiàn)的:
casConfirmView.jsp —— 確認(rèn)信息(警告信息)頁面
casGenericSuccess.jsp —— 登陸成功提示頁面
casLoginView.jsp —— 登錄輸入頁面
casLogoutView.jsp —— SSO登出提示頁面
這些都是標(biāo)準(zhǔn)的jsp頁面,如何實現(xiàn)他們,完全由您說了算,除了名字不能改。
CAS為view的展示提供了3個級別的定制方式,讓我們從最直觀簡單的開始吧。
1. 采用文件覆蓋方式:直接修改default中的頁面或者將新寫好的四個jsp文件覆蓋到default目錄中。這種方式最直觀和簡單,但咖啡建議各位在使用這種方式前將原有目錄中的文件備份一下,以備不時之需。
2. 修改UI配置文件,定位UI目錄:在CAS Server端/webapp/WEB-INF/classes/ 目錄下,有一個名為default_views.properties的屬性配置文件,你可以通過修改配置文件中的各個頁面文件位置,指向你新UI文件,來達(dá)到修改頁面展示的目的。
3. 修改配置文件的配置文件,這話看起來有點別扭,其實一點不難理解。在方法2中的default_views.properties文件是一整套的UI頁面配置。如果我想保存多套的UI頁面配置就可以寫多個的properties文件來保存這些配置。在CAS Server端/webapp/WEB-INF/目錄下有cas-servlet.xml和cas.properties兩個文件,cas-servlet.xml使用了cas.properties文件中的cas.viewResolver.basename屬性來定義view屬性文件的名字,因此你可以選者直接修改cas-servlet.xml中的viewResolver 下的basenames屬性,或者修改cas.properties中的cas.viewResolver.basename屬性,指定新的properties文件名,這樣可以輕松的替換全套UI。
CAS客戶端配置及API應(yīng)用
CASFilter的配置
對于大部分web應(yīng)用而言,使用CAS集成統(tǒng)一認(rèn)證是相對簡單的事,只要為需要認(rèn)證的URL配置edu.yale.its.tp.cas.client.filter.CASFilter認(rèn)證過濾器。下面我們就針對過濾器的配置進(jìn)行說明。首先參看一下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://secure.its.yale.edu/cas/login<;/param-value>
</init-param>
<init-param>
<param-name>edu.yale.its.tp.cas.client.filter.validateUrl</param-name>
<param-value>https://secure.its.yale.edu/cas/serviceValidate<;/param-value>
</init-param>
<init-param>
<param-name>edu.yale.its.tp.cas.client.filter.serverName</param-name>
<param-value>your server name and port (e.g., www.yale.edu:8080)</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Filter</filter-name>
<url-pattern>/requires-cas-authetication/*</url-pattern>
</filter-mapping>
...
</web-app>
上述配置中的init-param是filter的3個必備的屬性,下面這張表則是filter全部屬性的詳細(xì)說明:
ProxyTicketReceptor的配置
大家還記得在前面我們說過的Proxy Authentication中的call back URL嗎?ProxyTicketReceptor是部署在client端的一個servlet,提供server端回傳PGT和PGTIOU的。它的xml部署如下:
引用
<web-app>
...
<servlet>
<servlet-name>ProxyTicketReceptor</servlet-name>
<servlet-class>edu.yale.its.tp.cas.proxy.ProxyTicketReceptor</servlet-class>
<init-param>
<param-name>edu.yale.its.tp.cas.proxyUrl</param-name>
<param-value>https://secure.its.yale.edu/cas/proxy<;/param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>ProxyTicketReceptor</servlet-name>
<url-pattern>/CasProxyServlet</url-pattern>
</servlet-mapping>
...
</webapp>
這里要說明的是它的參數(shù)edu.yale.its.tp.cas.proxyUrl。在服務(wù)端通過ProxyTicketReceptor將PGT和PGTIOU傳給客戶端后,ProxyTicketReceptor在進(jìn)行Proxy Authentication的過程中需要向服務(wù)端請求一個ProxyTicket(PT),這個proxyUrl就是服務(wù)端的請求入口了。(關(guān)于Proxy Authentication的運作原理,參見
JA-SIG(CAS)學(xué)習(xí)筆記2 )
CAS Client端的API應(yīng)用1.用戶可以通過以下兩種方式的任意一種,從JSP或servlet中獲取通過認(rèn)證的用戶名:
引用
String username = (String)session.getAttribute(CASFilter.CAS_FILTER_USER);
或者
String username = (String)session.getAttribute("edu.yale.its.tp.cas.client.filter.user");
2.獲得更完整的受認(rèn)證用戶信息對象CASReceipt Java Bean,可以使用以下語句的任一:
引用
CASReceipt receipt = (CASReceipt )session.getAttribute(CASFilter.CAS_FILTER_RECEIPT);
或者
CASReceipt receipt = (CASReceipt )session.getAttribute("edu.yale.its.tp.cas.client.filter.receipt");
3.手工編碼使用CAS Java Object進(jìn)行用戶驗證,使用ServiceTicketValidator或者 ProxyTicketValidator(代理認(rèn)證模式下),在servlet中對用戶身份進(jìn)行驗證。
3-1.ServiceTicketValidator
- import edu.yale.its.tp.cas.client.*;
- ...
- String user = null;
- String errorCode = null;
- String errorMessage = null;
- String xmlResponse = null;
-
-
- ServiceTicketValidator sv = new ServiceTicketValidator();
-
-
- sv.setCasValidateUrl("https://secure.its.yale.edu/cas/serviceValidate");
- sv.setService(urlOfThisService);
- sv.setServiceTicket(request.getParameter("ticket"));
-
- String urlOfProxyCallbackServlet = "https://portal.yale.edu/CasProxyServlet";
- sv.setProxyCallbackUrl(urlOfProxyCallbackServlet);
-
-
- sv.validate();
-
-
- xmlResponse = sv.getResponse();
-
- if(sv.isAuthenticationSuccesful()) {
- user = sv.getUser();
- } else {
- errorCode = sv.getErrorCode();
- errorMessage = sv.getErrorMessage();
- }
-
-
- String urlOfTargetService = "http://hkg2.its.yale.edu/someApp/portalFeed";
- String proxyTicket = ProxyTicketReceptor.getProxyTicket( sv.getPgtIou() , urlOfTargetService);
import edu.yale.its.tp.cas.client.*;
...
String user = null;
String errorCode = null;
String errorMessage = null;
String xmlResponse = null;
/* instantiate a new ServiceTicketValidator */
ServiceTicketValidator sv = new ServiceTicketValidator();
/* set its parameters */
sv.setCasValidateUrl("https://secure.its.yale.edu/cas/serviceValidate");
sv.setService(urlOfThisService);
sv.setServiceTicket(request.getParameter("ticket"));
String urlOfProxyCallbackServlet = "https://portal.yale.edu/CasProxyServlet";
sv.setProxyCallbackUrl(urlOfProxyCallbackServlet);
/* contact CAS and validate */
sv.validate();
/* if we want to look at the raw response, we can use getResponse() */
xmlResponse = sv.getResponse();
if(sv.isAuthenticationSuccesful()) {
user = sv.getUser();
} else {
errorCode = sv.getErrorCode();
errorMessage = sv.getErrorMessage();
}
/* The user is now authenticated. */
/* If we did set the proxy callback url, we can get proxy tickets with: */
String urlOfTargetService = "http://hkg2.its.yale.edu/someApp/portalFeed";
String proxyTicket = ProxyTicketReceptor.getProxyTicket( sv.getPgtIou() , urlOfTargetService);
3-2.ProxyTicketValidator
- import edu.yale.its.tp.cas.client.*;
- ...
- String user = null;
- String errorCode = null;
- String errorMessage = null;
- String xmlResponse = null;
- List proxyList = null;
-
-
- ProxyTicketValidator pv = new ProxyTicketValidator();
-
-
- pv.setCasValidateUrl("https://secure.its.yale.edu/cas/proxyValidate");
- pv.setService(urlOfThisService);
- pv.setServiceTicket(request.getParameter("ticket"));
-
- String urlOfProxyCallbackServlet = "https://portal.yale.edu/CasProxyServlet";
- pv.setProxyCallbackUrl(urlOfProxyCallbackServlet);
-
-
- pv.validate();
-
-
- xmlResponse = pv.getResponse();
-
-
- if(pv.isAuthenticationSuccesful()) {
- user = pv.getUser();
- proxyList = pv.getProxyList();
- } else {
- errorCode = pv.getErrorCode();
- errorMessage = pv.getErrorMessage();
-
- }
-
-
- String urlOfTargetService = "http://hkg2.its.yale.edu/someApp/portalFeed";
- String proxyTicket = ProxyTicketReceptor.getProxyTicket( pv.getPgtIou() , urlOfTargetService);
import edu.yale.its.tp.cas.client.*;
...
String user = null;
String errorCode = null;
String errorMessage = null;
String xmlResponse = null;
List proxyList = null;
/* instantiate a new ProxyTicketValidator */
ProxyTicketValidator pv = new ProxyTicketValidator();
/* set its parameters */
pv.setCasValidateUrl("https://secure.its.yale.edu/cas/proxyValidate");
pv.setService(urlOfThisService);
pv.setServiceTicket(request.getParameter("ticket"));
String urlOfProxyCallbackServlet = "https://portal.yale.edu/CasProxyServlet";
pv.setProxyCallbackUrl(urlOfProxyCallbackServlet);
/* contact CAS and validate */
pv.validate();
/* if we want to look at the raw response, we can use getResponse() */
xmlResponse = pv.getResponse();
/* read the response */
if(pv.isAuthenticationSuccesful()) {
user = pv.getUser();
proxyList = pv.getProxyList();
} else {
errorCode = pv.getErrorCode();
errorMessage = pv.getErrorMessage();
/* handle the error */
}
/* The user is now authenticated. */
/* If we did set the proxy callback url, we can get proxy tickets with this method call: */
String urlOfTargetService = "http://hkg2.its.yale.edu/someApp/portalFeed";
String proxyTicket = ProxyTicketReceptor.getProxyTicket( pv.getPgtIou() , urlOfTargetService);
在這里,我們假設(shè)上下文環(huán)境中的用戶已經(jīng)通過了CAS登錄認(rèn)證,被重定向到當(dāng)前的servlet下,我們在servlet中獲取ticket憑證,servlet的URL對用戶身份進(jìn)行確認(rèn)。如果上下文參數(shù)中無法獲取ticket憑證,我們就認(rèn)為用戶尚未登錄,那么,該servlet必須負(fù)責(zé)將用戶重定向到CAS的登錄頁面去。
posted on 2008-06-28 16:51
鄧兵野 閱讀(636)
評論(0) 編輯 收藏 所屬分類:
sso單點登錄