`
yeminping
  • 浏览: 176755 次
  • 性别: Icon_minigender_1
  • 来自: 福州
社区版块
存档分类
最新评论

CAS 3.x代理配置

    博客分类:
  • JAVA
阅读更多

http://fallenlord.blogbus.com/logs/57175888.html

前阵子改造系统,需要做CAS的Proxy,上网找了很久,找到了一些讲述原理的文章,但几乎没有讲述配置的文章,有讲述配置的也是错误的或是不完整的或是针对2.x版本的,还有很多是直接把代理配置当做普通配置使用-_-

那么,既然网上没有,我就在这里写一下吧

1. 什么情况下需要使用代理

显然这是最需要首先搞清楚的一个问题

假设现在有服务A使用了CAS,我们作为用户zhangsan去访问服务A,这没有什么问题
然后出现了一个新的需求,有服务B也实现了CAS,要求我们仍然只是访问A,但是A会以我们相同的用户(zhangsan)去访问服务B获取我们所需的内容并呈现出来。
此时A就被我们称为代理(Proxy),B被称为后台服务(back-end service)或被代理应用(Proxied Application)

一个简单的应用场景就是Portal,我们只访问Portal,由Portal去各个子系统抓取我们所需的内容并呈现出来
如果有兄弟没有玩过Portal,那么iGoogle也是个很好的场景,你在iGoogle上登录后就可以直接查看到GMail的内容了,这就是用了Proxy技术

 

2. 代理运转原理

运转原理写起来比较麻烦,这里推荐三篇文章,均是介绍CAS的代理运转原理的:

相信看完以上三篇神来之笔后,可以对CAS代理这块的原理有个简单的认识
但需要深入理解,显然光靠神来之笔是靠不住的

 

3. 代理相关配置

OK,直接就到配置了,首先我们需要分为两部分来描述,第一部分是代理应用(Proxying Applicatiion)也就是前端的Portal之类的东东:

<filter>
    <filter-name>CAS Validation Filter</filter-name>
    <filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
    <init-param>
        <param-name>casServerUrlPrefix</param-name>
        <param-value>http://www.cas-server.com</param-value>
    </init-param>
    <init-param>
        <param-name>redirectAfterValidation</param-name>
        <param-value>true</param-value>
    </init-param>
    <init-param>
        <param-name>exceptionOnValidationFailure</param-name>
        <param-value>false</param-value>
    </init-param>
    <init-param>
        <param-name>serverName</param-name>
        <param-value>http://www.local-app.com</param-value>
    </init-param>
    <init-param>
        <param-name>proxyCallbackUrl</param-name>
        <param-value>http://www.local-app.com/cas-proxy/proxyCallback</param-value>
    </init-param>
    <init-param>
        <param-name>proxyReceptorUrl</param-name>
        <param-value>/proxyCallback</param-value>
    </init-param>
</filter>

高亮的两个参数配置应该指向实际同一个地址(只是proxyReceptorUrl用相对路径描述,而proxyCallbackUrl用绝对路径 描述),它们将为这个服务提供一个callback的拦截,使得在每次validator去校验ST的时候,CAS服务器都会发起一个回调请求来访问 proxyCallbackUrl的位置(因此这个地址必须是绝对地址且从CAS服务器发起请求可以访问到的),将PGT(及PGTIou)告诉这个代理 应用,而代理应用会保存这个PGT到Session中

这里有个需要特别注意的问题,这个问题在所有的帖子甚至官方的文档中都没有提及,那就是回调的地址,必须在AuthenticationFilter的url-pattern之前单独的配出来:

<filter-mapping>
    <filter-name>CAS Validation Filter </filter-name>
    <url-pattern>/proxyCallback </url-pattern>
</filter-mapping>

<filter-mapping>
    <filter-name>CAS Authentication Filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
    <filter-name>CAS Validation Filter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

否则callback调用会被AuthenticationFilter给拦截而无法到达ValidationFilter

第二部分是后端服务: 

<filter>
    <filter-name>CAS Validation Filter</filter-name>
    <filter-class>org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter</filter-class>
    <init-param>
        <param-name>casServerUrlPrefix</param-name>
        <param-value>http://www.cas-server.com</param-value>
    </init-param>
    <init-param>
        <param-name>redirectAfterValidation</param-name>
        <param-value>false</param-value>
    </init-param>
    <init-param>
        <param-name>exceptionOnValidationFailure</param-name>
        <param-value>false</param-value>
    </init-param>
    <init-param>
        <param-name>serverName</param-name>
        <param-value>http://www.local-app.com</param-value>
    </init-param>
    <init-param>
        <param-name>acceptAnyProxy</param-name>
        <param-value>true</param-value>
    </init-param>

    <init-param>
        <param-name>proxyCallbackUrl</param-name>
        <param-value>http://www.local-app.com/cas-backend/proxyCallback</param-value>
    </init-param>
    <init-param>
        <param-name>proxyReceptorUrl</param-name>
        <param-value>/proxyCallback</param-value>
    </init-param>
</filter> 

以上高亮代码的acceptAnyProxy是后端服务所必须的(它与allowedProxyChains两个配置二选一),若没有配置acceptAnyProxy或allowedProxyChains,将无法校验代理票据(Proxy Ticket)

而proxyCallbackUrl和proxyReceptorUrl是可选的,如果给出则在校验代理票据(Proxy Ticket)时同时CAS会再次执行回调颁发PGT给此后台服务,使之可以同时成为代理用于代理另一个后台服务。若不配置则CAS不会告诉此应用PGT

 

4. 如何使用代理

看起来好像大功告成了,但是实际上还有最重要的一点我们还没做,就Server to Server的这个请求要怎么来发的问题

首先,既然要发送请求,一定是我们的代理端已经完成了到CAS的认证,那么我们首先获取到Assertion:

final Assertion assertion = (Assertion) (session == null ? request.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION) : session.getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION));

然后从assertion中获取到proxy ticket:

final String proxyTicket = assertion.getPrincipal().getProxyTicketFor(serviceUrl);

最后将proxyTicket以参数的形式绑定到serviceUrl(待访问的backend service地址)之后并发送请求,参数名必须是ticket:

URL url = new URL(serviceUrl + "?ticket=" + proxyTicket);
HttpURLConnection conn = null;
try {
    conn = (HttpURLConnection) url.openConnection();
    // 业务处理
    ...
} catch (final Exception ex) {
    ...
} finally {
    if (conn != null) {
        conn.disconnect();
    }
}
...

这样,当后台服务(back-end service)获取到这条请求后,会到CAS上去校验ticket是否为合法的PT,若合法则允许通过(此时redirectAfterValidation应该为false以避免直接响应一个302 Redirect请求)

 

5. 代理使用方案的一点点讨论 

按照上面的做法,应该说功能都已经实现了,那么现在的问题是什么呢?——性能与压力
这里面有两次请求是我们看起来多余的:

  • 1. 从代理端Assertion中获取PT需要发送一次请求给CAS(PT由CAS颁发)
  • 2. 从被代理端获取到PT后发送请求给CAS以求校验

这些请求触发频率是每次代理端向被代理端发送请求时,不光消耗了代理端和被代理端的性能,也对CAS产生了多余的压力。

那么如何解决这个问题呢?有两种方案:

  • 1. 缓存,在代理和被代理两端将PT缓存起来,这样只有第一次双方需要去访问CAS请求颁发PT和校验PT,以后可以直接使用这个PT即可
  • 2. 颁发自己的票据,这与前端Browser-CAS客户端的模式很相似,CAS客户端为浏览器颁发SessionID作为自己的认证标识,从而撇开了CAS服务器,那么这里我们也可以由被代理端向代理端颁发一个唯一的标识来作为自己的认证标识

这两种方法如出一辙,其实本质上没什么区别。在我们的系统改造中,最后使用的是后者,这是由于它本身就存在一个自己的票据的功能

OK,接下来我们看下使用第二种方案的改造细节吧

方案决定了我们只是需要在第一次代理端到被代理端认证的时候发起与CAS的认证,因此我们继承了 org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter 类,实现了自己的ValidationFilter只扩展了它的一个方法:

protected void onSuccessfulValidation(final HttpServletRequest request, final HttpServletResponse response, final Assertion assertion) {
    // 认证服务地址
    final String loginServiceUrl = "http://somewhere.com/loginService ";
   
    // serviceUrl为要发送的目标地址——被代理端上的一个URL   
    final String proxyTicket = assertion.getPrincipal().getProxyTicketFor(loginServiceUrl);
    final String username = assertion.getPrincipal().getName();

    // 获取到URLConnection或是HttpClient
    URL url = new URL(loginServiceUrl + "?ticket=" + proxyTicket);
    HttpURLConnection conn = null;
    try {
        conn = (HttpURLConnection) url.openConnection();
        // 业务处理
        // 将响应回来的myticket存储到Session中,以后每次发送请求都将携带这个myticket
        // 而被代理端也将myticket存储到Session中,以后每次互相访问都只需校验此值即可
        ...
    } catch (final Exception ex) {
        ...
    } finally {
        if (conn != null) {
            conn.disconnect();
        }
    }
    ...
}

这个方法将在票据第一次被校验成功的时候触发

在被代理端,我们新建一个地址为/loginService的服务,大致逻辑如下:

// 获取CAS票据
String proxyTicket = req.getParameter("ticket");
if (proxyTicket == null) {
    throw new WebScriptException(HttpServletResponse.SC_BAD_REQUEST, "Proxy ticket not specified");
}

try {

    // 使用CAS代理票据校验器校验PT
    final Cas20ProxyTicketValidator validator = new Cas20ProxyTicketValidator(casServerUrlPrefix);
    validator.setAcceptAnyProxy(true);
    final String serviceUrl = req.getServerPath() + req.getServicePath();
    final Assertion assertion = validator.validate(proxyTicket, serviceUrl);

    // 完成认证并从Session中获取myticket
    authenticationComponent.setCurrentUser(username);
    final String myTicket = authenticationService.getCurrentTicket();
   
    render(myTicket);
   
} catch (AuthenticationException ex) {
    logger.error("Login failed", ex);
    throw new WebScriptException(HttpServletResponse.SC_FORBIDDEN, "Login failed");

} catch (TicketValidationException ex) {
    logger.error("Ticket validate failred.", ex);
    throw new WebScriptException(HttpServletResponse.SC_FORBIDDEN, "Ticket validate failred");
   
} finally {
    // ...
}

可以看到,我们自己实例化了一个CAS的Validator来完成校验而不是通过那些CAS的Filter,这使得整个后端服务可以无需被CAS Filter拦截(当然,被拦截了也没啥关系)

OK,至此,一个较为完整的CAS代理就完成了,have fun

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics