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

results matching ""

    No results matching ""

    results matching ""

      No results matching ""