美文网首页前后端知识交流分享CAS
CAS 5.1.x版本代理模式实现

CAS 5.1.x版本代理模式实现

作者: AaronSimon | 来源:发表于2018-07-03 11:19 被阅读131次

    一、什么是CAS的代理认证

      在我们的项目中,有这样一个场景:有两个服务holiday(节假日服务)和mainWeb(集成服务),这两个服务都集成了CAS,所有的请求都要经过CAS Server的认证。由于mainWeb内部会去调用holiday的服务,但是mainWeb的请求会被holiday配置的CAS拦截器AuthenticationFilter拦截并重定向到CAS Server。这样我们的mainWeb就没法直接访问holiday服务。通过查阅官方文档,前辈博客发现,CAS的代理模式可以解决这个问题。

    二、CAS代理模式(CAS Proxy)原理剖析

      CAS Proxy代理模式的主要原理如下:mainWeb(代理端)请求holiday(被代理端),mainWeb首先通过CAS Server的认证,然后向CAS Server申请一个针对holiday的proxy ticket,之后在访问holiday的请求中将proxy ticket以参数ticket的形式传递过去,holiday的AuthenticationFilte拦截到该请求,但是发现这个请求携带了ticket参数,于是交给后续的Ticket Validation Filter处理。Ticket Validation Filter将会传递该ticket到CAS Server进行认证,由于该ticket是由Cas Server针对于holiday发行的,holiday申请校验的时候自然验证成功,这样mainWeb就可以访问到holiday。下面是在前辈博客中找到的CAS Proxy的uml图:

    CAS Proxy

    三、CAS代理模式相关配置说明

      CAS Proxy代理模式实现的核心过滤器是Cas20ProxyReceivingTicketValidationFilter,对于代理端,这个过滤器要配置在AuthenticationFilter之前。另外,Cas20ProxyReceivingTicketValidationFilter在代理端和被代理端的配置是不一样的,这个会在项目实战中具体指出。在我们的项目中我们使用的是Cas30ProxyReceivingTicketValidationFilter,实际上Cas30ProxyReceivingTicketValidationFilterCas20ProxyReceivingTicketValidationFilter的子类。

      CAS是基于HTTP2和HTTP3协议的,任何一个组件都可以通过特定的URL访问。Cas30ProxyReceivingTicketValidationFilter使用/p3/xxx

    URI 描述
    /login 登录
    /logout 销毁CAS会话(注销)
    /validate service ticket validation
    /serviceValidate service ticket validation [CAS 2.0]
    /proxyValidate service/proxy ticket validation [CAS 2.0]
    /proxy proxy ticket service [CAS 2.0]
    /p3/serviceValidate service ticket validation [CAS 3.0]
    /p3/proxyValidate service/proxy ticket validation [CAS 3.0]

    Cas20ProxyReceivingTicketValidationFilter相关参数说明:

    属性 描述 需要
    casServerUrlPrefix CAS服务器URL的开始,即https://localhost:8443/cas
    serverName 此应用程序所在的服务器的名称。服务URL将使用此动态构建,即https://localhost:8443(您必须包含协议,但如果端口是标准端口,则端口是可选的)
    renew 指定是否renew=true应该发送到CAS服务器。有效值是true/false(或根本没有值)。请注意,renew不能将其指定为本地init-param设置
    redirectAfterValidation 是否在故障单验证后重定向到相同的URL,但没有参数中的故障单。默认为true
    useSession 是否在会话中存储断言。如果不使用会话,则每个请求都需要票证。默认为true
    exceptionOnValidationFailure 是否在票证验证失败时抛出异常。默认为true
    proxyReceptorUrl 要查看PGTIOU/PGT来自CAS服务器的响应的URL 。应该从上下文的根来定义。例如,如果您的应用程序部署在/cas-client-app您想要的代理服务器URL中,/cas-client-app/my/receptor您需要配置proxyReceptorUrl/my/receptor
    acceptAnyProxy 指定是否有任何代理正常。默认为false。 没有
    allowedProxyChains 指定代理链。每个可接受的代理链应包含一个由空格分隔的URL列表(用于完全匹配)或URL的正则表达式(由^字符开始)。每个可接受的代理链应该出现在自己的行上
    proxyCallbackUrl 用于提供CAS服务器以接受代理授予票证的回叫URL
    proxyGrantingTicketStorageClass 指定具有无参数构造函数的ProxyGrantingTicketStorage类的实现。
    sslConfigFile 包含用于客户端SSL配置的SSL设置的属性文件的引用,用于反向通道调用期间。该配置包括用于键protocol默认为SSL,keyStoreType,keyStorePath,keyStorePass,keyManagerType默认为SunX509和certificatePassword。
    encoding 指定客户端应使用的编码字符集
    secretKey proxyGrantingTicketStorageClass它使用的密钥,如果它支持加密。
    cipherAlgorithm 该算法使用的proxyGrantingTicketStorageClass是否支持加密。默认为DESede
    millisBetweenCleanUps 清理任务的启动延迟从存储中删除过期票证。默认为60000 msec
    ticketValidatorClass 要使用/创建票证验证程序类 没有
    hostnameVerifier 主机名验证程序类名称,用于进行反向通话

    四、项目实战

    项目名 请求地址 角色
    cas http://localhost:8100/cas cas服务端
    mainWeb http://localhost:8080/mainWeb 代理端
    holiday http://localhost:8080/holiday 被代理端

    (一)CAS服务端配置

      由于我们的项目使用的http协议,但是代理模式的roxyCallbackUrl回调地址默认必须是https,经过查看源码,发现cas根据一个有关代理认证策略有关系,默认http是不会颁发PGT的,不过我们可以修改json格式的service文件中的相关策略解决这个问题。下面是我们自定义的service的相关代码,增加了proxyPolicy策略

    {
      "@class": "org.apereo.cas.services.RegexRegisteredService",
      "serviceId": "^(https|http)://localhost:8080/holiday.*",
      "name": "holiday",
      "id": 100001,
      "description": "这是一个holiday域名下的服务,通过holiday访问都允许通过",
      "evaluationOrder": 1,
      "theme": "holiday",
      "attributeReleasePolicy": {
        "@class": "org.apereo.cas.services.ReturnAllAttributeReleasePolicy"
      },
      "proxyPolicy": {
        "@class": "org.apereo.cas.services.RegexMatchingRegisteredServiceProxyPolicy",
        "pattern": "^(https|http)?://.*"
      }
    }
    

      为了使我们的项目全部使用http协议,我们还需要在cas的application.properties中修改以下属性:

    #关闭ssl
    server.ssl.enabled=false
    #解决http下登录状态不互通
    cas.tgc.secure=false
    cas.warningCookie.secure=false
    

      为了方便debug cas服务端出现的验证方面的问题,添加以下依赖(对结果无影响):

    <dependency>
        <groupId>org.apereo.cas</groupId>
        <artifactId>cas-server-support-validation</artifactId>
        <version>${cas.version}</version>
    </dependency>
    

    (二)、代理端配置

      作为代理端Cas30ProxyReceivingTicketValidationFilter除了上面表提到的必须配置外,还需要额外配置两个参数:roxyCallbackUrlproxyReceptorUrl

    1. proxyCallbackUrl:用于指定一个回调地址,在代理端通过Cas Server校验ticket成功后,Cas Server将回调该地址以传递pgtId和pgtIou,Cas30ProxyReceivingTicketValidationFilter在接收到对应的响应后会将它们保存在内部持有的ProxyGrantingTicketStorage(PGT)中。之后在对传递过来的ticket进行validate的时候又会根据pgtIou从ProxyGrantingTicketStorage中获取对应的pgtId,用以保存在AttributePrincipal中,而AttributePrincipal又会保存在Assertion中。proxyCallbackUrl因为是指定Cas Server回调的地址,所以其必须是一个可以供外部访问的绝对地址。此外,因为Cas Server默认只回调使用安全通道协议https进行通信的地址,所以我们的proxyCallbackUrl需要是一个使用https协议访问的地址。
    2. proxyReceptorUrl:该地址是proxyCallbackUrl相对于代理端的一个地址, Cas30ProxyReceivingTicketValidationFilter将根据该地址来决定请求是否来自Cas Server的回调。

      下面是代理端配置的代码:

    /**
       * Cas30ProxyReceivingTicketValidationFilter 验证过滤器
       *
       * 该过滤器负责对Ticket的校验工作,必须配置
       * cas与后台应用服务间确认性验证,保证服务间可信
       *
       * @return
       */
      @Bean
      public FilterRegistrationBean filterValidationRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new Cas30ProxyReceivingTicketValidationFilter());
        // 设定匹配的路径
        // registration.addUrlPatterns("/*");
        // 代理模式(代理端)
        registration.addUrlPatterns("/proxyCallback","/*");
        Map<String,String>  initParameters = new HashMap<>();
        // cas 服务地址,从后台请求CAS服务,得到ticket信息(内部通讯)
        initParameters.put("casServerUrlPrefix", CAS_SERVER_URL_PREFIX);
        // 验证ticket正确后,对当前请求重定向一次的服务地址(主要消除地址中的ticket参数),代理服务的请求(内部通讯)或客户端请求都会处理。可由参数redirectAfterValidation设置不重定向
        initParameters.put("serverName", SERVER_NAME);
        // 是否对serviceUrl进行编码,默认true:设置false可以在302对URL跳转时取消显示;jsessionid=xxx的字符串
        initParameters.put("encodeServiceUrl","false");
        initParameters.put("encoding", "UTF-8");
        // 代理模式(代理端)
        // 发送给CAS服务器,用于代理验证后的回调地址(内部通讯)
         initParameters.put("proxyCallbackUrl","http://localhost:8080/mainWeb/proxyCallback");
        // 代理验证请求地址后缀,与proxyCallbackUrl中设置的一致。用于拦截验证回调
         initParameters.put("proxyReceptorUrl","/mainWeb/proxyCallback");
        // 代理模式(被代理端)
        //initParameters.put("acceptAnyProxy","true");
        //initParameters.put("redirectAfterValidation","false");
    
        //initParameters.put("useSession", "true");
        registration.setInitParameters(initParameters);
        // 设定加载的顺序
        registration.setOrder(1);
        return registration;
      }
    

    (三)、被代理端配置

      在被代理端,Cas30ProxyReceivingTicketValidationFilter既可以验证正常通过CAS Server登录认证成功后返回的ticket(ST),也可以验证来自其他代理端传递过来的proxy ticket(PT),最终均通过CAS Server服务端完成验证。Cas30ProxyReceivingTicketValidationFilter为了proxy ticket,在代理端需要指定接收哪些代理,acceptAnyProxyallowedProxyChains完成这项工作。

    1. acceptAnyProxy:表示是否接受所有应用的代理,其对应的参数值是true或者false
    2. allowedProxyChains:指定具体接受哪些应用的代理,多个应用就写多行,allowedProxyChains的值对应的是代理端提供给Cas Server的回调地址,如果使用前文示例的代理端配置,我们就可以指定被代理端的allowedProxyChains为http://localhost:8080/mainWeb/proxyCallback,这样当mainWeb作为代理端来访问该被代理端时就能通过验证,得到正确的响应。

      下面是被代理端配置代码:

    /**
       * Cas30ProxyReceivingTicketValidationFilter 验证过滤器
       *
       * 该过滤器负责对Ticket的校验工作,必须配置
       * cas与后台应用服务间确认性验证,保证服务间可信
       *
       * @return
       */
      @Bean
      public FilterRegistrationBean filterValidationRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new Cas30ProxyReceivingTicketValidationFilter());
        // 设定匹配的路径
        registration.addUrlPatterns("/*");
        // 代理模式(代理端)
        //registration.addUrlPatterns("/proxyCallback","/*");
        Map<String,String>  initParameters = new HashMap<>();
        // cas 服务地址,从后台请求CAS服务,得到ticket信息(内部通讯)
        initParameters.put("casServerUrlPrefix", CAS_SERVER_URL_PREFIX);
        // 验证ticket正确后,对当前请求重定向一次的服务地址(主要消除地址中的ticket参数),代理服务的请求(内部通讯)或客户端请求都会处理。可由参数redirectAfterValidation设置不重定向
        initParameters.put("serverName", SERVER_NAME);
        initParameters.put("encoding", "UTF-8");
        // 是否对serviceUrl进行编码,默认true:设置false可以在302对URL跳转时取消显示;jsessionid=xxx的字符串
        initParameters.put("encodeServiceUrl","false");
        // 代理模式(代理端)
        // 发送给CAS服务器,用于代理验证后的回调地址(内部通讯)
        // initParameters.put("proxyCallbackUrl","http://localhost:8080/holiday/proxyCallback");
        // 代理验证请求地址后缀,与proxyCallbackUrl中设置的一致。用于拦截验证回调
        // initParameters.put("proxyReceptorUrl","/holiday/proxyCallback");
        // 代理模式(被代理端)
         initParameters.put("acceptAnyProxy","true");
         initParameters.put("redirectAfterValidation","false");
    
        //initParameters.put("useSession", "true");
        registration.setInitParameters(initParameters);
        // 设定加载的顺序
        registration.setOrder(1);
        return registration;
      }
    

      应用既可以作为代理端也可以作为被代理端,那么配置文件如下:

    /**
       * Cas30ProxyReceivingTicketValidationFilter 验证过滤器
       *
       * 该过滤器负责对Ticket的校验工作,必须配置
       * cas与后台应用服务间确认性验证,保证服务间可信
       *
       * @return
       */
      @Bean
      public FilterRegistrationBean filterValidationRegistration() {
        FilterRegistrationBean registration = new FilterRegistrationBean();
        registration.setFilter(new Cas30ProxyReceivingTicketValidationFilter());
        // 设定匹配的路径
    //    registration.addUrlPatterns("/*");
        // 代理模式(代理端)
        registration.addUrlPatterns("/proxyCallback","/*");
        Map<String,String>  initParameters = new HashMap<>();
        // cas 服务地址,从后台请求CAS服务,得到ticket信息(内部通讯)
        initParameters.put("casServerUrlPrefix", CAS_SERVER_URL_PREFIX);
        // 验证ticket正确后,对当前请求重定向一次的服务地址(主要消除地址中的ticket参数),代理服务的请求(内部通讯)或客户端请求都会处理。可由参数redirectAfterValidation设置不重定向
        initParameters.put("serverName", SERVER_NAME);
        initParameters.put("encoding", "UTF-8");
        // 是否对serviceUrl进行编码,默认true:设置false可以在302对URL跳转时取消显示;jsessionid=xxx的字符串
        initParameters.put("encodeServiceUrl","false");
        // 代理模式(代理端)
        // 发送给CAS服务器,用于代理验证后的回调地址(内部通讯)
        initParameters.put("proxyCallbackUrl","http://localhost:8080/holiday/proxyCallback");
        // 代理验证请求地址后缀,与proxyCallbackUrl中设置的一致。用于拦截验证回调
        initParameters.put("proxyReceptorUrl","/holiday/proxyCallback");
        // 代理模式(被代理端)
         initParameters.put("acceptAnyProxy","true");
         initParameters.put("redirectAfterValidation","false");
    
        //initParameters.put("useSession", "true");
        registration.setInitParameters(initParameters);
        // 设定加载的顺序
        registration.setOrder(1);
        return registration;
      }
    

    (四)、代理端代理请求

    @RequestMapping("/proxy")
      @ResponseBody
      public String proxy(HttpServletRequest request, HttpServletResponse response) throws IOException {
        StringBuilder result = new StringBuilder();
    
        // 被代理应用的URL
        String serviceUrl = "http://localhost:8080/holiday/getHoliday";
    
        //1、获取到AttributePrincipal对象
        AttributePrincipal principal = (AttributePrincipal) request.getUserPrincipal();
        if (principal == null) {
          return "用户未登录";
        }
    
        //2、获取对应的(PT)proxy ticket
        String proxyTicket = principal.getProxyTicketFor(serviceUrl);
        if (proxyTicket == null) {
          return "PGT 或 PT 不存在";
        }
        //3、请求被代理应用时将获取到的proxy ticket以参数ticket进行传递
        URL url = new URL(serviceUrl + "?ticket=" + proxyTicket);// 不需要cookie,只需传入代理票据
    
        HttpURLConnection conn;
        conn = (HttpURLConnection) url.openConnection();
        //使用POST方式
        conn.setRequestMethod("POST");
        // 设置是否向connection输出,因为这个是post请求,参数要放在
        // http正文内,因此需要设为true
        conn.setDoOutput(true);
        // Post 请求不能使用缓存
        conn.setUseCaches(false);
    
        //设置本次连接是否自动重定向
        conn.setInstanceFollowRedirects(true);
        // 配置本次连接的Content-type,配置为application/x-www-form-urlencoded的
        // 意思是正文是urlencoded编码过的form参数
        conn.setRequestProperty("Content-Type","application/x-www-form-urlencoded");
        // 连接,从postUrl.openConnection()至此的配置必须要在connect之前完成,
        // 要注意的是connection.getOutputStream会隐含的进行connect。
        conn.connect();
    
        DataOutputStream out = new DataOutputStream(conn
                .getOutputStream());
        // 正文,正文内容其实跟get的URL中 '? '后的参数字符串一致
        String content = "start=" + URLEncoder.encode("1901-01-01", "UTF-8")+"&end="+URLEncoder.encode("2018-01-01", "UTF-8");
        // DataOutputStream.writeBytes将字符串中的16位的unicode字符以8位的字符形式写到流里面
        out.writeBytes(content);
        //流用完记得关
        out.flush();
        out.close();
        //获取响应
        BufferedReader reader = new BufferedReader(new InputStreamReader(conn.getInputStream()));
        String line;
        while ((line = reader.readLine()) != null){
          result.append(line);
        }
        reader.close();
        //连接断了
        conn.disconnect();
    
        return result.toString();
      }
    

    相关文章

      网友评论

        本文标题:CAS 5.1.x版本代理模式实现

        本文链接:https://www.haomeiwen.com/subject/vaeduftx.html