CAS单点登录开源框架解读(八)--CAS单点登录客户端认证之客户端向服务端发起请求

时间:2024-03-16 10:49:03

客户端是如何向服务端发起请求的

前面客户端部分讲过,客户端的web.xml里配置了五个filter。下文中会使用序号来表示,具体的过滤器名称可以看上一章节。

1. 调用第1个filter

首先进入第一个filter:SingleSignOutFilter,因为现在是进行登录认证,没有登出请求,所以什么都没做,进入下一个filter。

2. 调用第2个filter

*AuthenticationFilter.java*
public class AuthenticationFilter extends AbstractCasFilter {
    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;
        
        if (isRequestUrlExcluded(request)) {
            logger.debug("Request is ignored.");
            filterChain.doFilter(request, response);
            return;
        }
        //此时重定向回来,还没有创建session,因此不能获取到assertion
        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 = retrieveTicketFromRequest(request);
        final boolean wasGatewayed = this.gateway && this.gatewayStorage.hasGatewayedAlready(request, serviceUrl);
        //由于CAS服务端认证成功后重定向到客户端会带ticket参数,因此ticket不为空进入此代码中
        if (CommonUtils.isNotBlank(ticket) || wasGatewayed) {
            filterChain.doFilter(request, response);
            return;//执行到这
        }
      ......
}

执行到对ticket的判断,CAS单点登录服务端认证成功后会带上ticket重定向到客户端,因为此时ticket不为空,所以进入下一个filter。

4. 调用第3个filter

该类Cas20ProxyReceivingTicketValidationFilter在org.jasig.cas.client.validation包中继承自AbstractTicketValidationFilter。首先执行doFilter方法,注意doFilter是在父类中进行调用的,我们先分析父类方法。

*AbstractTicketValidationFilter.java*
public abstract class AbstractTicketValidationFilter extends AbstractCasFilter {
    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;
        //从请求中获取到ticket
        final String ticket = retrieveTicketFromRequest(request);
        //判断ticket是否为空
        if (CommonUtils.isNotBlank(ticket)) {
            logger.debug("Attempting to validate ticket: {}", ticket);

            try {
                //通过ticket和service去校验ticket,并返回assertion用户认证信息
                final Assertion assertion = this.ticketValidator.validate(ticket,
                        constructServiceUrl(request, response));

                logger.debug("Successfully authenticated user: {}", assertion.getPrincipal().getName());

                request.setAttribute(CONST_CAS_ASSERTION, assertion);

                if (this.useSession) {
                //如果需要使用session,则把获取到的assertion认证信息放入会话
                    request.getSession().setAttribute(CONST_CAS_ASSERTION, assertion);
                }
                onSuccessfulValidation(request, response, assertion);

                if (this.redirectAfterValidation) {
                //认证成功后重定向到客户端的service地址
                    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);
}
…………
}

通过ticketValidator.validate(ticket, constructServiceUrl(request, response))对ticket进行校验。下面我们分析是如何进行ticket校验的。

5. 调用ticketValidator.validate校验ticket

AbstractUrlBasedTicketValidator类在org.jasig.cas.client.validation包中。

*AbstractUrlBasedTicketValidator.java*

public abstract class AbstractUrlBasedTicketValidator implements TicketValidator {
   …………
   public final Assertion validate(final String ticket, final String service) throws TicketValidationException {
        //计算需要请求到CAS服务端的url地址(注意这里是普通登录模式,所以校验地址为:serviceValidate
        final String validationUrl = constructValidationUrl(ticket, service);
        logger.debug("Constructing validation url: {}", validationUrl);

        try {
            logger.debug("Retrieving response from server.");
            //到CAS单点登录服务进行校验,获取返回结果
            final String serverResponse = retrieveResponseFromServer(new URL(validationUrl), ticket);

            if (serverResponse == null) {
                throw new TicketValidationException("The CAS server returned no response.");
            }
            logger.debug("Server response: {}", serverResponse);
            //解析从服务端获取到的信息
            return parseResponseFromServer(serverResponse);
        } catch (final MalformedURLException e) {
            throw new TicketValidationException(e);
        }
}
…………

注意retrieveResponseFromServer()会访问CAS单点登录服务端。

6. 调用retrieveResponseFromServer请求CAS单点登录服务端

AbstractCasProtocolUrlBasedTicketValidator类在org.jasig.cas.client.validation包中。

*AbstractCasProtocolUrlBasedTicketValidator.java*

public abstract class AbstractCasProtocolUrlBasedTicketValidator extends AbstractUrlBasedTicketValidator {

    protected AbstractCasProtocolUrlBasedTicketValidator(final String casServerUrlPrefix) {
        super(casServerUrlPrefix);
    }

    /**
     * Retrieves the response from the server by opening a connection and merely reading the response.
     */
    protected final String retrieveResponseFromServer(final URL validationUrl, final String ticket) {
        return CommonUtils.getResponseFromServer(validationUrl, getURLConnectionFactory(), getEncoding());
    }
}

最终调用CommonUtils.getResponseFromServer(),我们还要再往下找。

7. 调用CommonUtils.getResponseFromServer()

最终其实是通过后端发起http请求获取到cas服务端的数据。


## CommonUtils.java

public final class CommonUtils {
…………
public static String getResponseFromServer(final URL constructedUrl, final HttpURLConnectionFactory factory,
            final String encoding) {

        HttpURLConnection conn = null;
        InputStreamReader in = null;
        try {
            //创建http的连接
            conn = factory.buildHttpURLConnection(constructedUrl.openConnection());

            if (CommonUtils.isEmpty(encoding)) {
                in = new InputStreamReader(conn.getInputStream());
            } else {
                in = new InputStreamReader(conn.getInputStream(), encoding);
            }

            final StringBuilder builder = new StringBuilder(255);
            int byteRead;
            //通过输入流获取到服务端的响应
            while ((byteRead = in.read()) != -1) {
                builder.append((char) byteRead);
            }
			//通过流创建返回的数据
            return builder.toString();
        } catch (final Exception e) {
            LOGGER.error(e.getMessage(), e);
            throw new RuntimeException(e);
        } finally {
            closeQuietly(in);
            if (conn != null) {
                conn.disconnect();
            }
        }
}
…………
}

constructedUrl的传入值为:http://localhost:8080/cas/serviceValidate?ticket=ST-2-ZRix41JWjd3oHDoN3DhI-cas01.example.org&service=http%3A%2F%2Flocalhost%3A8088%2Fcas-client%2F
开始向服务端发验证请求,验证ticket,至此,完成了流程图的第5步:
CAS单点登录开源框架解读(八)--CAS单点登录客户端认证之客户端向服务端发起请求
后续我们就要对通过ST票据验证之后的数据进行解析以及ST验证成功和失败后返回给客户端的到底是什么信息,将在下一章节进行分析。