CAS提供的客户端是基于Servlet写的,也就是说我们如果使用非Servlet应用,那么客户端是无法继承的,项目中使用没有使用Zuul来作为网关,而是使用Gateway,所以我们需要将原有的逻辑迁移到Gateway上
根据查询原有客户端我们可以发现,客户端本质是一些Servlet拦截器,在拦截器中对登录和验证进行各种逻辑,而Gateway也提供了拦截器GlobalFilter,所以我们只要实现GlobalFilter将原有的逻辑迁移到GlobalFilter中并在Spring中注册将其作用到Gateway即可,并且将代码封装成starter实现开箱即用。
我们在Servelt应用中集成CAS客户端主要使用的是Cas30ProxyReceivingTicketValidationFilter和AuthenticationFilter两个拦截器,通过查看源码我们可以发现Cas30ProxyReceivingTicketValidationFilter的主要作用是验证Ticket,就是登陆完成后服务端会下发给一个临时的Ticket来验证请求的正确性,用ST来表示,这个类主要用来验证ST和在处理代理模式下的逻辑**(代理模式:两个应用同时集成了cas客户单,现在A应用要通过http协议直接调用B应用的接口,类似于Nginx,如果是这这类请求走的验证Ticket逻辑是不一样的,CAS会生成PGT和PGTIOU,通过PGTIOU作为键来对应PGT来验证代理服务器时候之前已经认证过,代理这部分放在以后再说明)**,下面是根据官网的流程图和结合客户端代码绘制的总体的CAS验证流程图
上文说到Cas30ProxyReceivingTicketValidationFilter的核心作用是验证Ticket,而具体的验证逻辑由TicketValidator类来控制,我们可以在Cas30ProxyReceivingTicketValidationFilter的父类Cas20ProxyReceivingTicketValidationFilter中的getTicketValidator方法中查看初始化TicketValidator的逻辑,总体分为2总一种是初始化代理端的TicketValidator,另一种是初始化被代理端的TicketValidator,下面的注释已经说明,而getTicketValidator方法又会在初始化Cas30ProxyReceivingTicketValidationFilter时被调用。通过源码我们可以发现根据逻辑的不同其初始化了Cas20ServiceTicketValidator或者Cas20ProxyTicketValidator的ticket验证器,前者是作为代理端的ticket验证器(类似nginx)如果不设置代理用这个ticket验证器即可,后者是被代理端的验证器,处理被代理端的ticket验证逻辑
protected final TicketValidator getTicketValidator(final FilterConfig filterConfig) {
final boolean allowAnyProxy = getBoolean(ConfigurationKeys.ACCEPT_ANY_PROXY);
final String allowedProxyChains = getString(ConfigurationKeys.ALLOWED_PROXY_CHAINS);
final String casServerUrlPrefix = getString(ConfigurationKeys.CAS_SERVER_URL_PREFIX);
final Class extends Cas20ServiceTicketValidator> ticketValidatorClass = getClass(ConfigurationKeys.TICKET_VALIDATOR_CLASS);
final Cas20ServiceTicketValidator validator;
//根据servlet拦截器的初始化参数来判断,如果是被代理端(被调用的应用)
if (allowAnyProxy || CommonUtils.isNotBlank(allowedProxyChains)) {
final Cas20ProxyTicketValidator v = createNewTicketValidator(ticketValidatorClass, casServerUrlPrefix,
this.defaultProxyTicketValidatorClass);
v.setAcceptAnyProxy(allowAnyProxy);
v.setAllowedProxyChains(CommonUtils.createProxyList(allowedProxyChains));
validator = v;
} else {
//如果是代理端(类似nginx)
validator = createNewTicketValidator(ticketValidatorClass, casServerUrlPrefix,
this.defaultServiceTicketValidatorClass);
}
validator.setProxyCallbackUrl(getString(ConfigurationKeys.PROXY_CALLBACK_URL));
validator.setProxyGrantingTicketStorage(this.proxyGrantingTicketStorage);
final HttpURLConnectionFactory factory = new HttpsURLConnectionFactory(getHostnameVerifier(),
getSSLConfig());
validator.setURLConnectionFactory(factory);
validator.setProxyRetriever(new Cas20ProxyRetriever(casServerUrlPrefix, getString(ConfigurationKeys.ENCODING), factory));
validator.setRenew(getBoolean(ConfigurationKeys.RENEW));
validator.setEncoding(getString(ConfigurationKeys.ENCODING));
final Map additionalParameters = new HashMap();
final List params = Arrays.asList(RESERVED_INIT_PARAMS);
for (final Enumeration> e = filterConfig.getInitParameterNames(); e.hasMoreElements(); ) {
final String s = (String) e.nextElement();
if (!params.contains(s)) {
additionalParameters.put(s, filterConfig.getInitParameter(s));
}
}
validator.setCustomParameters(additionalParameters);
return validator;
}
我们查看Cas30ProxyReceivingTicketValidationFilter的父类AbstractTicketValidationFilter的doFilter方法,主要逻辑已经在下面代码中做出注释,那么现在我们要做的主要事情已经清晰,重写拦截器中根据不同的功能配置不同协议的TicketValidator(我采用的是CAS3协议对应的是Cas30ServiceTicketValidator和Cas30ProxyTicketValidator,前者作为代理服务端验证ticket的逻辑,后者作为验证被代理端的ticket逻辑)每种不同协议的实现类会调用Cas Service的对应协议的URL具体可以查看各个协议实现的TicketValidator的getUrlSuffix()方法,然后验证成功后再将信息存在缓存中
public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
final FilterChain filterChain) throws IOException, ServletException {
if (!preFilter(servletRequest, servletResponse, filterChain)) {
return;
}
final HttpServletRequest request = (HttpServletRequest) servletRequest;
final HttpServletResponse response = (HttpServletResponse) servletResponse;
final String ticket = retrieveTicketFromRequest(request);
if (CommonUtils.isNotBlank(ticket)) {
logger.debug("Attempting to validate ticket: {}", ticket);
try {
//验证ticket的有效性
final Assertion assertion = this.ticketValidator.validate(ticket,
constructServiceUrl(request, response));
logger.debug("Successfully authenticated user: {}", assertion.getPrincipal().getName());
request.setAttribute(CONST_CAS_ASSERTION, assertion);
//验证成功存入Session
if (this.useSession) {
request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion);
}
onSuccessfulValidation(request, response, assertion);
//验证成功重定向到原来访问的链接
if (this.redirectAfterValidation) {
logger.debug("Redirecting after successful ticket validation.");
response.sendRedirect(constructServiceUrl(request, response));
return;
}
} catch (final TicketValidationException e) {
logger.debug(e.getMessage(), e);
onFailedValidation(request, response);
if (this.exceptionOnValidationFailure) {
throw new ServletException(e);
}
response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage());
return;
}
}
filterChain.doFilter(request, response);
}
serlvet客户单中采用缓存在session中的方式来缓存用户信息,在gateway中没有session的概念,因此我用cookie来解决用户标识的问题,在用户完成验证后会往cookie中写入信息,在请求时带上cooike信息然后再redis中判断用户是否登录过来模拟实现session
上面提到我是使用redis来当做分布式session来使用,那么单点登出实现的逻辑是在gateway中提供统一的单点退出接口,用户调用此接口后清除redies中缓存的信息,并且重定向到CAS Service 调用/logout方法,并且在url中带上service参数后面跟上退出成功后cas重定向到的地址,这样在清除掉本地缓存的时又清楚了CAS Service中缓存的TGT信息实现单点退出.
https://github.com/liushprofessor/cas_demo