业务场景:之前写过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; }
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。
我来说两句