Web應用系統集成CAS-rest指南


一、 前言

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。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM