• 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。
  • 相关阅读:
    获取父类参数类型工具类
    date工具类
    Ascii工具类
    AES加解密工具类
    请求ip获取工具类
    对象和map互相转换工具类
    HTTP中get、post请求工具类
    时间日期各种工具类
    算法练习题——两数相除
    ETag
  • 原文地址:https://www.cnblogs.com/wggj/p/7541953.html
Copyright © 2020-2023  润新知