6. Here's the "logout" filter:
Code:
import org.springframework.beans.factory.InitializingBean;
import org.springframework.dao.DataRetrievalFailureException;
import org.springframework.util.Assert;
import org.acegisecurity.Authentication;
import org.acegisecurity.context.SecurityContextHolder;
import org.acegisecurity.providers.cas.CasAuthenticationToken;
import org.acegisecurity.ui.cas.ServiceProperties;
import org.acegisecurity.ui.logout.LogoutHandler;
public class CasLogoutFilter implements Filter, InitializingBean
{
private String filterProcessesUrl;
private String logoutSuccessUrl;
private LogoutHandler[] logoutHandlers;
private ServiceProperties serviceProperties;
private String logoutUrl;
private ExpiredTicketCache expiredTicketCache;
//-------------------------------------------------------------------------
// Methods
//-------------------------------------------------------------------------
/**
* The "magic" URL that triggers a CAS logout
*/
public void setFilterProcessesUrl( String s )
{
this.filterProcessesUrl = s;
}
/**
* Where to go after user successfully logs out from CAS
*/
public void setLogoutSuccessUrl( String s ) { this.logoutSuccessUrl = s; }
/**
* Logout handlers that clean up after logout
*/
public void setLogoutHandlers( LogoutHandler[] handlers )
{
this.logoutHandlers = handlers;
}
/**
* Provides the "service URL" to send to CAS
*/
public void setServiceProperties( ServiceProperties sp )
{
this.serviceProperties = sp;
}
/**
* The CAS logout URL
*/
public void setLogoutUrl( String s ) { this.logoutUrl = s; }
/**
* The store of expired tickets received from CAS
*/
public void setExpiredTicketCache( ExpiredTicketCache cache )
{
this.expiredTicketCache = cache;
}
//-------------------------------------------------------------------------
// Implements InitializingBean
//-------------------------------------------------------------------------
public void afterPropertiesSet() throws Exception
{
Assert.hasText(this.filterProcessesUrl, "filterProcessesUrl required");
Assert.hasText(this.logoutSuccessUrl, "logoutSuccessUrl required");
Assert.notEmpty(this.logoutHandlers, "logoutHandlers are required");
Assert.notNull(this.serviceProperties, "serviceProperties required");
Assert.notNull(this.logoutUrl, "logoutUrl required");
}
//-------------------------------------------------------------------------
// Implements Filter
//-------------------------------------------------------------------------
public void init( FilterConfig config ) throws ServletException { }
public void destroy() { }
public void doFilter(
ServletRequest request, ServletResponse response, FilterChain chain )
throws IOException, ServletException
{
if(! (request instanceof HttpServletRequest) )
{
throw new ServletException("Can only process HttpServletRequest");
}
if(! (response instanceof HttpServletResponse) )
{
throw new ServletException("Can only process HttpServletResponse");
}
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
boolean loggedOut = false;
Authentication auth =
SecurityContextHolder.getContext().getAuthentication();
// has the authentication's ticket expired because of a CAS logout
// initiated from another webapp?
if( auth instanceof CasAuthenticationToken &&
this.expiredTicketCache != null )
{
String serviceTicket = auth.getCredentials().toString();
if( this.expiredTicketCache.isTicketExpired(serviceTicket) )
{
for( int i = 0; i < this.logoutHandlers.length; i++ )
{
this.logoutHandlers[i].logout(httpRequest, httpResponse, auth);
}
this.expiredTicketCache.removeTicketFromCache(serviceTicket);
loggedOut = true;
}
}
// is the user explicitly requesting logout?
if( requiresLogout(httpRequest) )
{
if( loggedOut == false )
{
// we haven't called the logout handlers above, so do so now
for( int i = 0; i < this.logoutHandlers.length; i++ )
{
this.logoutHandlers[i].logout(httpRequest, httpResponse, auth);
}
}
String urlEncodedService = httpResponse.encodeURL(
this.serviceProperties.getService());
StringBuffer buffer = new StringBuffer(255);
synchronized( buffer )
{
buffer.append(this.logoutUrl);
buffer.append("?service=");
buffer.append(URLEncoder.encode(urlEncodedService, "UTF-8"));
buffer.append("&url=");
String successUrl = this.logoutSuccessUrl;
// ensure success URL is an absolute URL
if( !successUrl.startsWith("http://") &&
!successUrl.startsWith("https://") )
{
buffer.append(httpRequest.getContextPath());
}
buffer.append(successUrl);
}
// have browser tell CAS to log out
httpResponse.sendRedirect(buffer.toString());
return;
}
chain.doFilter(request, response);
}
protected boolean requiresLogout( HttpServletRequest request )
{
String uri = request.getRequestURI();
// strip everything after the first semi-colon
int pathParamIndex = uri.indexOf(';');
if( pathParamIndex > 0 )
{
uri = uri.substring(0, pathParamIndex);
}
return uri.endsWith(
request.getContextPath() + this.filterProcessesUrl);
}
}
7. Here's the Spring bean definition for the "logout" filter:
Code:
<bean id="casLogoutFilter"
class="example.CasLogoutFilter">
<property name="filterProcessesUrl">
<value>/Logout.html</value>
</property>
<property name="logoutSuccessUrl">
<value>/Home.html</value>
</property>
<property name="logoutHandlers">
<list>
<bean class="org.acegisecurity.ui.logout.SecurityContextLogoutHandler">
<property name="invalidateHttpSession"><value>true</value></property>
</bean>
</list>
</property>
<property name="serviceProperties">
<ref local="serviceProperties" />
</property>
<property name="logoutUrl">
<value>https://example.com/cas/logout</value>
</property>
<property name="expiredTicketCache">
<ref local="expiredTicketCache" />
</property>
</bean>
This filter does two things:
a) when the user activates the "logout" link, "Logout.html", they are redirected to CAS' "/logout" page
b) if the incoming request is from a previously-authenticated user whose ticket has expired because of a CAS logout initiated elsewhere, the user is logged out of the webapp.
8. Here's the FilterChainProxy bean:
Code:
<bean id="filterChainProxy"
class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/**=httpSessionContextIntegrationFilter,casLogoutFilter,casLogoutCallbackFilter,authenticationProcessingFilter,exceptionTranslationFilter,filterInvocationInterceptor,casSignonFilter
</value>
</property>
</bean>
The casLogoutFilter is pretty early in the chain because we want to be sure to log the expired user out before checking any authentication.
The casLogoutCallbackFilter comes before the authenticationProcessingFilter, because we don't want the authenticationProcessingFilter to try to process the "/j_acegi_cas_security_check" request that CAS sends for the logout callback.
Again, this appears to be working for me in my environment, which I have not yet deployed to a production server. Your mileage may vary (and if it does, I'd like to hear about it).
(Sorry for the long post(s)!)