CAS 笔记

By | 2021年12月31日

FAQ

1 CAS登录后,URL出现 “;jsessionid=xxx” 问题

项目上使用的 cas version: 5.2.0,cas-client-core-3.4.1。
CAS登录成功后,重定向的URL上携带ticket,通过ticket换取 cookie 时,URL上会携带”;jsessionid=xxx”,导致ember路由失败,页面无法访问。日志输出:2018-12-17 07:53:01,507 [https-jsse-nio-8443-exec-4] DEBUG org.jasig.cas.client.util.CommonUtils – serviceUrl generated: https://192.168.1.220:8443/falpolicy/;jsessionid=552D8ADBC0E3ADD5C581579BD7AD814A

通过分析 cas-client ticket验证的方法,发现 AbstractTicketValidationFilter.doFilter(),里面有一段:response.sendRedirect(constructServiceUrl(request, response)),
其中 constructServiceUrl()的最后一个参数是“bool encode”,参数描述:whether to encode the url or not (i.e. Jsession),它控制着是否将 url进行 response.encodeURL(url),这个值默认为true。
目前暂不知道为什么 response.encodeURL() 会认为 cookie 无法使用。

response.encodeURL() 方法对是否再URL上添加 session ID的说明: The implementation of this method includes the logic to determine whether the session ID needs to be encoded in the URL. For example, if the browser supports cookies, or session tracking is turned off, URL encoding is unnecessary.

问题解决方法很简单,设置 encodeServiceUrl 为 false就可以,这里使用 HashMap 方式设置(key为 encodeServiceUrl),casFilter 会调用init() 来将hashMap值初始化到变量中。

下面是我们最终的 casFilter 设置:

@Bean
public FilterRegistrationBean ValidationFilterRegistrationBean() {

        /*
         * falpos、cas在一个容器内,但存在302重定向,不能使用localhost,需要使用宿主主机的IP,但这也有问题:
         * 因为cas调用涉及两个地方,一个是falpos内部容器内调用它,一个是宿主主机(或外网)浏览器调用它。 容器内调用不能使用宿主主机的IP
         * 必须改用"falpos"。分析如下: cas在 Cas30ProxyReceivingTicketValidationFilter
         * 验证时就涉及到容器间的访问,这个过滤器的作用:
         * 验证URL上传入的Ticket,通过则换取falpos域的cookie。这个过程都是后台自己通过调用类似httpclient方式处理的,
         * 而不是通过302重定向。 因此是falpos容器与cas容器之间的通信。 解决办法是再添加一个
         * cas-server-inner-host,仅供falpos内部调用cas时使用。
         */

        FilterRegistrationBean authenticationFilter = new FilterRegistrationBean();

        Cas30ProxyReceivingTicketValidationFilter casFilter = new Cas30ProxyReceivingTicketValidationFilter();
        //casFilter.setEncodeServiceUrl(false);
        authenticationFilter.setFilter(casFilter);

        Map<String, String> initParameters = new HashMap<String, String>();
        initParameters.put("casServerUrlPrefix", this.getServer().rootContextInnerPath);
        initParameters.put("serverName", this.getClient().serverRootPath);
        initParameters.put("hostnameVerifier", "com.falsec.pom.config.CustomHostnameVerifierImpl");
        initParameters.put("encodeServiceUrl", "false");

        authenticationFilter.setInitParameters(initParameters);
        authenticationFilter.setOrder(1);
        List<String> urlPatterns = new ArrayList<String>();
        urlPatterns.add("/*");
        authenticationFilter.setUrlPatterns(urlPatterns);
        return authenticationFilter;
}

Cas30ProxyReceivingTicketValidationFilter 的继承关系是:
Cas30ProxyReceivingTicketValidationFilter > Cas20ProxyReceivingTicketValidationFilter > AbstractTicketValidationFilter。

AbstractTicketValidationFilter.class 源码

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;
        final String ticket = retrieveTicketFromRequest(request);

        if (CommonUtils.isNotBlank(ticket)) {
            logger.debug("Attempting to validate ticket: {}", ticket);

            try {
                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) {
                    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);

    }
}

AbstractCasFilter.class 源码

protected final String constructServiceUrl(final HttpServletRequest request, final HttpServletResponse response) {
    return CommonUtils.constructServiceUrl(request, response, this.service, this.serverName,
        this.protocol.getServiceParameterName(),
        this.protocol.getArtifactParameterName(), this.encodeServiceUrl);
}

CommonUtils.class 源码

/**
 * Constructs a service url from the HttpServletRequest or from the given
 * serviceUrl. Prefers the serviceUrl provided if both a serviceUrl and a
 * serviceName.
 *
 * @param request the HttpServletRequest
 * @param response the HttpServletResponse
 * @param service the configured service url (this will be used if not null)
 * @param serverNames the server name to  use to construct the service url if the service param is empty.  Note, prior to CAS Client 3.3, this was a single value.
 *           As of 3.3, it can be a space-separated value.  We keep it as a single value, but will convert it to an array internally to get the matching value. This keeps backward compatability with anything using this public
 *           method.
 * @param serviceParameterName the service parameter name to remove (i.e. service)
 * @param artifactParameterName the artifact parameter name to remove (i.e. ticket)
 * @param encode whether to encode the url or not (i.e. Jsession).
 * @return the service url to use.
 */
public static String constructServiceUrl(final HttpServletRequest request, final HttpServletResponse response,
    final String service, final String serverNames, final String serviceParameterName,
    final String artifactParameterName, final boolean encode) {
  if (CommonUtils.isNotBlank(service)) {
    return encode ? response.encodeURL(service) : service;
  }

  final String serverName = findMatchingServerName(request, serverNames);
  final URIBuilder originalRequestUrl = new URIBuilder(request.getRequestURL().toString(), encode);
  originalRequestUrl.setParameters(request.getQueryString());

  URIBuilder builder = null;

  boolean containsScheme = true;
  if (!serverName.startsWith("https://") && !serverName.startsWith("http://")) {
    builder = new URIBuilder(encode);
    builder.setScheme(request.isSecure() ? "https" : "http");
    builder.setHost(serverName);
    containsScheme = false;
  }  else {
    builder = new URIBuilder(serverName, encode);
  }


  if (!serverNameContainsPort(containsScheme, serverName) && !requestIsOnStandardPort(request)) {
    builder.setPort(request.getServerPort());
  }

  builder.setEncodedPath(request.getRequestURI());

  final List<String> serviceParameterNames = Arrays.asList(serviceParameterName.split(","));
  if (!serviceParameterNames.isEmpty() && !originalRequestUrl.getQueryParams().isEmpty()) {
    for (final URIBuilder.BasicNameValuePair pair : originalRequestUrl.getQueryParams()) {
      if (!pair.getName().equals(artifactParameterName) && !serviceParameterNames.contains(pair.getName())) {
        builder.addParameter(pair.getName(), pair.getValue());
      }
    }
  }

  final String result = builder.toString();
  final String returnValue = encode ? response.encodeURL(result) : result;
  LOGGER.debug("serviceUrl generated: {}", returnValue);
  return returnValue;
}

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注