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请求无法被正常接收处理:
SingleSignOutFilter回调路径配置错误
手动给过滤器设置了setLogoutCallbackPath("/exit/cas"),但CAS服务端6.x版本默认向前端客户端发送SLO登出通知的请求路径是客户端注册的服务回调地址对应路径,不会主动请求/exit/cas,过滤器自然拦截不到登出通知。要么删掉这行配置走默认拦截规则,要么去CAS服务端配置SLO通知路径匹配自定义的/exit/cas。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; }
- 安全拦截规则未放行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
相关产品推荐
相关产品推荐

