第1章 背景
SaCa Dataviz 是一个自助式可视化分析工具,而当应用系统中需要使用本产品时,常常会面临与已有系统集成整合。SaCa Dataviz推荐的集成方式是与已有应用做单点登录(SSO)集成。本文将介绍如何在SaCa DataViz中进行修改,以与已搭建好的CAS(Central Authentication Service) 服务进行集成。
本文中所对应的CAS版本为4.1,其他版本除较早版本外方法应大体相同。推荐使用较新版本进行集成。
本文建议 5.7.03 及其之前版本使用,5.7.03 之后的版本应按 新版CAS集成文档 进行集成。
第2章 集成
本文不涉及CAS端的搭建或定制化内容。在使用本文中的方法进行集成前,请先确保CAS服务已正确搭建,已有系统已可以正常使用CAS完成登录、登出。如果对CAS不了解,可以参考 《Apereo CAS 单点登录系统介绍》 .
2.1 思路
SaCa DataViz采用前后端分离架构,前端为纯HTML,后端为Java Web应用。CAS集成针对的是后端Java端与CAS的集成,前端作为纯展现端可以独立运行,也可以直接集成到应用系统内。
后端集成主要考虑两个点,一是登录流程的处理,二是登录用户集成。SaCa DataViz 后端使用Spring Security 5.5 版本进行身份验证,两者的处理均基于Spring Security完成。
除后端外,前台有一些配置需要修改,以适应登录跳转。
集成过程需要基于DataViz后端进行少量代码开发。后续配置除个别点外,均依照Spring Security 5.5与CAS集成的一般方法配置,如果配置后某些效果不满足需求,可以在了解集成方法后自行修改实现。
2.2 准备
将 spring-security-cas-5.5.8.jar, cas-client-core-3.6.4.jar 放入后端应用WEB-INF/lib 下。
按照之前添加已有系统的方法,将 DataViz 后台的访问地址加入到 CAS 服务授权service中。如:http://192.168.3.1:8080/dataviz-service.
2.3 登录流程处理
编写集成实现类
编写一个实现类,实现
org.springframework.security.core.userdetails.UserDetailsService
中的方法,该方法接收一个username
(用户唯一标识,由CAS认证成功后返回),返回一个包含ROLE_USER
权限的com.neusoft.saca.dataviz.common.security.UserDetailExt
对象,该接口继承了org.springframework.security.core.userdetails.UserDetails
接口。(如果需要使用公共项目管理员功能,需要对管理员用户额外添加ROLE_ADMIN
权限)将该类打包放入WEB-INF/lib 下。示例代码如下(可直接使用):
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import com.neusoft.saca.dataviz.common.security.DefaultUser;
public class SampleUserDetailService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) {
// 根据username或去用户信息,可以根据当前用户的信息判断是否为管理员,如果是管理员,userId为用户ID
// return new DefaultUser(userId, "",new //SimpleGrantedAuthority("ROLE_ADMIN"));
return new DefaultUser(userId, “”);
}
}
添加新的AuthenticationEntryPoint
实现类,代码如下:
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
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 CasAuthenticationEntryPointWithAjaxSupport implements
AuthenticationEntryPoint, InitializingBean {
// ~ Instance fields
// ================================================================================================
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.");
}
@Override
public final void commence(final HttpServletRequest servletRequest,
final HttpServletResponse response,
final AuthenticationException authenticationException) throws IOException,
ServletException {
if (isAjaxRequest(servletRequest)) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
} else {
final String urlEncodedService = createServiceUrl(servletRequest, response);
final String redirectUrl = createRedirectUrl(urlEncodedService);
preCommence(servletRequest, response);
response.sendRedirect(redirectUrl);
}
}
protected boolean isAjaxRequest(final HttpServletRequest servletRequest) {
String xRequestedWith = servletRequest.getHeader("X-Requested-With");
return xRequestedWith != null && xRequestedWith.trim().length() > 0;
}
/**
* 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.
*/
protected String createServiceUrl(final HttpServletRequest request,
final HttpServletResponse response) {
return CommonUtils.constructServiceUrl(null, response,
this.serviceProperties.getService(), 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;
}
}
修改 applicationContext-security.xml
该文件位于 WEB-INF/conf/spring/applicationContext-security.xml,包含了全部Spring Security登录验证等相关内容的配置。
删除以下相关内容:
<http use-expressions="true" entry-point-ref="authenticationEntryPoint"> … </http>
<beans:bean id="formLoginFilter" class=…> … </beans:bean>
<beans:bean id="sessionManagementFilter" class=…> … </beans:bean>
<beans:bean id="invalidSessionStrategy" class=…> … </beans:bean>
<beans:bean id="logoutSuccessHandler" class=…> … </beans:bean>
<beans:bean id="authenticationEntryPoint" class=…> … </beans:bean>
<custom-filter position="SESSION_MANAGEMENT_FILTER" ref="sessionManagementFilter"></custom-filter>
增加以下内容,并将其中的
CasAuthenticationEntryPointWithAjaxSupport
以及casUserDetailService
替换为之前添加的实现类:
<beans:bean id="casPropertyConfigurer"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<beans:property name="ignoreUnresolvablePlaceholders" value="true" />
<beans:property name="locations">
<beans:list>
<beans:value>WEB-INF/conf/cas.properties</beans:value>
</beans:list>
</beans:property>
</beans:bean>
<http use-expressions="true" entry-point-ref="casEntryPoint">
<!-- 禁用CSRF Protection,否则无法执行POST请求,会报403错误 -->
<csrf disabled="true" />
<!-- 对所有资源,都必须要有USER角色 -->
<intercept-url pattern="/**" access="hasRole('USER')" />
<!-- 退出配置,如需删除cookie可添加:delete-cookies="JSESSIONID"-->
<logout logout-success-url="/cas-logout.jsp"/>
<custom-filter after="FORM_LOGIN_FILTER" ref="internalLoginFilter" />
<custom-filter position="CAS_FILTER" ref="casFilter" />
<custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/>
<custom-filter ref="singleLogoutFilter" before="CAS_FILTER"/>
<access-denied-handler ref="accessDeniedHandler"></access-denied-handler>
</http>
<beans:bean id="serviceProperties" class="org.springframework.security.cas.ServiceProperties">
<beans:property name="service" value="${sso.cas.localServer}${sso.cas.localServicePath}" />
<beans:property name="sendRenew" value="false" />
</beans:bean>
<beans:bean id="casFilter" class="org.springframework.security.cas.web.CasAuthenticationFilter">
<beans:property name="authenticationManager" ref="authenticationManager" />
<beans:property name="authenticationSuccessHandler">
<beans:bean
class=" org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler">
<beans:property name="defaultTargetUrl" value="${sso.cas.defaultTargetRedirect}" />
<beans:property name="targetUrlParameter" value="target" />
<beans:property name="alwaysUseDefaultTargetUrl" value="true" />
</beans:bean>
</beans:property>
</beans:bean>
<beans:bean id="casEntryPoint"
class="CasAuthenticationEntryPointWithAjaxSupport 完整类名">
<beans:property name="loginUrl" value="${sso.cas.casServer}/login" />
<beans:property name="serviceProperties" ref="serviceProperties" />
</beans:bean>
<beans:bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
<beans:property name="authenticationUserDetailsService">
<beans:bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
<beans:constructor-arg ref="casUserDetailService" />
</beans:bean>
</beans:property>
<beans:property name="serviceProperties" ref="serviceProperties" />
<beans:property name="ticketValidator">
<beans:bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
<!-- server URL prefix -->
<beans:constructor-arg index="0" value="${sso.cas.casServerLocal}" />
<beans:property name="encoding" value="UTF-8" />
</beans:bean>
</beans:property>
<beans:property name="key" value="DataViz" />
</beans:bean>
<!-- This filter handles a Single Logout Request from the CAS Server -->
<beans:bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter">
</beans:bean>
<!-- This filter redirects to the CAS Server to signal Single Logout should be performed -->
<beans:bean id="requestSingleLogoutFilter"
class="org.springframework.security.web.authentication.logout.LogoutFilter">
<beans:constructor-arg value="${sso.cas.casServer}/logout?service=${sso.cas.localServer}"/>
<beans:constructor-arg>
<beans:bean class="org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
</beans:constructor-arg>
<beans:property name="filterProcessesUrl" value="${sso.cas.localLogoutPath}"/>
</beans:bean>
<beans:bean id="casUserDetailService" class="UserDetails完整实现类名" />
查找 <authentication-manager alias="authenticationManager">
标签,将以下内容添加到该标签内:
<authentication-provider ref="casAuthenticationProvider"/>
检查其中其他对authenticationEntryPoint
bean的引用,将 authenticationEntryPoint
改为 casEntryPoint
。
- 添加cas.properties
于 WEB-INF/conf 下,添加 cas.properties 文件。参考以下内容。
# cas 服务访问地址
sso.cas.casServer=https://sso.local-example.org:8443/cas
# cas 服务内部地址,用于内部验证通信,当仅在内网使用或没有内外网隔离时可与外部访问地址相同
sso.cas.casServerLocal=https://sso.local-example.org:8443/cas
# DataViz 后台应用访问地址
sso.cas.localServer=http://www.local-example.org:8080/dataviz-service
# DataViz 登录地址,不用修改
sso.cas.localServicePath=/login/cas
# DataViz 登出地址,不用修改
sso.cas.localLogoutPath=/logout/cas
# DataViz 前台访问地址
sso.cas.defaultTargetRedirect=http://www.local-example.org:8080/dataviz/src/index.html
- 修改 /WEB-INF/web.xml,添加如下内容
<listener>
<listener-class>
org.jasig.cas.client.session.SingleSignOutHttpSessionListener
</listener-class>
</listener>
2.3.1 报表集成版本
报表集成版本相较一般DataViz中额外增加了一些内容,如果集成时需要不通过DataViz界面或图册直接查看某张报表,则额外需要一些配置上的修改。
2.3.1.1 AuthenticationSuccessHandler
将该实现类添加到应用中:
import java.io.IOException;
import java.util.regex.Pattern;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.security.web.savedrequest.HttpSessionRequestCache;
import org.springframework.security.web.savedrequest.RequestCache;
import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.util.StringUtils;
public class SavedRequestPartialAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
protected final Log logger = LogFactory.getLog(this.getClass());
private RequestCache requestCache = new HttpSessionRequestCache();
private Pattern awaredUrlPattern;
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws ServletException, IOException {
SavedRequest savedRequest = requestCache.getRequest(request, response);
if (savedRequest == null) {
super.onAuthenticationSuccess(request, response, authentication);
return;
}
String targetUrlParameter = getTargetUrlParameter();
String targetUrl = savedRequest.getRedirectUrl();
logger.debug("Saved target Url is " + targetUrl);
if (isAlwaysUseDefaultTargetUrl() || (awaredUrlPattern != null &&
!awaredUrlPattern.matcher(targetUrl).matches()) ||
(targetUrlParameter != null && StringUtils.hasText(request
.getParameter(targetUrlParameter)))) {
requestCache.removeRequest(request, response);
super.onAuthenticationSuccess(request, response, authentication);
return;
}
clearAuthenticationAttributes(request);
// Use the DefaultSavedRequest URL
logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
getRedirectStrategy().sendRedirect(request, response, targetUrl);
}
public void setRequestCache(RequestCache requestCache) {
this.requestCache = requestCache;
}
public void setAwaredUrlPattern(String awaredUrlPattern) {
if (awaredUrlPattern != null) {
this.awaredUrlPattern = Pattern.compile(awaredUrlPattern, Pattern.COMMENTS);
}
}
}
2.3.1.2 applicationContext-security.xml
- 查找
<http pattern="(?x: /Report-PreviewAction\.do | /Report-.*\.do | /unieap/pages/report/.* | /components/widget/.*)(?:\?.+)?"
部分,在标签内添加以下内容:
<intercept-url pattern="/**" access="hasRole('USER')"/>
<logout logout-success-url="/cas-logout.jsp"/>
<custom-filter position="CAS_FILTER" ref="casFilter"/>
<custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/>
<custom-filter ref="singleLogoutFilter" before="CAS_FILTER"/>
- 查找
<beans:bean id="casFilter"
,将标签内容改为以下内容,并将 AuthenticationSuccessHandler 实现类 替换为上述实现的完整类路径:
<beans:property name="authenticationManager" ref="authenticationManager"/>
<beans:property name="authenticationSuccessHandler">
<beans:bean class="AuthenticationSuccessHandler 实现类">
<beans:property name="defaultTargetUrl" value="${sso.cas.defaultTargetRedirect}"/>
<beans:property name="targetUrlParameter" value="target"/>
<beans:property name="alwaysUseDefaultTargetUrl" value="false"/>
<beans:property name="awaredUrlPattern" value=".*/Report-.*\.do.*"/>
</beans:bean>
</beans:property>
2.4 登录用户集成
CAS集成属于整个系统集成中的登录集成部分,用户在登陆成功之后,前台会获取当前的登录用户的详细信息,系统后台会根据当前session中的用户ID获取当前登录用户的详细信息,由于用户不存在于DataViz本身的支撑库中,所以在CAS集成后还需要进行用户的集成。 用户集成在DataViz中主要是用作权限管理等功能,这些功能需要实现用户管理的接口。在登录集成中略过该步骤不会影响DataViz核心功能使用。详细内容可参见 《系统集成》中的 《第4章 用户集成》。
2.5 前台登录跳转
修改前台 /common/config.js 文件,修改以下变量值:
// cas登录地址
window.cas_server = "https://sso.local-example.org:8443/cas/login";
// cas服务回调地址
window.cas_callback_server = "?service=http://www.local-example.org:8080/dataviz-service/login/cas";
// 是否使用cas登录
window.isCasLogin = true;