在我们的项目中,有这样一个场景:有两个服务holiday(节假日服务)和mainWeb(集成服务),这两个服务都集成了CAS,所有的请求都要经过CAS Server的认证。由于mainWeb内部会去调用holiday的服务,但是mainWeb的请求会被holiday配置的CAS拦截器AuthenticationFilter
拦截并重定向到CAS Server。这样我们的mainWeb就没法直接访问holiday服务。通过查阅官方文档,前辈博客发现,CAS的代理模式可以解决这个问题。
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代理模式实现的核心过滤器是Cas20ProxyReceivingTicketValidationFilter
,对于代理端,这个过滤器要配置在AuthenticationFilter
之前。另外,Cas20ProxyReceivingTicketValidationFilter
在代理端和被代理端的配置是不一样的,这个会在项目实战中具体指出。在我们的项目中我们使用的是Cas30ProxyReceivingTicketValidationFilter
,实际上Cas30ProxyReceivingTicketValidationFilter
是Cas20ProxyReceivingTicketValidationFilter
的子类。
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 | 被代理端 |
由于我们的项目使用的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.casgroupId>
<artifactId>cas-server-support-validationartifactId>
<version>${cas.version}version>
dependency>
作为代理端Cas30ProxyReceivingTicketValidationFilter
除了上面表提到的必须配置外,还需要额外配置两个参数:roxyCallbackUrl
和proxyReceptorUrl
。
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 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,在代理端需要指定接收哪些代理,acceptAnyProxy
和allowedProxyChains
完成这项工作。
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 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 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();
}