前期准备:
其中cas-client-core-3.2.1.jar为CAS客户端的核心jar包,cas-client-3.2.1-release.zip为CAS客户端的源码zip包。
web.xml部分代码
- <filter>
- <filter-name>CASFilter</filter-name>
- <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
- </filter>
-
- <filter>
- <filter-name>CASValidationFilter</filter-name>
- <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
- </filter>
-
- <filter>
- <filter-name>initLoginUserFilter</filter-name>
- <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
- </filter>
-
- <filter-mapping>
- <filter-name>CASFilter</filter-name>
- <url-pattern>/*</url-pattern>
- </filter-mapping>
-
- <filter-mapping>
- <filter-name>CASValidationFilter</filter-name>
- <url-pattern>/*</url-pattern>
- </filter-mapping>
-
- <filter-mapping>
- <filter-name>initLoginUserFilter</filter-name>
- <url-pattern>/*</url-pattern>
- </filter-mapping>
spring-context.xml部分代码
- <bean id="CASFilter" class="com.master.client.filter.AuthenticationFilter" init-method="initConfig"/>
- <bean id="CASValidationFilter" class="com.master.client.filter.TicketValidationFilter" init-method="initConfig"/>
- <bean id="initLoginUserFilter" class="com.master.client.filter.InitLoginUserFilter" init-method="initConfig"/>
cas_config.properties代码
- service = http://127.0.0.1:8090/webapp/main.do
- casServerUrlPrefix = http://127.0.0.1:8081/cas-server/
- casServerLoginUrl = http://127.0.0.1:8081/cas-server/login
- casServerLogoutUrl = http://127.0.0.1:8081/cas-server/logout
- errorUrl = http://127.0.0.1:8081/cas-server/error
首先,从web.xml配置信息可以看出,有三个过滤器负责处理单点登录流程,其中前两个为流程必须处理的过滤器,最后一个是我新增加的过滤器,主要功能是初始化用户信息和权限。既然是源码解读,我们必然要看源码。下面就从CAS客户端的源码给大家进行解读CAS单点登录客户端的工作流程。之后会单独写一篇关于CAS单点登录服务器的源码解读的文章。
AuthenticationFilter
从单点登录第一个过滤器开始解读,源码中的第一个过滤器是AuthenticationFilter,因为业务需求对此过滤器进行了二次开发。源码是从web.xml配置文件中读取初始化参数,我对此做了修改,从properties文件中读取初始化参数。把单点登录的配置参数定义到properties文件中,这样做的好处是对初始化配置参数统一管理,一目了然。源码走起。
AuthenticationFilter的父类AbstractCasFilter的初始化方法,主要工作是从properties中获取单点登录的系统参数。使大家看的更清楚,源码中省略了日志的输出。
- public void initConfig() throws ServletException {
- Properties conf = PropertiesUtil.getConfigProperties();
- setServerName(conf.getProperty("serverName", null));
- setService(conf.getProperty("service", null));
- setArtifactParameterName(conf.getProperty("artifactParameterName", "ticket"));
- setServiceParameterName(conf.getProperty("serviceParameterName", "service"));
- setEncodeServiceUrl(parseBoolean(conf.getProperty("encodeServiceUrl", "true")));
-
- setErrorUrl(conf.getProperty("errorUrl", null));
-
-
- initInternalConfig(conf);
- init();
- }
AuthenticationFilter的初始化方法。主要工作是从properties中获取单点登录的系统参数。使大家看的更清楚,源码中省略了日志的输出。
- public void initInternalConfig(Properties conf) throws ServletException {
- if (!isIgnoreInitConfiguration()) {
- setCasServerLoginUrl(conf.getProperty("casServerLoginUrl", null));
- setRenew(parseBoolean(conf.getProperty("renew", "false")));
- setGateway(parseBoolean(conf.getProperty("gateway", "false")));
-
- String gatewayStorageClass = conf.getProperty("gatewayStorageClass", null);
-
- if (gatewayStorageClass == null)
- return;
- try {
- this.gatewayStorage = ((GatewayResolver) Class.forName(gatewayStorageClass).newInstance());
- } catch (Exception e) {
- this.log.error(e, e);
- throw new ServletException(e);
- }
- }
- }
AuthenticationFilter的过滤方法。
- 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;
- final HttpSession session = request.getSession(false);
-
- final Assertion assertion = (session != null) ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;
-
- if (assertion != null) {
- filterChain.doFilter(request, response);
- return;
- }
-
- final String serviceUrl = constructServiceUrl(request, response);
-
- final String ticket = CommonUtils.safeGetParameter(request, getArtifactParameterName());
- final boolean wasGatewayed = this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
-
- if ((CommonUtils.isNotBlank(ticket)) || (wasGatewayed)) {
- filterChain.doFilter(request, response);
- return;
- }
-
- this.log.debug("no ticket and no assertion found");
-
- final String modifiedServiceUrl;
- if (this.gateway) {
- this.log.debug("setting gateway attribute in session");
- modifiedServiceUrl = this.gatewayStorage.storeGatewayInformation(request, serviceUrl);
- } else {
- modifiedServiceUrl = serviceUrl;
- }
-
- if (this.log.isDebugEnabled()) {
- this.log.debug("Constructed service url: " + modifiedServiceUrl);
- }
-
-
- final String urlToRedirectTo = CommonUtils.constructRedirectUrl(this.casServerLoginUrl, getServiceParameterName(),
- modifiedServiceUrl, this.renew, this.gateway);
-
- if (this.log.isDebugEnabled()) {
- this.log.debug("redirecting to \"" + urlToRedirectTo + "\"");
- }
-
- response.sendRedirect(urlToRedirectTo);
- }
当我们从浏览器访问配置了单点登录的应用系统时(http://127.0.0.1:8090/webapp/main.do),由于集成了CAS单点登录客户端,此时进入到第一个过滤器AuthenticationFilter(不考虑其他非单点登录的过滤器),执行以下操作:
- 从session中获取名为“_const_cas_assertion_”的assertion对象,判断assertion是否存在,如果存在,说明已经登录,执行下一个过滤器。如果不存在,执行第2步。
- 生成serviceUrl(http://127.0.0.1:8090/webapp/main.do),从request中获取票据参数ticket,判断ticket是否为空,如果不为空执行下一个过滤器。如果为空,执行第3步。
- 生成重定向URL(http://127.0.0.1:8081/cas-server/login?service=http://127.0.0.1:8090/webapp/main.do)。
- 跳转到单点登录服务器,显示登录页面,此时第一个过滤器执行完成。
TicketValidationFilter
AbstractTicketValidationFilter的过滤方法。
- 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 = CommonUtils.safeGetParameter(request, getArtifactParameterName());
-
- if (CommonUtils.isNotBlank(ticket)) {
- if (this.log.isDebugEnabled()) {
- this.log.debug("Attempting to validate ticket: " + ticket);
- }
- try {
-
- final Assertion assertion = this.ticketValidator.validate(ticket, constructServiceUrl(request, response));
-
- if (this.log.isDebugEnabled()) {
- this.log.debug("Successfully authenticated user: " + assertion.getPrincipal().getName());
- }
-
- request.setAttribute(CONST_CAS_ASSERTION, assertion);
-
- if (this.useSession) {
- request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion);
- }
- onSuccessfulValidation(request, response, assertion);
-
- if (this.redirectAfterValidation) {
- this.log.debug("Redirecting after successful ticket validation.");
- response.sendRedirect(constructServiceUrl(request, response));
- return;
- }
- } catch (final TicketValidationException e) {
- response.setStatus(403);
- this.log.info("获得用户认证信息失败:" + e.getMessage());
-
- response.sendRedirect(getErrorUrl() + "?errortype=" + e.getMessage() + "&" + createService());
- return;
- }
- }
-
- filterChain.doFilter(request, response);
- }
假设当执行完第一个过滤器后,跳转到CAS服务器端的登录页面,输入用户名和密码,验证通过后。CAS服务器端会生成ticket,并将ticket作为重新跳转到应用系统的参数(http://127.0.0.1:8090/webapp/main.do?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org)。此时又进入第一个过滤器AuthenticationFilter,由于存在ticket参数,进入到第二个过滤器TicketValidationFilter,执行以下操作:
- 从request获取ticket参数,如果ticket为空,继续处理下一个过滤器。如果参数不为空,验证ticket参数的合法性。
- 验证ticket,TicketValidationFilter的validate方法通过httpClient访问CAS服务器端(http://127.0.0.1:8081/cas-server/serviceValidate?ticket=ST-1-4hH2s5tzsMGCcToDvGCb-cas01.example.org&service=http://127.0.0.1:8090/webapp/main.do)验证ticket是否正确,并返回assertion对象。如果验证失败,抛出异常,跳转到错误页面。如果验证成功,session会以"_const_cas_assertion_"的名称保存assertion对象,继续处理下一个过滤器。
InitLoginUserFilter
InitLoginUserFilter的过滤方法。
- public final void doFilter(final ServletRequest servletRequest,
- final ServletResponse servletResponse, final FilterChain filterChain)
- throws IOException, ServletException {
-
- try {
-
-
-
-
- filterChain.doFilter(request, response);
- return;
- } catch (Exception e) {
-
- res.sendRedirect(getErrorUrl() + "?errortype=" + e.getMessage() + "&" + createService());
- }
- }
这个过滤器是我新增加的过滤器,以满足应用系统的特殊业务。当到达这个过滤器的时候,说明CAS服务器已经登录成功,此过滤器会对应用系统所需要的信息进行初始化(用户基本信息和权限等)。完成操作后,显示应用系统的首页。如果出现异常,跳转到错误页面。