CAS Client使用以及执行原理
流程介绍
CAS Client是利用Java Web中的Filter进行实现认证功能,客户端对CAS Server的认证流程分为以下步骤:
-
访问CAS Client服务
-
由于当前session中未检测到认证信息,会重定向到CAS Server地址进行认证
-
在CAS Server上进行认证
-
认证通过之后,CAS Server会重定向携带Ticket回到CAS Client地址
-
将携带的Ticket在后台进行CAS Server校验
-
CAS Server校验Ticket通过之后会返回当前认证的账号信息
-
在CAS Client拿到CAS Server返回的认证信息之后,缓存到当前session中
在以上流程中,CAS Client使用了两个Filter实现该主要功能。
使用
Maven依赖引入
<dependency> <groupId>org.jasig.cas.client</groupId> <artifactId>cas-client-core</artifactId> <version>3.6.4</version> </dependency>
这里使用的Spring Boot 2.x版本(即JDK8版本),所以使用的依赖版本为3.6.4版本,如果是Spring Boot 3.x的话(JDK17),应当使用如下依赖:
<dependency> <groupId>org.apereo.cas.client</groupId> <artifactId>cas-client-core</artifactId> <version>4.0.4</version> </dependency>
3.6.x以下的版本就不推荐引用了,之前的版本Spring版本为3.x,有兴趣的可以上Github看下官方版本:GitHub - apereo/java-cas-client: Apereo Java CAS Client。
配置
将CAS Client依赖包中的两个Filter过滤器注入到Spring容器即可。
Spring Boot的配置文件,CAS相关
cas: server: name: http://localhost:9999 login: url: http://localhost:8443/cas/login url: prefix: http://localhost:8443/cas/
AuthenticationFilter
该过滤器主要是用来检测当前客户端是否通过了CAS Server认证,如果没有通过的话则会调整到CAS Server地址,否则视为通过。
// 设置cas server的登陆地址 @Value("${cas.server.login.url}") private String casServerLoginUrl; // 设置cas server服务前缀,用于拼接cas server服务接口 @Value("${cas.server.url.prefix}") private String casServerUrlPrefix; // 设置当前服务名称 @Value("${cas.server.name}") private String serverName; @Bean public FilterRegistrationBean casClientFilterRegistrationBean() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); // 拦截所有接口请求,可根据各自的业务进行配置 filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.setFilter(new AuthenticationFilter()); filterRegistrationBean.addInitParameter("casServerLoginUrl", casServerLoginUrl); filterRegistrationBean.addInitParameter("casServerUrlPrefix", casServerUrlPrefix); filterRegistrationBean.addInitParameter("serverName", serverName); return filterRegistrationBean; }
TicketValidationFilter
该过滤器是用于获取Ticket以及校验Ticket是否有效,如果检验成功的话,则将校验接口返回的认证用户信息塞到当前session中。
该过滤器有多个版本,这边使用的是如下版本:
@Bean public FilterRegistrationBean ticketValidateFilterRegistrationBean() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); // 拦截所有接口请求,可根据各自的业务进行配置 filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.setFilter(new Cas20ProxyReceivingTicketValidationFilter()); filterRegistrationBean.addInitParameter("casServerLoginUrl", casServerLoginUrl); filterRegistrationBean.addInitParameter("casServerUrlPrefix", casServerUrlPrefix); filterRegistrationBean.addInitParameter("serverName", serverName); return filterRegistrationBean; }
其中配置与上个Filter参数一致即可;总共有四个是实现类,使用CAS20和CAS30的版本较多:
到这CAS Client的配置已经完成了。上述为主要配置,CAS Client中还有一些其他比较好用的Filter便于开发使用,比如AssertionThreadLocalFilter过滤器,将当前认证的用户信息保存到ThreadLocal当中,在开发过程中可以使用AssertionHolder.getAssertion()直接获取到当前用户信息。
执行原理
CAS Client的所有的配置项可以在ConfigurationKeys类中看到配置Key以及默认值;
Filter所有的参数初始化都是在initInternal方法进行的,在看doFilter方法时,需结合initInternal初始化方法一起,这样会了解的比较透彻。
通过解析CAS Client的两个过滤器来看看执行原理,先看看AuthenticationFilter中doFilter方法,重要步骤在如下一步一步说明:
@Override public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { final HttpServletRequest request = (HttpServletRequest) servletRequest; final HttpServletResponse response = (HttpServletResponse) servletResponse; // 1.判断当前接口请求是否需要被过滤 if (isRequestUrlExcluded(request)) { logger.debug("Request is ignored."); filterChain.doFilter(request, response); return; } final HttpSession session = request.getSession(false); final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null; // 2.判断当前session中是否存在认证用户信息 if (assertion != null) { filterChain.doFilter(request, response); return; } // 3.构建当前请求的服务地址,用于跳转cas server页面拼接的service参数 final String serviceUrl = constructServiceUrl(request, response); // 4.获取当前请求是否存在ticket final String ticket = retrieveTicketFromRequest(request); // 5.判断当前请求是否为配置的网关地址 final boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl); // 6.如果存在ticket获取是在配置的网关地址中,则直接放开 if (CommonUtils.isNotBlank(ticket) || wasGatewayed) { filterChain.doFilter(request, response); return; } final String modifiedServiceUrl; logger.debug("no ticket and no assertion found"); if (this.gateway) { logger.debug("setting gateway attribute in session"); modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl); } else { modifiedServiceUrl = serviceUrl; } logger.debug("Constructed service url: {}", modifiedServiceUrl); // 7.需拼接重定向地址页面 final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, getProtocol().getServiceParameterName(), modifiedServiceUrl, this.renew, this.gateway, this.method); logger.debug("redirecting to \"{}\"", urlToRedirectTo); this.authenticationRedirectStrategy.redirect(request, response, urlToRedirectTo); }
以上可以综合为,先校验配置的不需要校验的接口,判断当前session是否存在认证的信息,不存在则跳转到指定的CAS Server认证页面。
而在Cas20ProxyReceivingTicketValidationFilter过滤器中,则就是解析Ticket以及将返回的认证用户信息塞到当前session中;
@Override public final void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain) throws IOException, ServletException { // 1.检测是否执行代理请求,一般为false,除非一些特殊的业务场景才需要用到 if (!preFilter(servletRequest, servletResponse, filterChain)) { return; } final HttpServletRequest request = (HttpServletRequest) servletRequest; final HttpServletResponse response = (HttpServletResponse) servletResponse; // 2.获取Ticket,存在则进入解析,否则直接进入下一个过滤器 final String ticket = retrieveTicketFromRequest(request); if (CommonUtils.isNotBlank(ticket)) { logger.debug("Attempting to validate ticket: {}", ticket); try { // 3.去请求cas server的接口进行校验Ticket是否正确,正确并返回认证用户信息 // CAS30是请求/p3/serviceValidate,而CAS20是请求/serviceValidate,两者接口均可以 final Assertion assertion = this.ticketValidator.validate(ticket, constructServiceUrl(request, response)); logger.debug("Successfully authenticated user: {}", assertion.getPrincipal().getName()); // 4.校验Ticket通过之后,将认证用户信息塞入请求中 request.setAttribute(CONST_CAS_ASSERTION, assertion); // 默认是会塞入到session中,除非特殊业务场景 if (this.useSession) { request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion); } onSuccessfulValidation(request, response, assertion); // 5.默认是重定向到请求地址 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); // Ticket校验失败,封装错误信息返回 onFailedValidation(request, response); if (this.exceptionOnValidationFailure) { throw new ServletException(e); } response.sendError(HttpServletResponse.SC_FORBIDDEN, e.getMessage()); return; } } filterChain.doFilter(request, response); }
该过滤器用于校验Ticket并将返回的认证用户信息塞到session中,下次接口请求就无需校验了。
总结
以上为CAS Client的主要流程;在开发过程中大多数都是使用的Spring Boot作为业务开发框架,如果仅仅自己手动配置的CAS Client的话确实有点不和事宜,在CAS官方也提供了自动配置的依赖,目前找到了两种自动配置的依赖:
Unicon网址提供的CAS Client自动配置,该依赖很久没有更新了。
<dependency> <groupId>net.unicon.cas</groupId> <artifactId>cas-client-autoconfig-support</artifactId> <version>2.3.0-GA</version> </dependency>
CAS官方提供的,分为Spring Boot 2.x以及3.x版本。
<dependency> <groupId>org.jasig.cas.client</groupId> <artifactId>cas-client-support-springboot</artifactId> <version>3.6.4</version> </dependency>
<dependency> <groupId>org.apereo.cas.client</groupId> <artifactId>cas-client-support-springboot</artifactId> <version>4.0.4</version> </dependency>
两者代码几乎是一致的,参数配置的话均可以根据CasClientConfigurationProperties进行配置即可。