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;
}
- oc 中随机数的用法(arc4random() 、random()、CCRANDOM_0_1()
- 央行出台条码支付新规;美团打车将在7大城市上线;国产人工智能平台型芯片首发;苹果就降速门致歉
- 阿三又让全球看笑话,无人驾驶的地铁名不副实,还直接跑到大街上
- 盘点:2017年度这些科学大事件,哪个给您印象最深?
- 跳台阶问题
- mac下使用github
- CSS3新特性应用之结构与布局
- 程序猿的日常——Java基础之equals与hashCode
- 贾康:智能金融需在发展和规范中掌握理性权衡点
- Objective-C: 字符串NSString与NSMutableString
- 2017年最后一篇推送,仍然与技术有关盘点深度学习论文年度之“最”
- 【垂直居中高级篇】你不知道的垂直居中方式
- 第一个APP:IOS做简单运算的计算器
- 进程、线程、应用程序之间的关系
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释
- mybatis-plus一些关键配置
- mybatis-plus自定义sql注入器
- k8s代码走读---kube-controller-manager
- 我们一起学一学渗透测试——黑客应该掌握的HTML基础知识(一)
- 一套漏洞组合拳接管你的账号
- 我们一起学一学渗透测试——黑客应该掌握的HTML基础知识(二)
- 我用Paddle Lite在树莓派3b+上从零开始搭建“实时表情识别”项目
- mybatis-plus:自动填充功能
- 词义类比与全局词共现信息不可兼得?基于飞桨实现的GloVe说可以
- MyBatis-plus乐观锁插件
- Jmeter(十九) - 从入门到精通 - JMeter监听器 -上篇(详解教程)
- python---rsa加密根据指数和模生成加密参数模板
- 搞了这么多年终于知道接口和抽象类的应用场景了
- css实现页面加载动画
- 用Dota2“最强”算法PPO完成CarPole和四轴飞行器悬浮任务