前后端知识交流分享CAS

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

2018-07-03  本文已影响131人  AaronSimon

一、什么是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();
  }
上一篇下一篇

猜你喜欢

热点阅读