之前做的项目要迭代多租户功能,不同租户对应同一个数据库的多个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
最后是登出的处理,登出完成后再登录还需要访问之前的域名,因此需要动态获取登出后重定向的地址。自定义类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);
}
}



