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。

 

posted @ 2017-09-18 10:49  闻歌感旧  阅读(3654)  评论(0编辑  收藏  举报