使用 Spring 安全和 CAS 的单一注销
Single Sign Out with Spring Security and CAS
使用纯 Spring Java 配置我在获取 Spring 和 CAS 执行单一注销时遇到了麻烦。我使用以下配置进行单点登录。我使用一个简单的 JSP 页面对 url https://nginx.shane.com/app/logout 做一个表单 POST 并且我在 POST 的数据中包含了 CSRF 值。一切似乎都没有错误,但是当我进入安全页面时,它只是让我重新进入而无需登录。有任何想法吗?
@Configuration
@EnableWebSecurity
public class SecurityWebAppConfig extends WebSecurityConfigurerAdapter {
@Bean
protected ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService("https://nginx.shane.com/app/j_spring_cas_security_check");
serviceProperties.setSendRenew(false);
return serviceProperties;
}
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
casAuthenticationProvider.setAuthenticationUserDetailsService(authenticationUserDetailsService());
casAuthenticationProvider.setServiceProperties(serviceProperties());
casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
casAuthenticationProvider.setKey("an_id_for_this_auth_provider_only");
return casAuthenticationProvider;
}
@Bean
public AuthenticationUserDetailsService<CasAssertionAuthenticationToken> authenticationUserDetailsService() {
return new TestCasAuthenticationUserDetailsService();
}
@Bean
public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
return new Cas20ServiceTicketValidator("https://nginx.shane.com/cas");
}
@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setAuthenticationManager(authenticationManager());
return casAuthenticationFilter;
}
@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
casAuthenticationEntryPoint.setLoginUrl("https://nginx.shane.com/cas/login");
casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
return casAuthenticationEntryPoint;
}
@Bean
public SingleSignOutFilter singleSignOutFilter() {
// This filter handles a Single Logout Request from the CAS Server
return new SingleSignOutFilter();
}
@Bean
public LogoutFilter requestLogoutFilter() {
// This filter redirects to the CAS Server to signal Single Logout should be performed
SecurityContextLogoutHandler handler = new SecurityContextLogoutHandler();
handler.setClearAuthentication(true);
handler.setInvalidateHttpSession(true);
LogoutFilter logoutFilter = new LogoutFilter("https://nginx.shane.com/", handler);
return logoutFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilter(casAuthenticationFilter());
http.addFilterBefore(requestLogoutFilter(), LogoutFilter.class);
http.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class);
http.exceptionHandling()
.authenticationEntryPoint(casAuthenticationEntryPoint());
http.authorizeRequests()
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.antMatchers("/dba/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_DBA')");
http.logout()
.addLogoutHandler(handler)
.deleteCookies("remove")
.invalidateHttpSession(true)
.logoutUrl("/logout")
.logoutSuccessUrl("/");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(casAuthenticationProvider());
}
}
我还添加了一个 WebListener 来处理会话销毁事件:
@WebListener
public class SecurityWebListener implements HttpSessionListener {
private SingleSignOutHttpSessionListener listener = new SingleSignOutHttpSessionListener();
@Override
public void sessionCreated(HttpSessionEvent se) {
listener.sessionCreated(se);
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
listener.sessionDestroyed(se);
}
}
这是日志输出
[org.springframework.security.web.FilterChainProxy] [/logout at position 6 of 14 in additional filter chain; firing Filter: 'LogoutFilter'] []
[org.springframework.security.web.util.matcher.AntPathRequestMatcher] [Checking match of request : '/logout'; against '/logout'] []
[org.springframework.security.web.authentication.logout.LogoutFilter] [Logging out user 'org.springframework.security.cas.authentication.CasAuthenticationToken@836ad34b: Principal: org.springframework.security.core.userdetails.User@586034f: Username: admin; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ADMIN; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@fffdaa08: RemoteIpAddress: 127.0.0.1; SessionId: FA880C15EF09C033E1CA0C8E4785905F; Granted Authorities: ROLE_ADMIN Assertion: org.jasig.cas.client.validation.AssertionImpl@fcd38ec Credentials (Service/Proxy Ticket): ST-23-1UandqRxBcG6HCTx0Pdd-cas01.example.org' and transferring to logout destination] []
[org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler] [Invalidating session: FA880C15EF09C033E1CA0C8E4785905F] []
[org.jasig.cas.client.session.HashMapBackedSessionMappingStorage] [Attempting to remove Session=[FA880C15EF09C033E1CA0C8E4785905F]] []
[org.jasig.cas.client.session.HashMapBackedSessionMappingStorage] [Found mapping for session. Session Removed.] []
[org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler] [Using default Url: /] []
[org.springframework.security.web.DefaultRedirectStrategy] [Redirecting to '/app/'] []
(不)幸运的是我遇到了类似的问题;)它发生在 CAS 尝试调用您的应用程序以注销时。一方面 CAS 尝试传递 sessionId 以执行注销,另一方面 SpringSecurity 期望获得 CSRF 令牌,该令牌不是由 CAS 发送的,因为它仅发送 GET 请求。 CsrfFilter 找不到 csrf 令牌并中断过滤器链。用户不知道这一点,因为 CAS 隐式调用注销请求。请求直接从 CAS 服务器到应用程序服务器,而不是通过在 Web 浏览器中重定向用户。
为了使其正常工作,您需要将 HttpSecurity 配置为 exclude/not 以包含 LogoutFilter filterProcessesUrl(在您使用的情况下为 j_spring_security_logout默认的)。
假设你想在尝试创建新的管理员时检查CSRF,例如,你需要配置如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilter(casAuthenticationFilter());
http.addFilterBefore(requestLogoutFilter(), LogoutFilter.class);
http.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class);
http.exceptionHandling()
.authenticationEntryPoint(casAuthenticationEntryPoint());
http.authorizeRequests()
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.antMatchers("/dba/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_DBA')");
http.csrf()
.requireCsrfProtectionMatcher(new AntPathRequestMatcher("/admin/create"));
http.logout()
.addLogoutHandler(handler)
.deleteCookies("remove")
.invalidateHttpSession(true)
.logoutUrl("/logout")
.logoutSuccessUrl("/");
}
为了说明,我已经添加了
http.csrf().requireCsrfProtectionMatcher(new AntPathRequestMatcher("/admin/create"));
.
请注意,您不能使用匹配所有模式 (/admin/**),因为您可能还想调用一些 get 请求,而 CSRF 过滤器会期望它们随后发送令牌。
3.Spring 之前的安全性不会出现此类问题。2.x,因为那里引入了跨站点请求伪造 (CSRF) 支持。
希望这些帮助:)
使用纯 Spring Java 配置我在获取 Spring 和 CAS 执行单一注销时遇到了麻烦。我使用以下配置进行单点登录。我使用一个简单的 JSP 页面对 url https://nginx.shane.com/app/logout 做一个表单 POST 并且我在 POST 的数据中包含了 CSRF 值。一切似乎都没有错误,但是当我进入安全页面时,它只是让我重新进入而无需登录。有任何想法吗?
@Configuration
@EnableWebSecurity
public class SecurityWebAppConfig extends WebSecurityConfigurerAdapter {
@Bean
protected ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService("https://nginx.shane.com/app/j_spring_cas_security_check");
serviceProperties.setSendRenew(false);
return serviceProperties;
}
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider casAuthenticationProvider = new CasAuthenticationProvider();
casAuthenticationProvider.setAuthenticationUserDetailsService(authenticationUserDetailsService());
casAuthenticationProvider.setServiceProperties(serviceProperties());
casAuthenticationProvider.setTicketValidator(cas20ServiceTicketValidator());
casAuthenticationProvider.setKey("an_id_for_this_auth_provider_only");
return casAuthenticationProvider;
}
@Bean
public AuthenticationUserDetailsService<CasAssertionAuthenticationToken> authenticationUserDetailsService() {
return new TestCasAuthenticationUserDetailsService();
}
@Bean
public Cas20ServiceTicketValidator cas20ServiceTicketValidator() {
return new Cas20ServiceTicketValidator("https://nginx.shane.com/cas");
}
@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter casAuthenticationFilter = new CasAuthenticationFilter();
casAuthenticationFilter.setAuthenticationManager(authenticationManager());
return casAuthenticationFilter;
}
@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
CasAuthenticationEntryPoint casAuthenticationEntryPoint = new CasAuthenticationEntryPoint();
casAuthenticationEntryPoint.setLoginUrl("https://nginx.shane.com/cas/login");
casAuthenticationEntryPoint.setServiceProperties(serviceProperties());
return casAuthenticationEntryPoint;
}
@Bean
public SingleSignOutFilter singleSignOutFilter() {
// This filter handles a Single Logout Request from the CAS Server
return new SingleSignOutFilter();
}
@Bean
public LogoutFilter requestLogoutFilter() {
// This filter redirects to the CAS Server to signal Single Logout should be performed
SecurityContextLogoutHandler handler = new SecurityContextLogoutHandler();
handler.setClearAuthentication(true);
handler.setInvalidateHttpSession(true);
LogoutFilter logoutFilter = new LogoutFilter("https://nginx.shane.com/", handler);
return logoutFilter;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilter(casAuthenticationFilter());
http.addFilterBefore(requestLogoutFilter(), LogoutFilter.class);
http.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class);
http.exceptionHandling()
.authenticationEntryPoint(casAuthenticationEntryPoint());
http.authorizeRequests()
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.antMatchers("/dba/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_DBA')");
http.logout()
.addLogoutHandler(handler)
.deleteCookies("remove")
.invalidateHttpSession(true)
.logoutUrl("/logout")
.logoutSuccessUrl("/");
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(casAuthenticationProvider());
}
}
我还添加了一个 WebListener 来处理会话销毁事件:
@WebListener
public class SecurityWebListener implements HttpSessionListener {
private SingleSignOutHttpSessionListener listener = new SingleSignOutHttpSessionListener();
@Override
public void sessionCreated(HttpSessionEvent se) {
listener.sessionCreated(se);
}
@Override
public void sessionDestroyed(HttpSessionEvent se) {
listener.sessionDestroyed(se);
}
}
这是日志输出
[org.springframework.security.web.FilterChainProxy] [/logout at position 6 of 14 in additional filter chain; firing Filter: 'LogoutFilter'] []
[org.springframework.security.web.util.matcher.AntPathRequestMatcher] [Checking match of request : '/logout'; against '/logout'] []
[org.springframework.security.web.authentication.logout.LogoutFilter] [Logging out user 'org.springframework.security.cas.authentication.CasAuthenticationToken@836ad34b: Principal: org.springframework.security.core.userdetails.User@586034f: Username: admin; Password: [PROTECTED]; Enabled: true; AccountNonExpired: true; credentialsNonExpired: true; AccountNonLocked: true; Granted Authorities: ROLE_ADMIN; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@fffdaa08: RemoteIpAddress: 127.0.0.1; SessionId: FA880C15EF09C033E1CA0C8E4785905F; Granted Authorities: ROLE_ADMIN Assertion: org.jasig.cas.client.validation.AssertionImpl@fcd38ec Credentials (Service/Proxy Ticket): ST-23-1UandqRxBcG6HCTx0Pdd-cas01.example.org' and transferring to logout destination] []
[org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler] [Invalidating session: FA880C15EF09C033E1CA0C8E4785905F] []
[org.jasig.cas.client.session.HashMapBackedSessionMappingStorage] [Attempting to remove Session=[FA880C15EF09C033E1CA0C8E4785905F]] []
[org.jasig.cas.client.session.HashMapBackedSessionMappingStorage] [Found mapping for session. Session Removed.] []
[org.springframework.security.web.authentication.logout.SimpleUrlLogoutSuccessHandler] [Using default Url: /] []
[org.springframework.security.web.DefaultRedirectStrategy] [Redirecting to '/app/'] []
(不)幸运的是我遇到了类似的问题;)它发生在 CAS 尝试调用您的应用程序以注销时。一方面 CAS 尝试传递 sessionId 以执行注销,另一方面 SpringSecurity 期望获得 CSRF 令牌,该令牌不是由 CAS 发送的,因为它仅发送 GET 请求。 CsrfFilter 找不到 csrf 令牌并中断过滤器链。用户不知道这一点,因为 CAS 隐式调用注销请求。请求直接从 CAS 服务器到应用程序服务器,而不是通过在 Web 浏览器中重定向用户。
为了使其正常工作,您需要将 HttpSecurity 配置为 exclude/not 以包含 LogoutFilter filterProcessesUrl(在您使用的情况下为 j_spring_security_logout默认的)。
假设你想在尝试创建新的管理员时检查CSRF,例如,你需要配置如下:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.addFilter(casAuthenticationFilter());
http.addFilterBefore(requestLogoutFilter(), LogoutFilter.class);
http.addFilterBefore(singleSignOutFilter(), CasAuthenticationFilter.class);
http.exceptionHandling()
.authenticationEntryPoint(casAuthenticationEntryPoint());
http.authorizeRequests()
.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
.antMatchers("/dba/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_DBA')");
http.csrf()
.requireCsrfProtectionMatcher(new AntPathRequestMatcher("/admin/create"));
http.logout()
.addLogoutHandler(handler)
.deleteCookies("remove")
.invalidateHttpSession(true)
.logoutUrl("/logout")
.logoutSuccessUrl("/");
}
为了说明,我已经添加了
http.csrf().requireCsrfProtectionMatcher(new AntPathRequestMatcher("/admin/create"));
.
请注意,您不能使用匹配所有模式 (/admin/**),因为您可能还想调用一些 get 请求,而 CSRF 过滤器会期望它们随后发送令牌。
3.Spring 之前的安全性不会出现此类问题。2.x,因为那里引入了跨站点请求伪造 (CSRF) 支持。
希望这些帮助:)