一、 前言
CAS是一個旨在為應用系統提供單點登錄方案的企業級的開源項目,它為第三方應用提供了基於REST的操作接口。為方便公司的Web應用(及類似系統)中實現單點登錄的相應功能,實現了一個Cas_Service工程,以供相關項目調用。
為后續表達准確,對相關術語作簡單說明:
- Web應用系統:准備集成CAS單點登錄功能的各類Web應用;
- CAS Server:本文中特指cas-server-webapp的war文件,需要獨立部署,有時也稱為認證系統、認證中心;
- CAS Client:本文中特指cas-client-core-3.4.1.jar,需與應用系統一起部署。
此外,集成過程中的相關條件和約束如下:
- 單點登錄功能:各應用系統可統一登入/登出、JDBC認證、密碼MD5存取;
- CAS版本:除非特別聲明,CAS各組件的版本均為4.2.7;
- 訪問方式:訪問應用系統和CAS Server均使用https協議、8443端口;
- Web服務器:應用系統和CAS Server均由Tomcat提供Web服務;
- CAS Domain:即CAS Server的域名,本文假定為cas.hisign.con.cn/cas;
- 應用系統Domain:即各應用系統的域名,本文假定為app.hisign.com.cn/,后面是各自的應用系統名稱。通常情況下,cas.hisign.con.cn和app.hisign.con.cn相同。
二、 CAS Server部署
嚴格意義上,此章節內容不屬於應用系統集成的工作范圍,故只做簡要描述,供加深對整體工作了解之用。如需了解其中細節,請參閱其它資料。
1) 制作和配置SSL證書
用keytool工具制作所需證書。注意需要用到主機名的地方,不要用IP而是域名;如果CAS和應用系統部署在不同服務器節點上,各節點都需制作證書,並在每個應用系統的服務器節點上配置與CAS Server節點的信任關系。
然后修改Tomcat的配置文件,以正確使用SSL證書。
2) 部署CAS Server
將cas-server-webapp-4.2.7.war拷貝到Tomcat的webapps目錄,改名為cas.war;啟動Tomcat后,自動解壓為cas目錄。
將相應的數據庫JDBC驅動(如Oracle的ojdbc6.jar或ojdbc7.jar)、cas支持JDBC的jar文件(cas-server-support-jdbc-4.2.7.jar)拷貝至cas/WEB-INF/lib目錄。
3) 配置為JDBC認證、密碼MD5加密
修改CAS Server的配置文件deployerConfigContext.xml和cas.properties:
- 修改deployerConfigContext.xml,增加對datasource的描述;
- 修改deployerConfigContext.xml的primaryAuthenticationHandler項;
- 設置cas.propeities的cas.jdbc.authn.query.sql項;
- 設置cas.propeities的cas.authn.password.encoding.alg項;
- 修改deployerConfigContext.xml,增加對passwordEncoder的描述;
- 設置cas.propeities的cas.logout.followServiceRedirects項。
三、 Cas_Service接口
為在Java類中集成CAS功能,已將一些常用CAS功能封裝到cas-service包,以供Web應用系統調用。
首先需在maven工程的pom.xml文件里增加對cas_service包的依賴:
<dependency> <groupId>com.hisign.pu.abis</groupId> <artifactId>cas_service</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency>
可供調用的接口如下:
1) 獲取TGT
String getTicketGrantingTicket(String Server, String username, String password);
- 參數server為CAS Server的訪問URL;
- 參數username為登錄用戶名;
- 參數password為驗證用的密碼;
- 返回:驗證通過則返回TGT的值,否則拋出異常;
- 示例:
String tgt = casService.getTicketGrantingTicket("https://cas.hisign.com.cn:8443/cas", "casuser", "Mellon");
2) 根據TGT獲取ST
String getServiceTicket(String Server, String ticketGrantingTicket, String service);
- 參數server為CAS Server的訪問URL;
- 參數ticketGrantingTicket為已獲得的TGT;
- 參數service為欲訪問的service的URL;
- 返回:驗證通過則返回ST的值,否則拋出異常;
- 示例:
String st = casService.getTicketGrantingTicket("https://cas.hisign.com.cn:8443/cas", "TGT-2-6eTFeygWirXfgbQWdOitzwAFcuIJyYfmIRNeMELaqKiLSw9zOY-cas01.example.org", "https://app.hisign.com.cn:8443/app1");
3) 判別ST是否有效
String verifySeviceTicket(String server, String serviceTicket, String service);
- 參數server為CAS Server的訪問URL;
- 參數serviceTicket為已獲得的ST;
- 參數service為欲訪問的service的URL;
- 返回:ST有效返回登錄用戶名,無效返回null,若出錯拋出異常;
- 示例:
boolean String = casService.verifyServiceTicket("https://cas.hisign.com.cn:8443/cas", "ST-2-5kEeqQuPsnB1b4UyUHFW-cas01.example.org", "https://app.hisign.com.cn:8443/app1");
4) 刪除TGT(相當於在CAS Server端注銷)
boolean deleteTicketGrantingTicket(String Server, String ticketGrantingTicket);
- 參數server為CAS Server的訪問URL;
- 參數ticketGrantingTicket為已獲得的TGT;
- 返回:成功返回true,否則拋出異常;
- 示例:
boolean bool = casService.deleteTicketGrantingTicket("https://cas.hisign.com.cn:8443/cas", "TGT-2-6eTFeygWirXfgbQWdOitzwAFcuIJyYfmIRNeMELaqKiLSw9zOY-cas01.example.org");
注意事項:參數server必須真實有效,可從配置文件獲取;而參數service可以是虛構的、符合規范的格式。
四、 核心代碼
這里修改或去掉了一些內部的比較敏感的內容,望理解。
package cas_service; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.net.URLEncoder; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.apache.commons.httpclient.HttpClient; import org.apache.commons.httpclient.HttpStatus; import org.apache.commons.httpclient.NameValuePair; import org.apache.commons.httpclient.methods.DeleteMethod; import org.apache.commons.httpclient.methods.GetMethod; import org.apache.commons.httpclient.methods.PostMethod; import org.slf4j.Logger; import org.slf4j.LoggerFactory; ... public class CasService { static private final Logger LOG = LoggerFactory.getLogger(CasService.class); private static String serverAddr; private static String serverPort; private static String serverConnString; static { String fileName = "cas-service.ini"; try { ProjProperties props = newProjProperties(); props.load(new FileInputStream(fileName)); serverAddr = props.getProperty("SSO_SVR_ADDRESS", "localhost"); serverPort = props.getProperty("SSO_SVR_PORT", "8443"); serverConnString = "https://" + serverAddr + ":" + serverPort + "/cas"; } catch (Exception e) { LOG.warn("load application server configuration ({}) failed. {}", fileName, e.getMessage()); } } //獲取TGT public String getTicketGrantingTicket(String username, String password) { if (serverConnString==null || serverConnString.equals("")) throw new Exception("Invalid parameter: CAS Server"); HttpClient client = new HttpClient(); PostMethod method = new PostMethod(serverConnString + "/v1/tickets"); method.setRequestBody(new NameValuePair[] { new NameValuePair("username", username), new NameValuePair("password", password) }); try { client.executeMethod(method); String response = method.getResponseBodyAsString(); int status = method.getStatusCode(); switch (status) { case HttpStatus.SC_CREATED: // Created { Matcher matcher = Pattern.compile(".*action=\".*/(.*?)\".*").matcher(response); if (matcher.matches()) return matcher.group(1); break; } default: throw new Exception("Invalid Response code " + status + " from CAS Server!"); } } catch (IOException e) { LOG.error("some exception happened during apply for a TGT " + e.getMessage()); } finally { method.releaseConnection(); } return null; } //根據TGT獲得ST public String getServiceTicket(String ticketGrantingTicket, String moduleName) { if (serverConnString==null || serverConnString.equals("")) throw new Exception("Invalid parameter: CAS Server"); if (moduleName==null || moduleName.equals("")) throw new Exception("Invalid parameter: no module name within request."); if (ticketGrantingTicket==null || ticketGrantingTicket.equals("")) throw new Exception("Invalid TGT."); HttpClient client = new HttpClient(); PostMethod method = new PostMethod(serverConnString + "/v1/tickets/" + ticketGrantingTicket); String service1 = buildModuleServiceName(moduleName); method.setRequestBody(new NameValuePair[] { new NameValuePair("service", service1) }); try { client.executeMethod(method); String response = method.getResponseBodyAsString(); int status = method.getStatusCode(); switch (status) { case HttpStatus.SC_OK: // Accepted return response; default: throw new Exception("Invalid Response code " + status + " from CAS Server!"); } } catch (IOException e) { LOG.error("some exception occured during apply for a service ticket. " + e.getMessage()); } finally { method.releaseConnection(); } return null; } //檢驗ST是否有效 public String verifyServiceTicket(String serviceTicket, String moduleName) { if (serverConnString==null || serverConnString.equals("")) throw new Exception("Invalid parameter: CAS Server"); if (moduleName==null || moduleName.equals("")) throw new Exception("Invalid parameter: module name"); if (ABISHelper.isEmpty(serviceTicket)) return null; HttpClient client = new HttpClient(); GetMethod method = null; String service1 = buildModuleServiceName(moduleName); try { method = new GetMethod(serverConnString + "/p3/serviceValidate?ticket=" + URLEncoder.encode(serviceTicket, "utf-8") + "&service=" + URLEncoder.encode(service1, "utf-8")); client.executeMethod(method); String response = method.getResponseBodyAsString(); // 對有轉發的訪問請求,GetMethod才返回SC_OK,PostMethod返回的是302 int status = method.getStatusLine().getStatusCode(); switch (status) { case HttpStatus.SC_OK: // Accepted int begin = response.indexOf("<cas:user>"); if (begin < 0) return null; int end = response.indexOf("</cas:user>"); return response.substring(begin + 10, end); default: throw new Exception("Invalid Response code " + status + " from CAS Server!"); } } catch (IOException e) { LOG.error("some exception occured during verify a service ticket. " + e.getMessage()); } finally { method.releaseConnection(); } return null; } //刪除TGT public boolean deleteTicketGrantingTicket(String ticketGrantingTicket) { if (serverConnString==null || serverConnString.equals("")) throw new Exception("Invalid parameter: CAS Server"); if (ticketGrantingTicket==null || ticketGrantingTicket.equals("")) return false; HttpClient client = new HttpClient(); DeleteMethod method = new DeleteMethod(serverConnString + "/v1/tickets/" + ticketGrantingTicket); try { client.executeMethod(method); int status = method.getStatusCode(); switch (status) { case HttpStatus.SC_OK: return true; default: throw new Exception("Invalid Response code " + status + " from CAS Server!"); } } catch (IOException e) { LOG.error("some exception occured during verifing a service ticket" + e.getMessage()); } finally { method.releaseConnection(); } return false; } private String buildModuleServiceName(String moduleName) { return "https://" + serverAddr + ":" + serverPort + "/" + moduleName; } }
五、 TGT與ST的時效設置
TGT和ST有時效和限制,默認是TGT有2小時時效、保留8小時,而ST是 10秒時效且只能使用一次。
如需改變ST的時效和次數限制,可通過修改CAS Server的配置文件cas.propertities中的st.numberOfUses和st.timeToKillInSeconds項加以更改。如:
# Service Ticket Timeout
st.timeToKillInSeconds=10
st.numberOfUses=1
對於TGT略復雜一些,首先需通過修改配置文件deployerConfigContext.xml中的<alias name = … alias="grantingTicketExpirationPolicy"項來設置TGT的失效策略,然后再根據策略修改cas.propertities的相關項。比如:
# 默認失效策略 <alias name="ticketGrantingTicketExpirationPolicy" alias="grantingTicketExpirationPolicy" /> tgt.maxTimeToLiveInSeconds=28800 tgt.timeToKillInSeconds=7200 # 過期失效策略 <alias name="timeoutExpirationPolicy" alias="grantingTicketExpirationPolicy" /> tgt.timeout.maxTimeToLiveInSeconds=7200 # 硬時間失效策略 <alias name="hardTimeoutExpirationPolicy" alias="grantingTicketExpirationPolicy" /> tgt.timeout.hard.maxTimeToLiveInSeconds=14400 # 永不失效策略(有一定安全風險) <alias name="neverExpirationPolicy" alias="grantingTicketExpirationPolicy" /> # cas.propertities中無需設置
六、 一個典型的調用流程
以下是某個應用系統使用cas_service包接口的典型流程:
- 某用戶登錄應用A,因為是首次登錄,需提供用戶名、密碼;
- 應用A根據用戶名、密碼,調用getTicketGrantingTicket接口獲取TGT;
- TGT多次使用,需保存在session或其它存儲對象中;
- 應用A使用TGT,調用getServiceTicket接口獲取am服務的ST;
- 應用A可使用剛獲取的ST,作為參數訪問am服務;
- ST因有效期短暫且使用次數有限制,一般是一次性使用,不必保存;
- 用戶欲訪問應用B的bn服務,先從session或其它存儲對象中查找到TGT;
- 應用A(或應用B)TGT,調用getServiceTicket接口獲取bn服務的ST;
- 應用B接收ST,調用verifySeviceTicket接口,返回不為null則該ST有效;
- 驗證通過后,應用B使用該ST訪問bn服務;
- 應用B可調用接口getCasUserName和getCasAttributes,獲取登錄用戶及相關屬性;
- 欲根據ST查找當前登錄用戶,調用getUsernameSeviceTicket接口,返回值即是;
- 用戶從某應用注銷時,需調用deleteTicketGrantingTicket接口從Cas Server刪除TGT。