SpringMVC对接CAS客户端实现单点登录手册

时间:2022-07-28
本文章向大家介绍SpringMVC对接CAS客户端实现单点登录手册,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

业务场景:之前写过CAS服务端的例子,也对接过基于SpringBoot的CAS,不过最近项目要对接第三方的CAS实现单点登录,而我们项目是基于SpringMVC的,所以就摸索了一下对接方案,其它博客可以参考我之前专栏:CAS单点登录系列博客

pom加上cas配置:

<properties>
	<cas.client.version>3.4.1</cas.client.version>
</properties>
<dependency>
	<groupId>org.jasig.cas.client</groupId>
	<artifactId>cas-client-core</artifactId>
	<version>${cas.client.version}</version>
	<scope>compile</scope>
</dependency>

web.xml加上配置

<!-- CAS单点登录配置-->
<!-- 单点登出监听器,用于监听单点登出session情况 -->
	<listener>
		<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
	</listener>
	<!-- 该过滤器用于实现单点登出功能 -->
	<filter>
		<filter-name>CAS Single Sign Out Filter</filter-name>
		<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
		<init-param>
			<param-name>casServerUrlPrefix</param-name>
			<param-value>http://127.0.0.1:8080/CAS</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>CAS Single Sign Out Filter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	<!-- 该过滤器用于实现单点登录功能 -->
	<filter>
		<filter-name>CAS Filter</filter-name>
		<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
		<init-param>
			<param-name>casServerLoginUrl</param-name>
			<param-value>http://127.0.0.1:8080/CAS/login</param-value>
		</init-param>
		<init-param>
			<param-name>serverName</param-name>
			<param-value>http://127.0.0.1:8081</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>CAS Filter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	<!-- 该过滤器负责对Ticket的校验工作 -->
	<filter>
		<filter-name>CAS Validation Filter</filter-name>
		<filter-class>org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter</filter-class>
		<init-param>
			<param-name>casServerUrlPrefix</param-name>
			<param-value>http://127.0.0.1:8080/CAS</param-value>
		</init-param>
		<init-param>
			<param-name>serverName</param-name>
			<param-value>http://127.0.0.1:8081</param-value>
		</init-param>
		<init-param>
			<param-name>redirectAfterValidation</param-name>
			<param-value>true</param-value>
		</init-param>
		<init-param>
			<param-name>useSession</param-name>
			<param-value>true</param-value>
		</init-param>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>CAS Validation Filter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	<!-- 该过滤器负责实现HttpServletRequest请求的包裹, 比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。 -->
	<filter>
		<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
		<filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	<!-- 该过滤器使得开发者可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。-->
	<filter>
		<filter-name>CAS Assertion Thread Local Filter</filter-name>
		<filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
	</filter>
	<filter-mapping>
		<filter-name>CAS Assertion Thread Local Filter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	<!--// CAS单点登录配置 -->

配置好之后,通过AssertionHolder获取登录的用户账号,AssertionThreadLocalFilter过滤器需要配置才能获取,改过滤器通过ThreadLocal保存信息

protected String getCasLoginUser(){
		Assertion assertion = AssertionHolder.getAssertion();
		String userName = "";
		if (assertion != null) {
			userName = assertion.getPrincipal().getName();
			logger.info("userName:"+userName);
		}
		return userName;
	}

基于上面的简单配置基本就能实现cas单点登录和登出,不过配置ip和端口都写在web.xml,以后ip改了,又要翻配置改动,所以可以基于3.0的Servlet api实现动态配置,将配置放在properties

<dependency>
	<groupId>javax.servlet</groupId>
	<artifactId>javax.servlet-api</artifactId>
	<scope>provided</scope>
	<version>3.1.0</version>
</dependency>

实现AbstractAnnotationConfigDispatcherServletInitializer类:

package com.extra.login.core.servlet;

import com.extra.login.cas.client.filter.authentication.AuthenticationFilter;
import com.extra.login.cas.client.filter.session.SingleSignOutFilter;
import com.extra.login.cas.client.util.AssertionThreadLocalFilter;
import com.extra.login.cas.client.util.CasParamKeyEnum;
import com.extra.login.cas.client.util.CasPropertiesLoader;
import com.extra.login.cas.client.util.HttpServletRequestWrapperFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

import javax.servlet.*;
import java.util.EnumSet;


/**
 * <pre>
 *      基于Servlet3.0实现动态配置过滤器、监听器、Servlet
 * </pre>
 *
 * <pre>
 * @author mazq
 * 修改记录
 *    修改后版本:     修改人:  修改日期: 2020/08/27 14:33  修改内容:
 * </pre>
 */
@Component
public class WebServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer  {


    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[0];
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[0];
    }

    @Override
    protected String[] getServletMappings() {
        return new String[0];
    }

    @Override
    public void onStartup(ServletContext servletContext) throws ServletException {
        this.registerCasFilter(servletContext);
        this.registerCasListener(servletContext);
        super.onStartup(servletContext);
    }

    /**
     *  动态注册CAS过滤器
     * @Author mazq
     * @Date 2020/08/27 16:41
     * @Param [servletContext]
     * @return void
     */
    protected void registerCasFilter(ServletContext servletContext) {
        /* CAS单点登录校验过滤器 */
        FilterRegistration casFilter = servletContext.addFilter("casFilter", AuthenticationFilter.class);
        casFilter.setInitParameter("casServerLoginUrl" , CasPropertiesLoader.getValue(CasParamKeyEnum.CAS_SERVER_HOST_LOGIN_URL.getCasParamKey()));
        casFilter.setInitParameter("serverName" , CasPropertiesLoader.getValue(CasParamKeyEnum.APP_SERVER_HOST_URL.getCasParamKey()));
        casFilter.setInitParameter("ignorePattern" , "/static/*");
        casFilter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class) , true, "/*");
        /* CAS单点登录ticket校验过滤器 */
        FilterRegistration casValidationFilter = servletContext.addFilter("casValidationFilter", Cas30ProxyReceivingTicketValidationFilter.class);
        casValidationFilter.setInitParameter("casServerUrlPrefix" , CasPropertiesLoader.getValue(CasParamKeyEnum.CAS_SERVER_HOST_URL.getCasParamKey()));
        casValidationFilter.setInitParameter("serverName" , CasPropertiesLoader.getValue(CasParamKeyEnum.APP_SERVER_HOST_URL.getCasParamKey()));
        casValidationFilter.setInitParameter("redirectAfterValidation" , "true");
        casValidationFilter.setInitParameter("useSession" , "true");
        casValidationFilter.setInitParameter("encoding" , "UTF-8");
        casValidationFilter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class) , true, "/*");
        /* CAS单点登出过滤器 */
        FilterRegistration singleSignOutFilter = servletContext.addFilter("singleSignOutFilter", SingleSignOutFilter.class);
        singleSignOutFilter.setInitParameter("casServerUrlPrefix" , CasPropertiesLoader.getValue(CasParamKeyEnum.CAS_SERVER_HOST_URL.getCasParamKey()));
        singleSignOutFilter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class) , true, "/*");
        /* HttpServletRequestWrapper过滤器 */
        FilterRegistration httpServletRequestWrapperFilter = servletContext.addFilter("httpServletRequestWrapperFilter", HttpServletRequestWrapperFilter.class);
        httpServletRequestWrapperFilter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class) , true, "/*");
        /* AssertionThreadLocal过滤器 */
        FilterRegistration assertionThreadLocalFilter = servletContext.addFilter("assertionThreadLocalFilter", AssertionThreadLocalFilter.class);
        assertionThreadLocalFilter.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class) , true, "/*");
    }

    /**
     * 注册CAS监听器
     * @Author mazq
     * @Date 2020/08/27 16:43
     * @Param [servletContext]
     * @return void
     */
    protected void registerCasListener(ServletContext servletContext){
        //注册监听器
        servletContext.addListener(SingleSignOutHttpSessionListener.class);
    }

    @Override
    protected FilterRegistration.Dynamic registerServletFilter(ServletContext servletContext, Filter filter) {
        return super.registerServletFilter(servletContext, filter);
    }

    @Override
    protected void registerContextLoaderListener(ServletContext servletContext) {
        super.registerContextLoaderListener(servletContext);
    }
}

不过在一些原有就有自己的登录机制的系统,这里可以将主要的过滤器代码拿来改动,加上开关

public static boolean isCasLoginMode() {
        WebConfigService webConfigService = (WebConfigService) ApplicationContextHolder.getApplicationContext().getBean("webConfigService");
        boolean isCasMode = webConfigService.getFlag("${casLogin_Boolean}");
        if (isCasMode) {
            return true;
        }
        return false;
    }

获取Spring上下文工具类,filter里不能直接用@Autowired自动装载

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;

/**
 * 获取Spring上下文
 */
@Service
public class ApplicationContextHolder implements ApplicationContextAware {
	
	private static ApplicationContext ctx;

	@Override
	public void setApplicationContext(ApplicationContext applicationContext)
			throws BeansException {
		ctx = applicationContext;
	}

	public static ApplicationContext getApplicationContext() {
		return ctx;
	}
	
	@SuppressWarnings("unchecked")
	public static <T> T getBean(String beanName) {
		return (T) ctx.getBean(beanName);
	}
	
	public static <T> T getBean(Class<T> clazz) {
		return ctx.getBean(clazz);
	}
}

源码里的init配置也可以加上开关

cas配置写在cas.properties里,写个工具类进行读取:

# 是否启动CAS服务
security.cas.enabled=true
# CAS服务地址
security.cas.server.host.url=http://cas.server.org:6342/CAS
# CAS服务登录地址
security.cas.server.host.login_url=http://cas.server.org:6342/CAS/login
# CAS服务登出地址
security.cas.server.host.logout_url=http://cas.server.org:6342/CAS/logout?service=http://192.168.9.30:8081
# 应用访问地址
security.app.server.host.url=http://192.168.9.30:8081

CAS参数枚举类

package com.extra.login.cas.client.util;

import com.common.utils.config.CasPropertiesLoader;

/**
 * <pre>
 *      CAS配置参数
 * </pre>
 *
 * <pre>
 * @author mazq
 * 修改记录
 *    修改后版本:     修改人:  修改日期: 2020/08/27 15:46  修改内容:
 * </pre>
 */
public enum CasParamKeyEnum {
    // CAS服务地址
    CAS_SERVER_HOST_URL("security.cas.server.host.url"),
    // CAS服务登录地址
    CAS_SERVER_HOST_LOGIN_URL("security.cas.server.host.login_url"),
    // CAS服务登出地址
    CAS_SERVER_HOST_LOGOUT_URL("security.cas.server.host.logout_ur"),
    // 应用访问地址
    APP_SERVER_HOST_URL("security.app.server.host.url"),
    // CAS 标识
    CAS_SIGN("cas");

    private String casParamKey;

    CasParamKeyEnum(String casParamKey) {
        this.casParamKey = casParamKey;
    }

    public String getCasParamKey() {
        return casParamKey;
    }

    public void setCasParamKey(String casParamKey) {
        this.casParamKey = casParamKey;
    }

    public static void main(String[] args ){
        System.out.println(CasPropertiesLoader.getValue("security.cas.server.host.url"));
    }
}

cas.properties读取工具类:

package com.extra.login.cas.client.util;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.context.annotation.Conditional;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.core.type.AnnotationMetadata;
import org.springframework.core.type.MethodMetadata;
import org.springframework.stereotype.Component;

import java.util.Properties;

/**
 * <pre>
 *      CAS配置属性加载类
 * </pre>
 *
 * <pre>
 * @author mazq
 * 修改记录
 *    修改后版本:     修改人:  修改日期: 2020/08/24 15:41  修改内容:
 * </pre>
 */
@Component
public class CasPropertiesLoader {

    public final static String CAS_ENABLED = "security.cas.enabled";
    public final static String CAS_SERVER_HOST_URL = "security.cas.server.host.url";
    public final static String CAS_SERVER_HOST_LOGIN_URL = "security.cas.server.host.login_url";
    public final static String CAS_SERVER_HOST_LOGOUT_URL = "security.cas.server.host.logout_url";
    public final static String APP_SERVER_HOST_URL = "security.app.server.host.url";

    public CasPropertiesLoader(){}
    private static Properties props = new Properties();
    static{
        try {
            props.load(CasPropertiesLoader .class.getClassLoader().getResourceAsStream("cas.properties"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static String getValue(String key){
        return props.getProperty(key);
    }
    public static void updateProperties(String key,String value) {
        props.setProperty(key, value);
    }


}

对于remoteUser,可以改写HttpServletRequestWrapperFilter源码,实现符合自己的业务:

package com.extra.login.cas.client.util;

import java.io.IOException;
import java.security.Principal;
import java.util.Collection;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import javax.servlet.http.HttpSession;

import com.admin.system.oa.service.OaUserService;
import com.extra.login.hz.cas.client.config.FilterCondition;
import com.common.filter.SSOFilter;
import com.common.utils.ApplicationContextHolder;
import com.common.utils.config.ApprPropConfigUtil;
import org.apache.commons.lang3.StringUtils;
import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.configuration.ConfigurationKeys;
import org.jasig.cas.client.util.AbstractCasFilter;
import org.jasig.cas.client.util.AbstractConfigurationFilter;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.validation.Assertion;
import org.springframework.util.Assert;

/**
 * Implementation of a filter that wraps the normal HttpServletRequest with a
 * wrapper that overrides the following methods to provide data from the
 * CAS Assertion:
 * <ul>
 * <li>{@link HttpServletRequest#getUserPrincipal()}</li>
 * <li>{@link HttpServletRequest#getRemoteUser()}</li>
 * <li>{@link HttpServletRequest#isUserInRole(String)}</li>
 * </ul>
 * <p/>
 * This filter needs to be configured in the chain so that it executes after
 * both the authentication and the validation filters.
 *
 * @author Scott Battaglia
 * @author Marvin S. Addison
 * @version $Revision: 11729 $ $Date: 2007-09-26 14:22:30 -0400 (Tue, 26 Sep 2007) $
 * @since 3.0
 */
public final class HttpServletRequestWrapperFilter extends AbstractConfigurationFilter {

    /** Name of the attribute used to answer role membership queries */
    private String roleAttribute;

    /** Whether or not to ignore case in role membership queries */
    private boolean ignoreCase;

    private String sessionKeyName = ApprPropConfigUtil.get("session.keyName");

    // 默认sessionKey
    private static String DEFAULT_SESSION_KEY_NAME = "ssoLoginUser";

    public void destroy() {
        // nothing to do
    }

    /**
     * Wraps the HttpServletRequest in a wrapper class that delegates
     * <code>request.getRemoteUser</code> to the underlying Assertion object
     * stored in the user session.
     */
    public void doFilter(final ServletRequest servletRequest, final ServletResponse servletResponse,
                         final FilterChain filterChain) throws IOException, ServletException {
        if (!FilterCondition.isCasLoginMode()) { // 产品模式跳过
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }
        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        if (SSOFilter.notNeedLoginUrl.contains(request.getRequestURI()) || request.getRequestURI().startsWith("/Common/static")) {
            filterChain.doFilter(request, servletResponse);
            return;
        }
        final AttributePrincipal principal = retrievePrincipalFromSessionOrRequest(servletRequest);
        logger.info("cas用户账号:{}",principal.getName());
        //String userCode = this.getApprUser(principal.getName());
        filterChain.doFilter(new HttpServletRequestWrapperFilter.CasHttpServletRequestWrapper((HttpServletRequest) servletRequest),
                servletResponse);
    }

    @Deprecated
    protected String getApprUser(String casUser) {
        OaUserService oaUserService = (OaUserService) ApplicationContextHolder.getApplicationContext().getBean("oaUserService");
        String apprUserCode = oaUserService.getApproveUserCodeBySysType(casUser, CasParamKeyEnum.CAS_SIGN.getCasParamKey());
        logger.info("cas对应审批用户账号:{}",apprUserCode);
        Assert.state(StringUtils.isNotBlank(apprUserCode), "请联系管理员配置单点登录关联数据!");
        return apprUserCode;
    }

    protected AttributePrincipal retrievePrincipalFromSessionOrRequest(final ServletRequest servletRequest) {
        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final HttpSession session = request.getSession(false);
        final Assertion assertion = (Assertion) (session == null ? request
                .getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION) : session
                .getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION));

        return assertion == null ? null : assertion.getPrincipal();
    }

    public void init(final FilterConfig filterConfig) throws ServletException {
        if (FilterCondition.isHzCasLoginMode()) {
            super.init(filterConfig);
            this.roleAttribute = getString(ConfigurationKeys.ROLE_ATTRIBUTE);
            this.ignoreCase = getBoolean(ConfigurationKeys.IGNORE_CASE);
        }
    }

    final class CasHttpServletRequestWrapper extends HttpServletRequestWrapper {
        String userCode;
        CasHttpServletRequestWrapper(HttpServletRequest request) {
            super(request);
            this.userCode = (String) request.getSession()
                    .getAttribute(org.apache.commons.lang3.StringUtils.isBlank(sessionKeyName)?DEFAULT_SESSION_KEY_NAME:sessionKeyName);
        }
        @Override
        public String getRemoteUser() {
            return userCode;
        }

    }
}

根据cas用户账号,找业务系统关联数据,并丢数据到session

String userName = this.getCasLoginUser();
logger.info("CAS用户账号:"+userName);
String apprUserCode=oaUserService.getUserCodeBySysType(userName,CasParamKeyEnum.CAS_SIGN.getCasParamKey());
logger.info("业务系统对应的userCode:"+apprUserCode);
String sessionKey = org.springframework.util.StringUtils.isEmpty(ApprPropConfigUtil.get("session.keyName"))?"ssoLoginUser":ApprPropConfigUtil.get("session.keyName");
HttpSession session = request.getSession();
if (!StringUtils.isEmpty(apprUserCode)) {
	session.setAttribute(sessionKey, apprUserCode);
}

获取CAS用户账号:

protected String getCasLoginUser(){
		Assertion assertion = AssertionHolder.getAssertion();
		String userName = "";
		if (assertion != null) {
			userName = assertion.getPrincipal().getName();
			logger.info("userName:"+userName);
		}
		return userName;
	}