前端点击“登出”按钮,跳转到CAS的登出。
CAS默认配置了单点登出,在登出后,会向所有客户端系统发送这个用户登出的报文。
各客户端系统有责任接收并处理这个用户登出的报文,然后在注销该用户会话在本客户端的信息。
若不进行 #CAS配置客户端地址 或 #客户端后端 ,则网页里的登出按钮点击之后就无法通知其他客户端系统登出。
目前使用了客户端集成CAS源码并修改的方法,来对客户端做了Filter接收CAS登出请求之后的处理。
有悖于CAS的建议"不要修改CAS源码"。待以后再研究看有没有什么优化方法。
CAS配置客户端地址
CAS的单点登出是默认启用的。
CAS向客户端发送地址,默认为客户端请求的URL。但该请求的URL是客户端的前端地址(含端口),CAS需要向客户端的后端发送请求。因此需要对对CAS配置客户端的指定地址。
CAS 对客户端信息的存储在/services/
目录下,这里配置jl-iam-client的信息,文件命名为iam-client-10000003.json
,内容如下。
其中
- @class - 按默认填写
- serviceId - 正则表达式匹配客户端请求到CAS的来源URL,这里是前端的地址。因为请求都是从前端
window.location.href
过来的。 - name - 客户端名称
- id - ID,不要重复
- description - 描述
- evalutionOrder - 顺序。一个客户端的URL匹配成功了多个客户端信息,以evalutionOrder最小的来处理。
- logoutType - 登出类型。可填"BACK_CHANNEL"或"FRONT_CHANNEL",分别对应后端方式、前端方式。以不配置此项,则默认是"BACK_CHANNEL"。这里使用后端方式登出。
- logoutUrl - 登出URL。如果不配置此项,CAS会向来源URL发送登出请求。在前后端分离的结构下,来源是前端地址,与后端地址不同。因此需要配置该项为后端地址
该地址最后为上下文根+“/”。然后不需再有其他字符。
上下文根一定要有!
上下文根后的"/"一定要有!
否则无法访问到客户端的Filter。另外,由于配置单点登出的Filter过滤URL样式为"/*",因此后面不需要加其他字符。
{
"@class" : "org.apereo.cas.services.RegexRegisteredService",
"serviceId" : "^(http)://192.168.2.111:8086.*",
"name" : "iam-client",
"id" : 10000003,
"description" : "iam-client info",
"evaluationOrder" : 1,
"logoutType" : "BACK_CHANNEL",
"logoutUrl" : "http://192.168.2.111:8082/iam-client/"
}
客户端后端
配置Filter
客户端(jl-iam-client)需要配置Filter来接收CAS单点登出的请求。
接收请求后需要做这些处理:记录登出日志、删除Redis信息、调用Shiro的logout。
在cas-client-core
中的SingleSignOutFilter
清空了Session,但是并没有其他处理。
由于目前还没有找到追增处理的方法,因此现在将cas-client-core
的SingleSignOutFilter
以及SingleSignOutHandler
重写在工程里,对登出处理做了一些定制。
客户端使用springboot构建。使用@WebFilter 注解,将定制的SingleSignOutFilter
注册为Filter。
注意注解的参数配置。
@WebFilter(urlPatterns = "/*", initParams = {
@WebInitParam(name = "casServerUrlPrefix", value = "https://cas.example.com:8443/cas")
})
public class SingleSignOutFilter extends AbstractConfigurationFilter {
设置回调方法
注销时删除Session的方法,是在SingleSignOutHandler
的destroySession()
方法中执行的,并且CAS的票据ticket
也是在该方法中解析到的。
在SingleSignOutHandler
中,设置一个回调方法,在destroySession()
方法中调用。
回调方法定义
额外登出策略类 ExtraLogoutStrategy.java
public interface ExtraLogoutStrategy {
/**
* 登出方法
* @param ticket CAS票据
*/
void logout(String ticket);
}
额外登出策略实现类 CasLogoutStrategy.java
public class CasLogoutStrategy implements ExtraLogoutStrategy {
@Autowired
private RedisUtil redisUtil;
@Autowired
private ISysBaseAPI sysBaseAPI;
@Resource
private BaseCommonService baseCommonService;
@Override
public void logout(String ticket) {
String token = redisUtil.get(CommonConstant.PRIFIX_CAS_TICKET + ticket).toString();
String username = JwtUtil.getUsername(token);
LoginUser sysUser = sysBaseAPI.getUserByName(username);
if(sysUser != null) {
baseCommonService.addLog("用户名: "+sysUser.getRealname()+",退出成功!", CommonConstant.LOG_TYPE_1, null,sysUser);
log.info(" 用户名: "+sysUser.getRealname()+",退出成功!");
//清空用户登录CAS ticket缓存
redisUtil.del(CommonConstant.PRIFIX_CAS_TICKET + ticket);
//清空用户登录Token缓存
redisUtil.del(CommonConstant.PREFIX_USER_TOKEN + token);
//清空用户登录Shiro权限缓存
redisUtil.del(CommonConstant.PREFIX_USER_SHIRO_CACHE + sysUser.getId());
//清空用户的缓存信息(包括部门信息),例如sys:cache:user::<username>
redisUtil.del(String.format("%s::%s", CacheConstant.SYS_USERS_CACHE, sysUser.getUsername()));
//调用shiro的logout
SecurityUtils.getSubject().logout();
}
}
}
调整CAS源码 SingleSignOutHandler.java
/** 额外登出处理策略回调类 **/
private ExtraLogoutStrategy extraLogoutStrategy;
/**
* 设置额外登出策略
* @param extraLogoutStrategy 额外登出策略
*/
public void setExtraLogoutStrategy(ExtraLogoutStrategy extraLogoutStrategy) {
this.extraLogoutStrategy = extraLogoutStrategy;
}
回调方法设置
修改部分 调整CAS源码 SingleSignOutFilter.java
@WebFilter(urlPatterns = "/*", initParams = {
@WebInitParam(name = "casServerUrlPrefix", value = "https://iamlocal.90tech.cn:8443/cas")
})
@Qualifier("casLogoutStrategy")
@Autowired
private ExtraLogoutStrategy casLogoutStrategy;
HANDLER.setExtraLogoutStrategy(casLogoutStrategy);
完整类 SingleSignOutFilter.java
@Slf4j
@WebFilter(urlPatterns = "/*", initParams = {
@WebInitParam(name = "casServerUrlPrefix", value = "https://iamlocal.90tech.cn:8443/cas")
})
public class SingleSignOutFilter extends AbstractConfigurationFilter {
private static final SingleSignOutHandler HANDLER = new SingleSignOutHandler();
private final AtomicBoolean handlerInitialized = new AtomicBoolean(false);
@Qualifier("casLogoutStrategy")
@Autowired
private ExtraLogoutStrategy casLogoutStrategy;
@Override
public void init(final FilterConfig filterConfig) throws ServletException {
log.info("SingleSignOutFilter init.");
super.init(filterConfig);
if (!isIgnoreInitConfiguration()) {
setArtifactParameterName(getString(ConfigurationKeys.ARTIFACT_PARAMETER_NAME));
setLogoutParameterName(getString(ConfigurationKeys.LOGOUT_PARAMETER_NAME));
setRelayStateParameterName(getString(ConfigurationKeys.RELAY_STATE_PARAMETER_NAME));
setLogoutCallbackPath(getString(ConfigurationKeys.LOGOUT_CALLBACK_PATH));
HANDLER.setArtifactParameterOverPost(getBoolean(ConfigurationKeys.ARTIFACT_PARAMETER_OVER_POST));
HANDLER.setEagerlyCreateSessions(getBoolean(ConfigurationKeys.EAGERLY_CREATE_SESSIONS));
HANDLER.setExtraLogoutStrategy(casLogoutStrategy);
}
HANDLER.init();
handlerInitialized.set(true);
}
回调方法调用
修改部分 调整CAS源码 SingleSignOutHandler.destroySession()
if (this.extraLogoutStrategy != null) {
this.extraLogoutStrategy.logout(token);
}
完整方法 SingleSignOutHandler.destroySession()
/**
* Destroys the current HTTP session for the given CAS logout request.
*
* @param request HTTP request containing a CAS logout message.
*/
private void destroySession(final HttpServletRequest request) {
String logoutMessage = CommonUtils.safeGetParameter(request, this.logoutParameterName, this.safeParameters);
if (CommonUtils.isBlank(logoutMessage)) {
logger.error("Could not locate logout message of the request from {}", this.logoutParameterName);
return;
}
if (!logoutMessage.contains("SessionIndex")) {
logoutMessage = uncompressLogoutMessage(logoutMessage);
}
logger.trace("Logout request:\n{}", logoutMessage);
final String token = XmlUtils.getTextForElement(logoutMessage, "SessionIndex");
if (CommonUtils.isNotBlank(token)) {
final HttpSession session = this.sessionMappingStorage.removeSessionByMappingId(token);
if (session != null) {
final String sessionID = session.getId();
logger.debug("Invalidating session [{}] for token [{}]", sessionID, token);
try {
session.invalidate();
} catch (final IllegalStateException e) {
logger.debug("Error invalidating session.", e);
}
this.logoutStrategy.logout(request);
if (this.extraLogoutStrategy != null) {
this.extraLogoutStrategy.logout(token);
}
}
}
}
CAS ticket缓存
在CAS代码处理中,是以接收到的ticket为凭据的。我的系统通过缓存token实现认证。
所以,为了CAS能与系统成功交互,需要将CAS ticket缓存,并与系统中的token建立关联。
上文 #额外登出策略实现类 CasLogoutStrategy.java 中,已经通过CAS ticket缓存取到了token,并将ticket和token的缓存数据都删除了。
修改部分
// 设置超时时间
redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token);
redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, JwtUtil.EXPIRE_TIME*2 / 1000);
redisUtil.set(CommonConstant.PRIFIX_CAS_TICKET + ticket, token);
redisUtil.expire(CommonConstant.PRIFIX_CAS_TICKET + ticket, JwtUtil.EXPIRE_TIME*2 / 1000);