Cas 5.2.x版本使用 —— 代理认证实现SSO(十四)

一、什么是 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默认为SSLkeyStoreTypekeyStorePathkeyStorePasskeyManagerType默认为SunX509certificatePassword
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必须指定另外的两个参数:roxyCallbackUrlproxyReceptorUrl

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

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

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

/**
 * 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;
}

代理请求示例

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的回调地址,如果使用前文示例的代理端配置,我们就可以指定被代理端的allowedProxyChainshttps://elim:8043/app1/proxyCallback,这样当app1作为代理端来访问该被代理端时就能通过验证,得到正确的响应。

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

@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中,设置协议的正则规则即可。

{
  "@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)?://.*"
  }
}

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

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

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查看出现的验证方面问题。

<dependency>
    <groupId>org.apereo.cas</groupId>
    <artifactId>cas-server-support-validation</artifactId>
    <version>${cas.version}</version>
</dependency>
4.3 补充2:

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

五、我的源码

https://github.com/X-rapido/CAS_SSO_Record/tree/master/proxy-sso

六、测试

client1 访问受限地址,先进行登录。

访问代理的url时,直接返回给正确值。

一处登录,其他无需登录

七、视频演示

视频地址:https://v.qq.com/x/page/y0614dn6mpr.html

八、参考文档

九、总结

  • 不能一味的google,只有在实践中才能明白含义

  • 不能眼高手低,你以为看个图就明白了,其实里面遇到的各种问题真的会让你抓狂

  • 官网文档不一定是对的,该看源码就看源码,了解其中含义才明白配置参数表示什么意义

  • 凡事不要轻易下结论。先了解实现流程再写代码。


赞(52) 打赏
未经允许不得转载:优客志 » JAVA开发
分享到:

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏