You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot集成CAS 6.5时SingleSignOutFilter未捕获单点登出请求

Spring Boot集成CAS单点登出(SLO)失效问题

问题现象

我近日开发基于Spring Boot的应用,采用CAS实现SSO单点登录功能,目前单点登录流程运行顺畅,应用内置的登出按钮也可正常生效,但遇到单点登出(SLO)失效问题:

  • 当在其他接入CAS的应用执行登出操作时,除当前应用外其余所有接入应用均可正常完成单点登出
  • 在当前应用执行登出操作时,所有接入CAS的应用均能正常登出
    初步判断当前应用未监听SSO会话终止事件。

环境版本

  • CAS服务端版本:6.5
  • 客户端依赖spring-security-cas版本:5.7.2

现有配置代码

WebSecurityConfig 安全配置

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

  private Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);
  private SingleSignOutFilter singleSignOutFilter;
  private LogoutFilter logoutFilter;
  private CasAuthenticationProvider casAuthenticationProvider;
  private ServiceProperties serviceProperties;

  @Autowired
  public WebSecurityConfig(SingleSignOutFilter singleSignOutFilter, LogoutFilter logoutFilter,
                           CasAuthenticationProvider casAuthenticationProvider,
                           ServiceProperties serviceProperties) {
    this.logoutFilter = logoutFilter;
    this.singleSignOutFilter = singleSignOutFilter;
    this.serviceProperties = serviceProperties;
    this.casAuthenticationProvider = casAuthenticationProvider;
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.cors().and().csrf().disable();  //for testing

    http.authorizeRequests()
      .antMatchers("/login/cas","/styles.css").permitAll()    
      .antMatchers("/profile").hasAuthority(appAdmin) 
      .antMatchers( "/**").hasAnyAuthority(appUser)    

            .and()
            .addFilterBefore(singleSignOutFilter, CasAuthenticationFilter.class)
            .addFilterBefore(logoutFilter, LogoutFilter.class)                 
            .exceptionHandling().accessDeniedPage("/403")                 
            .authenticationEntryPoint(authenticationEntryPoint()
      );
  }

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.authenticationProvider(casAuthenticationProvider);
  }

  @Bean
  @Override
  protected AuthenticationManager authenticationManager() throws Exception {
    return new ProviderManager(Collections.singletonList(casAuthenticationProvider));
  }

  public AuthenticationEntryPoint authenticationEntryPoint() {
    CasAuthenticationEntryPoint entryPoint = new CasAuthenticationEntryPoint();
    entryPoint.setLoginUrl(casServer+"/cas/login");    
    entryPoint.setServiceProperties(serviceProperties);
    return entryPoint;
  }
}

CAS应用启动类配置

@EntityScan
@SpringBootApplication ()
public class CasSecuredApplication extends SpringBootServletInitializer {

  private static final Logger logger = LoggerFactory.getLogger(CasSecuredApplication.class);

  public static void main(String... args) {
    SpringApplication.run(CasSecuredApplication.class, args);
  }

  @Override
  protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
    return builder.sources(CasSecuredApplication.class);
  }

  @Bean
  public CasAuthenticationFilter casAuthenticationFilter(
    AuthenticationManager authenticationManager,
    ServiceProperties serviceProperties) throws Exception {
    CasAuthenticationFilter filter = new CasAuthenticationFilter();
    filter.setAuthenticationManager(authenticationManager);
    filter.setServiceProperties(serviceProperties);
    return filter;
  }

  @Bean
  public ServiceProperties serviceProperties() {
    logger.info("service properties");
    ServiceProperties serviceProperties = new ServiceProperties();
    serviceProperties.setService(appServer+contextPath+"/login/cas"); 
    serviceProperties.setSendRenew(false);
    return serviceProperties;
  }

  @Bean
  public TicketValidator ticketValidator() {
    return new Cas30ServiceTicketValidator(casServer+"/cas/") ;
  }   

  @Autowired
  private AuthenticationUserDetailsService UserDetail;

  @Bean
  public CasAuthenticationProvider casAuthenticationProvider(
    TicketValidator ticketValidator,
    ServiceProperties serviceProperties) {
    CasAuthenticationProvider provider = new CasAuthenticationProvider();
    provider.setServiceProperties(serviceProperties);
    provider.setTicketValidator(ticketValidator);
    provider.setAuthenticationUserDetailsService(UserDetail);
    provider.setKey("CAS_PROVIDER_LOCALHOST_8900");
    return provider;
  }

  @Bean
  public SecurityContextLogoutHandler securityContextLogoutHandler() {
    return new SecurityContextLogoutHandler();
  }

  @Bean
  public LogoutFilter logoutFilter() {
    LogoutFilter logoutFilter = new LogoutFilter(casServer+"/cas/logout", securityContextLogoutHandler());     
    logoutFilter.setFilterProcessesUrl("/logout/cas");
    return logoutFilter;
  }

  @Bean
  public SingleSignOutFilter singleSignOutFilter() {
    SingleSignOutFilter singleSignOutFilter = new SingleSignOutFilter();
    singleSignOutFilter.setLogoutCallbackPath("/exit/cas");
    singleSignOutFilter.setIgnoreInitConfiguration(true);
    return singleSignOutFilter;
  }

  @EventListener
  public SingleSignOutHttpSessionListener singleSignOutHttpSessionListener(HttpSessionEvent event) {
    return new SingleSignOutHttpSessionListener();
  }

  @Bean
  public ServletWebServerFactory servletContainer() {
    TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory() {};
    return tomcat;
  }
}

配置问题排查结论

一共3处核心配置错误,直接导致SLO请求无法被正常接收处理:

  1. SingleSignOutFilter回调路径配置错误
    手动给过滤器设置了setLogoutCallbackPath("/exit/cas"),但CAS服务端6.x版本默认向前端客户端发送SLO登出通知的请求路径是客户端注册的服务回调地址对应路径,不会主动请求/exit/cas,过滤器自然拦截不到登出通知。要么删掉这行配置走默认拦截规则,要么去CAS服务端配置SLO通知路径匹配自定义的/exit/cas。
  2. SingleSignOutHttpSessionListener注册方式错误
    用@EventListener注解修饰监听器Bean的创建方法完全无效,@EventListener是用来监听Spring容器内事件的,而SingleSignOutHttpSessionListener是标准的Servlet会话监听器,需要通过ServletListenerRegistrationBean注册到Servlet容器,否则ST与本地Session的映射关系无法被存储,就算收到SLO请求也找不到对应的会话销毁。正确注册代码如下:
@Bean
public ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> singleSignOutHttpSessionListener(){
    ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> bean = new ServletListenerRegistrationBean<>();
    bean.setListener(new SingleSignOutHttpSessionListener());
    return bean;
}
  1. 安全拦截规则未放行SLO请求
    CAS服务端发送SLO通知是POST请求,不会携带当前应用的有效登录凭证,当前拦截规则除了/login/cas和静态资源外,其余所有路径都要求有appUser权限,SLO请求会直接被权限拦截器挡掉,根本到不了SingleSignOutFilter。需要把SLO回调对应的路径加入permitAll()列表。

另外存在一个隐性问题:同时在WebSecurityConfig里通过构造器注入SingleSignOutFilter、LogoutFilter,又在启动类里定义了这两个过滤器的@Bean,Spring Boot会自动把Servlet Filter类型的Bean注册到全局容器过滤器链,可能出现过滤器重复执行的问题,建议要么去掉启动类里的过滤器Bean直接在安全配置中实例化,要么给Bean添加注册配置关闭全局注册,避免重复拦截。


内容的提问来源于stack exchange,提问作者dlemp

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 00:21:55