http://www.ibloger.net/article/3129.html

一、什么是 Cas代理认证

考虑这样一种场景:有两个应用App1和App2,它们都是受Cas Server保护的,即请求它们时都需要通过Cas Server的认证。现需要在App1中通过Http请求访问App2,显然该请求将会被App2配置的Cas的AuthenticationFilter拦截并转向Cas Server,Cas Server将引导用户进行登录认证,这样我们也就不能真正的访问到App2了。针对这种应用场景,Cas也提供了对应的支持。

二、Cas 代理认证原理

Cas Proxy可以让我们轻松的通过App1访问App2时通过Cas Server的认证,从而访问到App2。其主要原理是这样的,App1先通过Cas Server的认证,然后向Cas Server申请一个针对于App2的proxy ticket,之后在访问App2时把申请到的针对于App2的proxy ticket以参数ticket传递过去。App2的AuthenticationFilter将拦截到该请求,发现该请求携带了ticket参数后将放行交由后续的Ticket Validation Filter处理。Ticket Validation Filter将会传递该ticket到Cas Server进行认证,显然该ticket是由Cas Server针对于App2发行的,App2在申请校验时是可以校验通过的,这样我们就可以正常的访问到App2了。

针对Cas Proxy的原理,官网有一张图很能说明问题(官网看着不爽,我重新写了一个),如下所示。

三、Cas 代理如何配置

Cas Proxy实现的核心是Cas20ProxyReceivingTicketValidationFilter,该Filter是Ticket Validation Filter的一种。使用Cas Proxy时我们需要使用Cas20ProxyReceivingTicketValidationFilter作为我们的Ticket Validation Filter,而且对于代理端而言该Filter需要放置在AuthenticationFilter之前。对于上述应用场景而言,App1就是我们的代理端,而App2就是我们的被代理端。

Cas20ProxyReceivingTicketValidationFilter在代理端与被代理端的配置是不一样的。

在我的Demo中,用的是Cas30ProxyReceivingTicketValidationFilter验证器,但其实如果你观察源码就会发现Cas30ProxyReceivingTicketValidationFilter实际上是继承自Cas20ProxyReceivingTicketValidationFilter,通过下面的图可以看出继承关系


我为什么会使用Cas30ProxyReceivingTicketValidationFilter?

这是因为我喜欢最新的内容,CAS是基于HTTP2,3的协议,要求每个组件都可以通过特定的URI访问。

如下表格, Cas30 用的就是/p3/xxx,这点通过debug或查看cas-client.jar源码就能发现.

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 主机名验证程序类名称,用于进行反向通话 否
四、项目实战

项目采用 SpringBoot + maven 方式构建
文件名称 域名 功能
client1 http://client1.com:8888/ 客户端1 后台接口(代理方)
client2 http://client2.com:8889/ 客户端2 后台接口(被代理方)
cas-server-rest http://cas.server.com:8484/cas CAS-Server
1、本地hosts文件配置如下

127.0.0.1 cas.server.com
127.0.0.1 client1.com
127.0.0.1 client2.com

2、代理方配置

既然Cas20ProxyReceivingTicketValidationFilter是一个Ticket Validation Filter,所以之前我们介绍的Ticket Validation Filter需要配置的参数,在这里也需要配置,所不同的是对于代理端的Cas20ProxyReceivingTicketValidationFilter必须指定另外的两个参数:roxyCallbackUrl和proxyReceptorUrl。

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

proxyReceptorUrl:该地址是proxyCallbackUrl相对于代理端的一个地址, Cas20ProxyReceivingTicketValidationFilter将根据该地址来决定请求是否来自Cas Server的回调。

下面是一个Cas30ProxyReceivingTicketValidationFilter在代理端配置的示例
Java

/**

  • Cas30ProxyReceivingTicketValidationFilter 验证过滤器

  • 该过滤器负责对Ticket的校验工作,必须启用它
    *

  • @return
    /
    @Bean
    public FilterRegistrationBean filterValidationRegistration() {
    FilterRegistrationBean registration = new FilterRegistrationBean();
    registration.setFilter(new Cas30ProxyReceivingTicketValidationFilter());
    // 设定匹配的路径
    registration.addUrlPatterns(“/
    “);
    Map<String, String> initParameters = new HashMap();
    initParameters.put(“casServerUrlPrefix”, CasConfig.CAS_SERVER_PATH);
    initParameters.put(“serverName”, CasConfig.SERVER_NAME);

    initParameters.put(“proxyCallbackUrl”, CasConfig.PROXY_CALLBACK_URL);
    initParameters.put(“proxyReceptorUrl”, “/proxy/callback”);

    // 是否对serviceUrl进行编码,默认true:设置false可以在302对URL跳转时取消显示;jsessionid=xxx的字符串
    // 观察CommonUtils.constructServiceUrl方法可以看到
    initParameters.put(“encodeServiceUrl”, “false”);

    registration.setInitParameters(initParameters);
    // 设定加载的顺序
    registration.setOrder(1);
    return registration;
    }

代理请求示例
Java

package com.tingfeng.controller;

import com.tingfeng.utils.HttpProxy;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.util.AssertionHolder;
import org.springframework.http.HttpMethod;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.net.URLEncoder;
import java.security.Principal;
import java.util.Arrays;

/**

  • Cas代理回调处理
    */
    @RestController
    @RequestMapping(“/proxy”)
    public class CasProxyController {

    @GetMapping(“/users”)
    public String proxyUsers(HttpServletRequest request, HttpServletResponse response) {

     String result = "无结果";
     try {
         String serviceUrl = "http://client2.com:8889/user/users";
    
         AttributePrincipal principal = (AttributePrincipal) request.getUserPrincipal();
    
         //1、获取到AttributePrincipal对象

    // AttributePrincipal principal = AssertionHolder.getAssertion().getPrincipal();

         if (principal == null) {
             return "用户未登录";
         }
    
         //2、获取对应的(PT)proxy ticket
         String proxyTicket = principal.getProxyTicketFor(serviceUrl);
         if (proxyTicket == null) {
             return "PGT 或 PT 不存在";
         }
    
         //3、请求被代理应用时将获取到的proxy ticket以参数ticket进行传递
         String url = serviceUrl + "?ticket=" + URLEncoder.encode(proxyTicket, "UTF-8");
         result = HttpProxy.httpRequest(url, "", HttpMethod.GET);
    
         System.out.println("result结果:" + result);
     } catch (Exception e) {
         e.printStackTrace();
     }
    
     return result;

    }

@GetMapping("/books")
public String proxyBooks(HttpServletRequest request, HttpServletResponse response) {

    String result = "无结果";
    try {
        String serviceUrl = "http://client2.com:8889/book/books";

        //1、获取到AttributePrincipal对象
        AttributePrincipal principal = AssertionHolder.getAssertion().getPrincipal();
        if (principal == null) {
            return "用户未登录";
        }

        //2、获取对应的(PT)proxy ticket
        String proxyTicket = principal.getProxyTicketFor(serviceUrl);
        if (proxyTicket == null) {
            return "PGT 或 PT 不存在";
        }

        //3、请求被代理应用时将获取到的proxy ticket以参数ticket进行传递
        String url = serviceUrl + "?ticket=" + URLEncoder.encode(proxyTicket, "UTF-8");
        result = HttpProxy.httpRequest(url, "", HttpMethod.GET);

        System.out.println("result结果:" + result);
    } catch (Exception e) {
        e.printStackTrace();
    }

    return result;
}

}

3、被代理端配置

在被代理端Cas30ProxyReceivingTicketValidationFilter是扮演Ticket Validation Filter的角色,它可以验证正常通过Cas Server登录认证成功后返回的ticket,也可以认证来自其它代理端传递过来的proxy ticket(PT),当然,最终的认证都是通过Cas Server来完成的。既然Cas30ProxyReceivingTicketValidationFilter在被代理端是作为Ticket Validation Filter来使用的,可以有的参数其都可以配置。在被代理端需要配置一个参数用以表示接受来自哪些应用的代理,这个参数可以是acceptAnyProxy,也可以是allowedProxyChains。

acceptAnyProxy:表示接受所有的,其对应的参数值是true或者false

allowedProxyChains:指定具体接受哪些应用的代理,多个应用就写多行,allowedProxyChains的值对应的是代理端提供给Cas Server的回调地址,如果使用前文示例的代理端配置,我们就可以指定被代理端的allowedProxyChains为https://elim:8043/app1/proxyCallback,这样当app1作为代理端来访问该被代理端时就能通过验证,得到正确的响应。

下面是一个被代理端配置Cas30ProxyReceivingTicketValidationFilter的配置示例。 多了一个acceptAnyProxy参数而已。
Java

@Bean
public FilterRegistrationBean filterValidationRegistration() {
FilterRegistrationBean registration = new FilterRegistrationBean();
registration.setFilter(new Cas30ProxyReceivingTicketValidationFilter());
// 设定匹配的路径
registration.addUrlPatterns(“/*”);
Map<String, String> initParameters = new HashMap();
initParameters.put(“casServerUrlPrefix”, CasConfig.CAS_SERVER_PATH);
initParameters.put(“serverName”, CasConfig.SERVER_NAME);
initParameters.put(“encodeServiceUrl”, “false”);

initParameters.put("acceptAnyProxy", "true");   // 接收任何代理

registration.setInitParameters(initParameters);
// 设定加载的顺序
registration.setOrder(1);
return registration;

}

4、cas-server端配置

proxyCallbackUrl回调地址官方默认必须要配置一个https的协议,这也就意味着我们的线上项目,必须支持https才可以。而我们的线上项目,大多只支持http协议,这该怎么办呢?

经过不懈的google和cas官网文档查询,并没有发现有相关文章介绍如何支持http协议。最后经过debug查询cas-server源码发现,最终cas是根据一个有关代理认证策略的正则有关系,默认不允许颁发PGT,于是就修改了json格式service文件服务中的策略。问题得以解决。(非常郁闷,为啥CAS默认都https了,怎么就不能好好地默认代理策略开放呢!)

cas代理认证取消HTTPS,支持HTTP方式

默认的json服务策略中,不支持代理返回PGT,需要在自定义的service中增加proxyPolicy策略。在pattern中,设置协议的正则规则即可。
Markdown

{
@class” : “org.apereo.cas.services.RegexRegisteredService”,
“serviceId” : “^(https|http|imaps)://.*”,
“name” : “HTTPS and HTTP and IMAPS”,
“id” : 10000001,
“description” : “This service definition authorizes all application urls that support HTTPS and HTTP and IMAPS protocols.”,
“evaluationOrder” : 10000,
“attributeReleasePolicy”: {
@class”: “org.apereo.cas.services.ReturnAllAttributeReleasePolicy”
},
“proxyPolicy”: {
@class”: “org.apereo.cas.services.RegexMatchingRegisteredServiceProxyPolicy”,
“pattern”: “^(https|http)?://.*”
}
}

002.png
4.1 cas代理使用HTTPS,支持HTTP方式

如果你认真的看源代码,会观察到cas-server的HttpBasedServiceCredentialsAuthenticationHandler类中authenticate方法,如下
Java

public HandlerResult authenticate(Credential credential) throws GeneralSecurityException {
HttpBasedServiceCredential httpCredential = (HttpBasedServiceCredential)credential;
if (!httpCredential.getService().getProxyPolicy().isAllowedProxyCallbackUrl(httpCredential.getCallbackUrl())) {
LOGGER.warn(“Proxy policy for service [{}] cannot authorize the requested callback url [{}].”, httpCredential.getService().getServiceId(), httpCredential.getCallbackUrl());
throw new FailedLoginException(httpCredential.getCallbackUrl() + “ cannot be authorized”);
} else {
LOGGER.debug(“Attempting to authenticate [{}]”, httpCredential);
URL callbackUrl = httpCredential.getCallbackUrl();
if (!this.httpClient.isValidEndPoint(callbackUrl)) {
throw new FailedLoginException(callbackUrl.toExternalForm() + “ sent an unacceptable response status code”);
} else {
return new DefaultHandlerResult(this, httpCredential, this.principalFactory.createPrincipal(httpCredential.getId()));
}
}
}

getProxyPolicy()方法获取service的策略并根据isAllowedProxyCallbackUrl方法判断正则表达式是否允许使用https或http,那么直接设置正则就好了。将上面的”pattern”: “^http?://.*” 修改为”pattern”: “^(https|http)?://.*”即可,我已经测试过没问题。

使用https,你需要配置client支持https才行。配置很简单,查看我的github源码即可。 如果你不会配置,可能会遇到cas-server向客户端发送代理回调时出现PKIX path building failed等问题。查看下面的常见问题解决即可。

4.2 补充1:

在pom.xml文件中,加入如下配置,虽然对正确结果没有影响,只是方便Debug查看出现的验证方面问题。

org.apereo.cas cas-server-support-validation ${cas.version}

4.3 补充2:

另外看到官网给出的参数有关于http代理的,但是具体和代理认证有没有关系没得到答案,难道是取消cas服务端的https协议?测试过程中没有明星感知,就删除了,后面的就没再application.properties中配置,如有知道代表什么含义的,还请给我留言,感激不尽。
http-proxying:https://apereo.github.io/cas/5.2.x/installation/Configuration-Properties.html#http-proxying

作者:Jeebiz  创建时间:2024-01-17 23:44
最后编辑:Jeebiz  更新时间:2024-05-06 16:13