之前做的项目要迭代多租户功能,不同租户对应同一个数据库的多个schema,进行逻辑上的数据隔离。每个租户要求独立域名,但是前端服务和后端服务仍然只有一份(部署是集群部署)。本来只需要在dns服务器上配置一下域名解析就可以了,但是要集成单点登录cas和安全框架(security), cas 中原生的类是不支持多个serve-name(服务域名)的,需要修改一下cas中的一些组件,所以总结一下。

看一下关键的配置文件:SecurityConfiguration

package com.xxx.config;

import java.util.ArrayList;
import java.util.List;

import javax.annotation.Resource;

import com.xxx.handler.multidomain.MDCasAuthenticationEntryPoint;
import com.xxx.handler.multidomain.MDCasServiceTicketValidator;
import com.xxx.handler.multidomain.MDSimpleUrlLogoutSuccessHandler;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.cas.authentication.CasAssertionAuthenticationToken;
import org.springframework.security.cas.authentication.CasAuthenticationProvider;
import org.springframework.security.cas.web.CasAuthenticationFilter;
import org.springframework.security.core.userdetails.AuthenticationUserDetailsService;
import org.springframework.security.web.authentication.logout.LogoutFilter;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;

/**
 * cas与security整合,需要配置的对象
 * <p>
 * 在cas与security整合中,首先需要做的是将应用的登录认证入口改为使用CasAuthenticationEntryPoint。
 * 所以首先我们需要配置一个CasAuthenticationEntryPoint对应的bean,
 * 然后指定需要进行登录认证时使用该AuthenticationEntryPoint。
 * 配置CasAuthenticationEntryPoint时需要指定一个ServiceProperties,
 * 该对象主要用来描述service (Cas概念) 相关的属性,主要是指定在Cas Server认证成功后将要跳转的地址。
 * <p>
 * CasAuthenticationFilter认证过滤器,负责认证跳转和票据验证
 */
@Configuration
@EnableConfigurationProperties(value = {com.xxx.config.CasServerProperties.class})
public class SecurityConfiguration {

    @Resource
    private com.xxx.config.CasServerProperties casServerProperties;

    @Resource
    private AuthenticationUserDetailsService<CasAssertionAuthenticationToken> userDetailsService;

    @Bean
    public AuthenticationManager authenticationManager(CasAuthenticationProvider provider) {
        List<AuthenticationProvider> providers = new ArrayList<>();
        providers.add(provider);
        ProviderManager providerManager = new ProviderManager(providers);
        return providerManager;
    }

    /**
     * 我们自己应用的配置信息,该对象主要用于构建CasAuthenticationEntryPoint。
     *
     * @return
     */
    @Bean
    public ServiceProperties serviceProperties() {
        ServiceProperties serviceProperties = new ServiceProperties();
        //设置默认的cas登陆后回跳地址
        serviceProperties.setService(casServerProperties.getServerName() + "/login");
        //设置我们应用是否敏感
        serviceProperties.setSendRenew(false);
        //设置是否对未拥有ticket的访问均需要验证
        serviceProperties.setAuthenticateAllArtifacts(true);
        return serviceProperties;
    }

    /**
     * CAS认证过滤器,主要实现票据认证和认证成功后的跳转。
     *
     * @param auth
     * @param serviceProperties
     * @return
     */
    @Bean
    public CasAuthenticationFilter casAuthenticationFilter(AuthenticationManager auth, ServiceProperties serviceProperties) {
        CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
        //给过滤器设置我们应用的基本配置
        casAuthenticationFilter.setServiceProperties(serviceProperties);
        //给过滤器设置认证管理器
        casAuthenticationFilter.setAuthenticationManager(auth);
        //设置过滤器到cas server认证的地址
        casAuthenticationFilter.setFilterProcessesUrl(casServerProperties.getCasServerLoginUrl());
        //设置是否继续执行其他过滤器,在完成认证前
        casAuthenticationFilter.setContinueChainBeforeSuccessfulAuthentication(false);
        //设置认证成功后的处理handler, 目前使用默认的SavedRequestAwareAuthenticationSuccessHandler
//        casAuthenticationFilter.setAuthenticationSuccessHandler(new AddressBarUrlAuthenticationSuccessHandler());
//        casAuthenticationFilter.setAuthenticationSuccessHandler(new SimpleUrlAuthenticationSuccessHandler("/demo/admin"));
        return casAuthenticationFilter;
    }

    /**
     * 认证的入口,即跳转至服务端的cas地址
     * security框架整合cas认证的入口,也就是security不再走自己的认证入口,而是cas的,该对象就是cas的认证入口
     *
     * @param serviceProperties
     * @return
     */
    @Bean
    public MDCasAuthenticationEntryPoint mCasAuthenticationEntryPoint(ServiceProperties serviceProperties) {
        MDCasAuthenticationEntryPoint mdCasAuthenticationEntryPoint = new MDCasAuthenticationEntryPoint();
        //security框架整合cas认证的入口,也就是security不再走自己的认证入口,而是cas的,该对象就是cas的认证入口
        mdCasAuthenticationEntryPoint.setServiceProperties(serviceProperties);
        mdCasAuthenticationEntryPoint.setLoginUrl(casServerProperties.getCasServerLoginUrl());
        return mdCasAuthenticationEntryPoint;
    }

    /**
     * 配置TicketValidator在登录认证成功后验证ticket
     * 该对象就是一个ticket校验器
     *
     * @return
     */
    @Bean
    public MDCasServiceTicketValidator cas20ServiceTicketValidator() {
        //需要设置cas server的前缀,也就是根路径
        return new MDCasServiceTicketValidator(casServerProperties.getCasServerUrlPrefix());
    }

    /**
     * 该对象为cas校验对象,TicketValidator、AuthenticationUserDetailService属性必须设置;
     * serviceProperties属性主要应用于ticketValidator用于去cas服务端校验ticket
     *
     * @param userDetailsService
     * @param serviceProperties
     * @param ticketValidator
     * @return
     */
    @Bean("casProvider")
    public CasAuthenticationProvider casAuthenticationProvider(AuthenticationUserDetailsService<CasAssertionAuthenticationToken>
                                                                           userDetailsService,
                                                               ServiceProperties serviceProperties,
                                                               MDCasServiceTicketValidator ticketValidator) {
        CasAuthenticationProvider provider = new CasAuthenticationProvider();
        provider.setKey("casProvider");
        provider.setServiceProperties(serviceProperties);
        provider.setTicketValidator(ticketValidator);
        provider.setAuthenticationUserDetailsService(userDetailsService);
        return provider;
    }

    @Bean
    public LogoutFilter logoutFilter(MDSimpleUrlLogoutSuccessHandler mdSimpleUrlLogoutSuccessHandler) {
        LogoutFilter logoutFilter = new LogoutFilter(mdSimpleUrlLogoutSuccessHandler, new SecurityContextLogoutHandler());
        logoutFilter.setFilterProcessesUrl("/checkSession");
        return logoutFilter;
    }

    @Bean
    public MDSimpleUrlLogoutSuccessHandler mdSimpleUrlLogoutSuccessHandler() {
        String logoutRedirectPath = casServerProperties.getCasServerLogoutUrl();
        MDSimpleUrlLogoutSuccessHandler mdSimpleUrlLogoutSuccessHandler = new MDSimpleUrlLogoutSuccessHandler();
        mdSimpleUrlLogoutSuccessHandler.setDefaultTargetUrl(logoutRedirectPath);
        return mdSimpleUrlLogoutSuccessHandler;
    }

}


MDCasAuthenticationEntryPoint我们自己定义的入口类,实现接口AuthenticationEntryPoint,主要是copy了CasAuthenticationEntryPoint的内容,修改了createServiceUrl的逻辑,这个方法主要是创建服务地址(域名),一个项目只能有一个,因此改造它,根据请求的域名动态进行生成。


package com.xxx.handler.multidomain;

import java.io.IOException;

import javax.annotation.Resource;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.xxx.config.CasServerProperties;
import com.xxx.util.ProgramVariable;

import org.jasig.cas.client.util.CommonUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.cas.ServiceProperties;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.util.Assert;

/**
 * author:lgq
 * date:2022-1-1
 */
public class MDCasAuthenticationEntryPoint implements AuthenticationEntryPoint,
        InitializingBean {
    @Resource
    private ProgramVariable programVariable;

    @Resource
    private CasServerProperties casServerProperties;

    private ServiceProperties serviceProperties;

    private String loginUrl;

    /**
     * Determines whether the Service URL should include the session id for the specific
     * user. As of CAS 3.0.5, the session id will automatically be stripped. However,
     * older versions of CAS (i.e. CAS 2), do not automatically strip the session
     * identifier (this is a bug on the part of the older server implementations), so an
     * option to disable the session encoding is provided for backwards compatibility.
     *
     * By default, encoding is enabled.
     */
    private boolean encodeServiceUrlWithSessionId = true;

    // ~ Methods
    // ========================================================================================================

    public void afterPropertiesSet() throws Exception {
        Assert.hasLength(this.loginUrl, "loginUrl must be specified");
        Assert.notNull(this.serviceProperties, "serviceProperties must be specified");
        Assert.notNull(this.serviceProperties.getService(),
                "serviceProperties.getService() cannot be null.");
    }

    public final void commence(final HttpServletRequest servletRequest,
                               final HttpServletResponse response,
                               final AuthenticationException authenticationException) throws IOException,
            ServletException {

        final String urlEncodedService = createServiceUrl(servletRequest, response);
        final String redirectUrl = createRedirectUrl(urlEncodedService);

        preCommence(servletRequest, response);

        response.sendRedirect(redirectUrl);
    }

    /**
     * Constructs a new Service Url. The default implementation relies on the CAS client
     * to do the bulk of the work.
     * @param request the HttpServletRequest
     * @param response the HttpServlet Response
     * @return the constructed service url. CANNOT be NULL.
     *  改造该方法,根据请求url动态获取服务地址
     */
    protected String createServiceUrl(final HttpServletRequest request,
                                      final HttpServletResponse response) {
        //自定义方法
        String service = getService(request);
        this.serviceProperties.setService(service);
        return CommonUtils.constructServiceUrl(null, response,
                service, null,
                this.serviceProperties.getArtifactParameter(),
                this.encodeServiceUrlWithSessionId);
    }

    /**
     * Constructs the Url for Redirection to the CAS server. Default implementation relies
     * on the CAS client to do the bulk of the work.
     *
     * @param serviceUrl the service url that should be included.
     * @return the redirect url. CANNOT be NULL.
     */
    protected String createRedirectUrl(final String serviceUrl) {
        return CommonUtils.constructRedirectUrl(this.loginUrl,
                this.serviceProperties.getServiceParameter(), serviceUrl,
                this.serviceProperties.isSendRenew(), false);
    }

    /**
     * Template method for you to do your own pre-processing before the redirect occurs.
     *
     * @param request the HttpServletRequest
     * @param response the HttpServletResponse
     */
    protected void preCommence(final HttpServletRequest request,
                               final HttpServletResponse response) {

    }

    /**
     * The enterprise-wide CAS login URL. Usually something like
     * <code>https://www.mycompany.com/cas/login</code>.
     *
     * @return the enterprise-wide CAS login URL
     */
    public final String getLoginUrl() {
        return this.loginUrl;
    }

    public final ServiceProperties getServiceProperties() {
        return this.serviceProperties;
    }

    public final void setLoginUrl(final String loginUrl) {
        this.loginUrl = loginUrl;
    }

    public final void setServiceProperties(final ServiceProperties serviceProperties) {
        this.serviceProperties = serviceProperties;
    }

    /**
     * Sets whether to encode the service url with the session id or not.
     *
     * @param encodeServiceUrlWithSessionId whether to encode the service url with the
     * session id or not.
     */
    public final void setEncodeServiceUrlWithSessionId(
            final boolean encodeServiceUrlWithSessionId) {
        this.encodeServiceUrlWithSessionId = encodeServiceUrlWithSessionId;
    }

    /**
     * Sets whether to encode the service url with the session id or not.
     * @return whether to encode the service url with the session id or not.
     *
     */
    protected boolean getEncodeServiceUrlWithSessionId() {
        return this.encodeServiceUrlWithSessionId;
    }

    /**
     * 根据请求url设置服务地址,即域名
     *
     */
    private String getService(HttpServletRequest request) {
        String url = request.getRequestURL().toString();
        if (url.contains("www.alibabagroup.com")) {
            return programVariable.getAliServiceName() + "/login";
        } else if (url.contains("www.tencent.com") ) {
            return programVariable.getTencentServerName() + "/login";
        } else {
            return casServerProperties.getServerName() + "/login";
        }
    }
}


输入账号密码后,客户端获得ticket票据,还需要到cas服务器进行验证,这里需要访问应用服务地址,也需要和一开始输入的url是同一个域名,否则会报错。我定义的类MDCasServiceTicketValidator实现了TicketValidator接口,主要是汇总了Cas20ServiceTicketValidator及其父类AbstractCasProtocolUrlBasedTicketValidator中的内容。重写validate方法,目的是动态修改validationUrl,修改的逻辑和上面描述的一样。


package com.xxx.handler.multidomain;

import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLEncoder;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import com.xxx.config.CasServerProperties;
import com.xxx.util.ProgramVariable;

import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.authentication.AttributePrincipalImpl;
import org.jasig.cas.client.proxy.Cas20ProxyRetriever;
import org.jasig.cas.client.proxy.ProxyGrantingTicketStorage;
import org.jasig.cas.client.proxy.ProxyRetriever;
import org.jasig.cas.client.ssl.HttpURLConnectionFactory;
import org.jasig.cas.client.ssl.HttpsURLConnectionFactory;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.util.XmlUtils;
import org.jasig.cas.client.validation.Assertion;
import org.jasig.cas.client.validation.AssertionImpl;
import org.jasig.cas.client.validation.TicketValidationException;
import org.jasig.cas.client.validation.TicketValidator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.xml.sax.Attributes;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.XMLReader;
import org.xml.sax.helpers.DefaultHandler;

/**
 * author:lgq
 * date:2022-1-1
 */
public class MDCasServiceTicketValidator implements TicketValidator {
    @Resource
    private ProgramVariable programVariable;

    @Resource
    private CasServerProperties casServerProperties;

    protected final Logger logger = LoggerFactory.getLogger(getClass());
    /** The CAS 2.0 protocol proxy callback url. */
    private String proxyCallbackUrl;

    /** The storage location of the proxy granting tickets. */
    private ProxyGrantingTicketStorage proxyGrantingTicketStorage;

    /** Implementation of the proxy retriever. */
    private ProxyRetriever proxyRetriever;

    /**
     * Prefix for the CAS server.   Should be everything up to the url endpoint, including the /.
     *
     * i.e. https://cas.rutgers.edu/
     */
    private final String casServerUrlPrefix;

    /**
     * URLConnection factory instance to use when making validation requests to the CAS server.
     * Defaults to {@link HttpsURLConnectionFactory}
     */
    private HttpURLConnectionFactory urlConnectionFactory = new HttpsURLConnectionFactory();

    /**
     * Whether the request include a renew or not.
     */
    private boolean renew;

    /**
     * A map containing custom parameters to pass to the validation url.
     */
    private Map<String, String> customParameters;

    private String encoding;

    /**
     * Constructs an instance of the CAS 2.0 Service Ticket Validator with the supplied
     * CAS server url prefix.
     *
     * @param casServerUrlPrefix the CAS Server URL prefix.
     */
    public MDCasServiceTicketValidator(final String casServerUrlPrefix) {
        CommonUtils.assertNotNull(casServerUrlPrefix, "casServerUrlPrefix cannot be null.");
        this.casServerUrlPrefix = CommonUtils.addTrailingSlash(casServerUrlPrefix);
        this.proxyRetriever = new Cas20ProxyRetriever(casServerUrlPrefix, getEncoding(), getURLConnectionFactory());
    }

    /**
     * Adds the pgtUrl to the list of parameters to pass to the CAS server.
     *
     * @param urlParameters the Map containing the existing parameters to send to the server.
     */
    protected final void populateUrlAttributeMap(final Map<String, String> urlParameters) {
        urlParameters.put("pgtUrl", this.proxyCallbackUrl);
    }

    protected String getUrlSuffix() {
        return "serviceValidate";
    }

    protected Assertion parseResponseFromServer(final String response) throws TicketValidationException {
        final String error = parseAuthenticationFailureFromResponse(response);

        if (CommonUtils.isNotBlank(error)) {
            throw new TicketValidationException(error);
        }

        final String principal = parsePrincipalFromResponse(response);
        final String proxyGrantingTicketIou = parseProxyGrantingTicketFromResponse(response);

        final String proxyGrantingTicket;
        if (CommonUtils.isBlank(proxyGrantingTicketIou) || this.proxyGrantingTicketStorage == null) {
            proxyGrantingTicket = null;
        } else {
            proxyGrantingTicket = this.proxyGrantingTicketStorage.retrieve(proxyGrantingTicketIou);
        }

        if (CommonUtils.isEmpty(principal)) {
            throw new TicketValidationException("No principal was found in the response from the CAS server.");
        }

        final Assertion assertion;
        final Map<String, Object> attributes = extractCustomAttributes(response);
        if (CommonUtils.isNotBlank(proxyGrantingTicket)) {
            final AttributePrincipal attributePrincipal = new AttributePrincipalImpl(principal, attributes,
                    proxyGrantingTicket, this.proxyRetriever);
            assertion = new AssertionImpl(attributePrincipal);
        } else {
            assertion = new AssertionImpl(new AttributePrincipalImpl(principal, attributes));
        }

        customParseResponse(response, assertion);

        return assertion;
    }

    protected String parseProxyGrantingTicketFromResponse(final String response) {
        return XmlUtils.getTextForElement(response, "proxyGrantingTicket");
    }

    protected String parsePrincipalFromResponse(final String response) {
        return XmlUtils.getTextForElement(response, "user");
    }

    protected String parseAuthenticationFailureFromResponse(final String response) {
        return XmlUtils.getTextForElement(response, "authenticationFailure");
    }

    /**
     * Default attribute parsing of attributes that look like the following:
     * <cas:attributes>
     *  <cas:attribute1>value</cas:attribute1>
     *  <cas:attribute2>value</cas:attribute2>
     * </cas:attributes>
     * <p>
     *
     * Attributes look like following also parsed correctly:
     * <cas:attributes><cas:attribute1>value</cas:attribute1><cas:attribute2>value<
     * /cas:attribute2></cas:attributes>
     * <p>
     *
     * This code is here merely for sample/demonstration purposes for those wishing to modify the CAS2 protocol.  You'll
     * probably want a more robust implementation or to use SAML 1.1
     *
     * @param xml the XML to parse.
     * @return the map of attributes.
     */
    protected Map<String, Object> extractCustomAttributes(final String xml) {
        final SAXParserFactory spf = SAXParserFactory.newInstance();
        spf.setNamespaceAware(true);
        spf.setValidating(false);
        try {
            final SAXParser saxParser = spf.newSAXParser();
            final XMLReader xmlReader = saxParser.getXMLReader();
            final MDCasServiceTicketValidator.CustomAttributeHandler handler = new MDCasServiceTicketValidator.CustomAttributeHandler();
            xmlReader.setContentHandler(handler);
            xmlReader.parse(new InputSource(new StringReader(xml)));
            return handler.getAttributes();
        } catch (final Exception e) {
            logger.error(e.getMessage(), e);
            return Collections.emptyMap();
        }
    }

    /**
     * Template method if additional custom parsing (such as Proxying) needs to be done.
     *
     * @param response the original response from the CAS server.
     * @param assertion the partially constructed assertion.
     * @throws TicketValidationException if there is a problem constructing the Assertion.
     */
    protected void customParseResponse(final String response, final Assertion assertion)
            throws TicketValidationException {
        // nothing to do
    }

    public final void setProxyCallbackUrl(final String proxyCallbackUrl) {
        this.proxyCallbackUrl = proxyCallbackUrl;
    }

    public final void setProxyGrantingTicketStorage(final ProxyGrantingTicketStorage proxyGrantingTicketStorage) {
        this.proxyGrantingTicketStorage = proxyGrantingTicketStorage;
    }

    public final void setProxyRetriever(final ProxyRetriever proxyRetriever) {
        this.proxyRetriever = proxyRetriever;
    }

    protected final String getProxyCallbackUrl() {
        return this.proxyCallbackUrl;
    }

    protected final ProxyGrantingTicketStorage getProxyGrantingTicketStorage() {
        return this.proxyGrantingTicketStorage;
    }

    protected final ProxyRetriever getProxyRetriever() {
        return this.proxyRetriever;
    }

    /**
     *
     */
    private class CustomAttributeHandler extends DefaultHandler {

        private Map<String, Object> attributes;

        private boolean foundAttributes;

        private String currentAttribute;

        private StringBuilder value;

        @Override
        public void startDocument() throws SAXException {
            this.attributes = new HashMap<String, Object>();
        }

        @Override
        public void startElement(final String namespaceURI, final String localName, final String qName,
                                 final Attributes attributes) throws SAXException {
            if ("attributes".equals(localName)) {
                this.foundAttributes = true;
            } else if (this.foundAttributes) {
                this.value = new StringBuilder();
                this.currentAttribute = localName;
            }
        }

        @Override
        public void characters(final char[] chars, final int start, final int length) throws SAXException {
            if (this.currentAttribute != null) {
                value.append(chars, start, length);
            }
        }

        @Override
        public void endElement(final String namespaceURI, final String localName, final String qName)
                throws SAXException {
            if ("attributes".equals(localName)) {
                this.foundAttributes = false;
                this.currentAttribute = null;
            } else if (this.foundAttributes) {
                final Object o = this.attributes.get(this.currentAttribute);

                if (o == null) {
                    this.attributes.put(this.currentAttribute, this.value.toString());
                } else {
                    final List<Object> items;
                    if (o instanceof List) {
                        items = (List<Object>) o;
                    } else {
                        items = new LinkedList<Object>();
                        items.add(o);
                        this.attributes.put(this.currentAttribute, items);
                    }
                    items.add(this.value.toString());
                }
            }
        }

        public Map<String, Object> getAttributes() {
            return this.attributes;
        }
    }

    protected final String getEncoding() {
        return this.encoding;
    }

    protected HttpURLConnectionFactory getURLConnectionFactory() {
        return this.urlConnectionFactory;
    }

    /**
     * Constructs the URL to send the validation request to.
     *
     * @param ticket the ticket to be validated.
     * @param serviceUrl the service identifier.
     * @return the fully constructed URL.
     */
    protected final String constructValidationUrl(final String ticket, final String serviceUrl) {
        final Map<String, String> urlParameters = new HashMap<String, String>();

        logger.debug("Placing URL parameters in map.");
        urlParameters.put("ticket", ticket);
        urlParameters.put("service", serviceUrl);

        if (this.renew) {
            urlParameters.put("renew", "true");
        }

        logger.debug("Calling template URL attribute map.");
        populateUrlAttributeMap(urlParameters);

        logger.debug("Loading custom parameters from configuration.");
        if (this.customParameters != null) {
            urlParameters.putAll(this.customParameters);
        }

        final String suffix = getUrlSuffix();
        final StringBuilder buffer = new StringBuilder(urlParameters.size() * 10 + this.casServerUrlPrefix.length()
                + suffix.length() + 1);

        int i = 0;

        buffer.append(this.casServerUrlPrefix);
        buffer.append(suffix);

        for (Map.Entry<String, String> entry : urlParameters.entrySet()) {
            final String key = entry.getKey();
            final String value = entry.getValue();

            if (value != null) {
                buffer.append(i++ == 0 ? "?" : "&");
                buffer.append(key);
                buffer.append("=");
                final String encodedValue = encodeUrl(value);
                buffer.append(encodedValue);
            }
        }

        return buffer.toString();

    }

    /**
     * Encodes a URL using the URLEncoder format.
     *
     * @param url the url to encode.
     * @return the encoded url, or the original url if "UTF-8" character encoding could not be found.
     */
    protected final String encodeUrl(final String url) {
        if (url == null) {
            return null;
        }

        try {
            return URLEncoder.encode(url, "UTF-8");
        } catch (final UnsupportedEncodingException e) {
            return url;
        }
    }

    /**
     * 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());
    }

    /**
     * Contacts the CAS Server to retrieve the response for the ticket validation.
     *
     * @param ticket the ticket to validate.
     * @return the response from the CAS server.
     * 改造方法,动态获取validationUrl
     */

    public final Assertion validate(final String ticket, final String service) throws TicketValidationException {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        String url = request.getRequestURL().toString();
        String serviceUrl;
        if (url.contains("www.alibabagroup.com")) {
            serviceUrl = programVariable.getAliServiceName() + "/login";
        } else if (url.contains("www.tencent.com") ) {
            serviceUrl = programVariable.getTencentServerName() + "/login";
        } else {
            serviceUrl = casServerProperties.getServerName() + "/login";
        }
        final String validationUrl = constructValidationUrl(ticket, serviceUrl);
        logger.debug("Constructing validation url: {}", validationUrl);

        try {
            logger.debug("Retrieving response from server.");
            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);
        }
    }

    public final void setRenew(final boolean renew) {
        this.renew = renew;
    }

    public final void setCustomParameters(final Map<String, String> customParameters) {
        this.customParameters = customParameters;
    }

    public final void setEncoding(final String encoding) {
        this.encoding = encoding;
    }

    protected final boolean isRenew() {
        return this.renew;
    }

    protected final String getCasServerUrlPrefix() {
        return this.casServerUrlPrefix;
    }

    protected final Map<String, String> getCustomParameters() {
        return this.customParameters;
    }

    public void setURLConnectionFactory(final HttpURLConnectionFactory urlConnectionFactory) {
        this.urlConnectionFactory = urlConnectionFactory;
    }
}

最后是登出的处理,登出完成后再登录还需要访问之前的域名,因此需要动态获取登出后重定向的地址。自定义类MDSimpleUrlLogoutSuccessHandler模仿SimpleUrlLogoutSuccessHandler类(默认类),只有一个方法,即重写onLogoutSuccess方法。动态获取地址的逻辑和上面一样。

package com.xxx.handler.multidomain;

import java.io.IOException;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import com.xxx.util.ProgramVariable;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AbstractAuthenticationTargetUrlRequestHandler;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;

/**
 * author:lgq
 * date:2022-1-1
 */
public class MDSimpleUrlLogoutSuccessHandler extends
        AbstractAuthenticationTargetUrlRequestHandler implements LogoutSuccessHandler {
    @Resource
    private ProgramVariable programVariable;

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
                                Authentication authentication) throws IOException {
        String targetUrl;
        String url = request.getRequestURL().toString();
        if (url.contains("www.alibabagroup.com")) {
            targetUrl = programVariable.getAliServiceName() + "/login";
        } else if (url.contains("www.tencent.com") ) {
            targetUrl = programVariable.getTencentServerName() + "/login";
        } else {
            targetUrl = casServerProperties.getServerName() + "/login";
        }
        if (response.isCommitted()) {
            logger.debug("Response has already been committed. Unable to redirect to "
                    + targetUrl);
            return;
        }

        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }
}