业务场景:之前写过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;
}
本文同步分享在 博客“smileNicky”(CSDN)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。