前端点击“登出”按钮,跳转到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-coreSingleSignOutFilter以及SingleSignOutHandler重写在工程里,对登出处理做了一些定制。

  客户端使用springboot构建。使用@WebFilter 注解,将定制的SingleSignOutFilter注册为Filter。

注意注解的参数配置。

@WebFilter(urlPatterns = "/*", initParams = {
        @WebInitParam(name = "casServerUrlPrefix", value = "https://cas.example.com:8443/cas")
})
public class SingleSignOutFilter extends AbstractConfigurationFilter {

设置回调方法

  注销时删除Session的方法,是在SingleSignOutHandlerdestroySession()方法中执行的,并且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);
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐