栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

多租户多域名访问同一个服务实现

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

多租户多域名访问同一个服务实现

之前做的项目要迭代多租户功能,不同租户对应同一个数据库的多个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;


@Configuration
@EnableConfigurationProperties(value = {com.xxx.config.CasServerProperties.class})
public class SecurityConfiguration {

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

    @Resource
    private AuthenticationUserDetailsService userDetailsService;

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

    
    @Bean
    public ServiceProperties serviceProperties() {
        ServiceProperties serviceProperties = new ServiceProperties();
        //设置默认的cas登陆后回跳地址
        serviceProperties.setService(casServerProperties.getServerName() + "/login");
        //设置我们应用是否敏感
        serviceProperties.setSendRenew(false);
        //设置是否对未拥有ticket的访问均需要验证
        serviceProperties.setAuthenticateAllArtifacts(true);
        return serviceProperties;
    }

    
    @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;
    }

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

    
    @Bean
    public MDCasServiceTicketValidator cas20ServiceTicketValidator() {
        //需要设置cas server的前缀,也就是根路径
        return new MDCasServiceTicketValidator(casServerProperties.getCasServerUrlPrefix());
    }

    
    @Bean("casProvider")
    public CasAuthenticationProvider casAuthenticationProvider(AuthenticationUserDetailsService
                                                                           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;


public class MDCasAuthenticationEntryPoint implements AuthenticationEntryPoint,
        InitializingBean {
    @Resource
    private ProgramVariable programVariable;

    @Resource
    private CasServerProperties casServerProperties;

    private ServiceProperties serviceProperties;

    private String loginUrl;

    
    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);
    }

    
    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);
    }

    
    protected String createRedirectUrl(final String serviceUrl) {
        return CommonUtils.constructRedirectUrl(this.loginUrl,
                this.serviceProperties.getServiceParameter(), serviceUrl,
                this.serviceProperties.isSendRenew(), false);
    }

    
    protected void preCommence(final HttpServletRequest request,
                               final HttpServletResponse response) {

    }

    
    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;
    }

    
    public final void setEncodeServiceUrlWithSessionId(
            final boolean encodeServiceUrlWithSessionId) {
        this.encodeServiceUrlWithSessionId = encodeServiceUrlWithSessionId;
    }

    
    protected boolean getEncodeServiceUrlWithSessionId() {
        return this.encodeServiceUrlWithSessionId;
    }

    
    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;


public class MDCasServiceTicketValidator implements TicketValidator {
    @Resource
    private ProgramVariable programVariable;

    @Resource
    private CasServerProperties casServerProperties;

    protected final Logger logger = LoggerFactory.getLogger(getClass());
    
    private String proxyCallbackUrl;

    
    private ProxyGrantingTicketStorage proxyGrantingTicketStorage;

    
    private ProxyRetriever proxyRetriever;

    
    private final String casServerUrlPrefix;

    
    private HttpURLConnectionFactory urlConnectionFactory = new HttpsURLConnectionFactory();

    
    private boolean renew;

    
    private Map customParameters;

    private String encoding;

    
    public MDCasServiceTicketValidator(final String casServerUrlPrefix) {
        CommonUtils.assertNotNull(casServerUrlPrefix, "casServerUrlPrefix cannot be null.");
        this.casServerUrlPrefix = CommonUtils.addTrailingSlash(casServerUrlPrefix);
        this.proxyRetriever = new Cas20ProxyRetriever(casServerUrlPrefix, getEncoding(), getURLConnectionFactory());
    }

    
    protected final void populateUrlAttributeMap(final Map 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 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");
    }

    
    protected Map 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();
        }
    }

    
    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 attributes;

        private boolean foundAttributes;

        private String currentAttribute;

        private StringBuilder value;

        @Override
        public void startdocument() throws SAXException {
            this.attributes = new HashMap();
        }

        @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 items;
                    if (o instanceof List) {
                        items = (List) o;
                    } else {
                        items = new linkedList();
                        items.add(o);
                        this.attributes.put(this.currentAttribute, items);
                    }
                    items.add(this.value.toString());
                }
            }
        }

        public Map getAttributes() {
            return this.attributes;
        }
    }

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

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

    
    protected final String constructValidationUrl(final String ticket, final String serviceUrl) {
        final Map urlParameters = new HashMap();

        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 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();

    }

    
    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;
        }
    }

    
    protected final String retrieveResponseFromServer(final URL validationUrl, final String ticket) {
        return CommonUtils.getResponseFromServer(validationUrl, getURLConnectionFactory(), getEncoding());
    }

    

    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 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 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;


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);
    }
}

转载请注明:文章转载自 www.mshxw.com
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号