一、什么是 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
在代理端配置的示例
/** * 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的回调地址,如果使用前文示例的代理端配置,我们就可以指定被代理端的allowedProxyChains
为https://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
八、参考文档
CAS代理协议流程图:https://github.com/X-rapido/CAS_SSO_Record/blob/master/assets/pdf/cas_proxy_protocol.pdf
CAS协议3.0规范,参考:https://apereo.github.io/cas/5.2.x/protocol/CAS-Protocol-Specification.html
cas-client.jar 配置项参数,参考:https://github.com/apereo/java-cas-client
九、总结
不能一味的google,只有在实践中才能明白含义
不能眼高手低,你以为看个图就明白了,其实里面遇到的各种问题真的会让你抓狂
官网文档不一定是对的,该看源码就看源码,了解其中含义才明白配置参数表示什么意义
凡事不要轻易下结论。先了解实现流程再写代码。