Concurrent Login Handling in Clustered Environment
In this article we are going to achieve followings.
* To user Springs Concurrent Log-in Handling in a clustered environment.
* To Handle concurrent log-in bit smarter way. Instead of giving two options for the developer to
o Login out the already logged in user when the second logon is requested.
o Preventing the second user from login in with same credentials.
Here we are trying to let the user select way the concurrent login detection should be handled. When the system detects that there is an already active session for the user, it will redirect to a page which has a check box , which can be clicked to indicate that it's OK to invalidate the pre-existing session.
To do this following need to be done.
* Replace SessionRegistryImpl with ClustereAwareSessionRegistryImpl (which persists the session to database. In addition to the interface methods (of SessionRegistry) this will have an additional method expireNow(String sessionId) to call the backend to expire the session.
* Need to manually configure Spring security without using Security Namespaces.
o To have a different page based on login exceptions. For ConcurrentLoginException we user multipleLogin page which has an additional checkbox which can be clicked to logout the logged in user.
o To override the LogoutFilter, We need to add a new LogoutHandler to invalidate the session in the database. This handler is invoked from ConcurrentLogonFilter and LogoutFilter. To configure these filters to use our handler we need to configure these two filters manually. For that we need to take out <security:http 's auto-configure=true away and configure the security manually. This is configured by injecting the class property to an instance of org.springframework.security.ui.AuthenticationDetailsSourceImpl .
[edit]
Spring Configureation files
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-2.0.4.xsd">
<!-- Configure Spring Security
NOTE: The order of the url interceptors is important. It stops at the first one that matches.
-->
<security:http
access-denied-page="/spring/login?login_error=1"
entry-point-ref="authenticationEntryPoint" >
<security:intercept-url
pattern="/spring/registerAccreditation/**"
access="ROLE_DOI-PTD-MY-ACCR-PO" />
<security:intercept-url pattern="/spring/login" filters="none" />
<security:intercept-url pattern="/resources/**" filters="none" />
<security:intercept-url pattern="/layouts/*" filters="none" />
<security:intercept-url pattern="/images/*" filters="none" />
<security:intercept-url pattern="/styles/*" filters="none" />
<security:intercept-url pattern="/spring/multipleLogins"
filters="none" />
<security:intercept-url pattern="/**"
access="ROLE_DOI-PTD-MY-ACCR-LOGIN" />
<!--
<security:form-login login-page="/spring/login"
login-processing-url="/spring/loginProcess"
default-target-url="/spring/dashboard"
always-use-default-target="true"
authentication-failure-url="/spring/multipleLogins" />
<security:logout logout-url="/spring/logout"
logout-success-url="/spring/dashboard" />
-->
<!--
<security:concurrent-session-control max-sessions="1"
exception-if-maximum-exceeded="true" session-registry-alias="concurrentSessionRegistry"
expired-url="" />
-->
</security:http>
<security:authentication-provider user-service-ref="userDetailsService"></security:authentication-provider>
<bean id="authenticationEntryPoint"
class="org.springframework.security.ui.webapp.AuthenticationProcessingFilterEntryPoint">
<property name="loginFormUrl" value="/spring/login" />
</bean>
<bean id="authenticationDetailsSource"
class="org.springframework.security.ui.AuthenticationDetailsSourceImpl">
<property name="clazz"
value="myapp.web.session.MYWebAuthenticationDetails" />
</bean>
<bean id="authenticationManager"
class="org.springframework.security.providers.ProviderManager">
<property name="providers">
<list>
<ref bean="ldapAuthenticationProvider" />
</list>
</property>
<property name="sessionController" ref="sc"></property>
</bean>
<bean id="authenticationProcessingFilter"
class="org.springframework.security.ui.webapp.AuthenticationProcessingFilter">
<property name="authenticationManager">
<ref bean="authenticationManager" />
</property>
<property name="authenticationFailureUrl">
<value>/spring/login</value>
</property>
<property name="defaultTargetUrl">
<value>/spring/dashboard</value>
</property>
<property name="filterProcessesUrl">
<value>/spring/loginProcess</value>
</property>
<property name="alwaysUseDefaultTargetUrl">
<value>true</value>
</property>
<property name="authenticationDetailsSource">
<ref local="authenticationDetailsSource" />
</property>
<property name="exceptionMappings">
<props>
<prop
key="org.springframework.security.concurrent.ConcurrentLoginException">
/spring/multipleLogins
</prop>
</props>
</property>
<security:custom-filter
after="AUTHENTICATION_PROCESSING_FILTER" />
</bean>
<bean id="logoutFilter" class="org.springframework.security.ui.logout.LogoutFilter">
<constructor-arg value="/spring/dashboard"/>
<constructor-arg>
<list>
<ref bean="clusterAwareLogoutHandler" />
<ref bean="securityContextLogoutHandler"/>
</list>
</constructor-arg>
<property name="filterProcessesUrl" value="/spring/logout"/>
<security:custom-filter position="LOGOUT_FILTER"></security:custom-filter>
</bean>
<bean id="securityContextLogoutHandler" class="org.springframework.security.ui.logout.SecurityContextLogoutHandler" />
<bean id="clusterAwareLogoutHandler" class="myapp.web.session.ClusterAwareLogoutHandler" >
<property name="sessionRegistry" ref="sessionRegistry" />
</bean>
<bean id="concurrentSessionFilter"
class="org.springframework.security.concurrent.ConcurrentSessionFilter">
<property name="sessionRegistry">
<ref local="sessionRegistry" />
</property>
<property name="expiredUrl">
<value>/spring/login</value>
</property>
<property name="logoutHandlers">
<list>
<ref bean="clusterAwareLogoutHandler" />
<ref bean="securityContextLogoutHandler"/>
</list>
</property>
<security:custom-filter position="CONCURRENT_SESSION_FILTER"></security:custom-filter>
</bean>
<bean id="sc"
class="myapp.web.session.ConcurrentSessionControllerImpl">
<property name="sessionRegistry" ref="sessionRegistry" />
</bean>
<bean id="sessionRegistry"
class="myapp.web.session.ClustereAwareSessionRegistryImpl" >
<property name="sessionInfoService" ref="sessionInfoService"></property>
</bean>
<security:authentication-manager alias="authManager"
session-controller-ref="sc" />
</beans>
Environment specific ldap configuration
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-2.0.4.xsd">
<!-- begin security configuration for LDAP Server. -->
<!-- had to use manual been configuration since required to override the concurrent session handling stratergy. see security-config.xml file. -->
<bean id="initialDirContextFactory"
class="org.springframework.security.ldap.DefaultInitialDirContextFactory">
<constructor-arg value="ldap://ldapserver:389" />
<property name="managerDn">
<value>username1</value>
</property>
<property name="managerPassword">
<value>pasword1</value>
</property>
</bean>
<bean id="userDetailsService"
class="org.springframework.security.userdetails.ldap.LdapUserDetailsService">
<constructor-arg ref="ldapUserSearch" />
<constructor-arg ref="ldapAuthoritiesPopulator" />
</bean>
<bean id="ldapUserSearch"
class="org.springframework.security.ldap.search.FilterBasedLdapUserSearch">
<constructor-arg index="0" value="" />
<constructor-arg index="1" value="(uid={0})" />
<constructor-arg index="2" ref="initialDirContextFactory" />
</bean>
<bean id="ldapBindAuthenticator"
class="org.springframework.security.providers.ldap.authenticator.BindAuthenticator">
<constructor-arg>
<ref local="initialDirContextFactory" />
</constructor-arg>
<property name="userDnPatterns">
<list>
<value>uid={0}</value>
</list>
</property>
<property name="userSearch" ref="ldapUserSearch" />
</bean>
<bean id="ldapAuthoritiesPopulator"
class="org.springframework.security.ldap.populator.DefaultLdapAuthoritiesPopulator">
<constructor-arg>
<ref local="initialDirContextFactory" />
</constructor-arg>
<constructor-arg>
<value></value>
</constructor-arg>
<property name="groupRoleAttribute">
<value>cn</value>
</property>
<property name="groupSearchFilter">
<value>uniqueMember={0}</value>
</property>
<!--
<property name="searchSubtree">
<value>true</value>
</property>
-->
<property name="rolePrefix">
<value>ROLE_</value>
</property>
<property name="convertToUpperCase">
<value>true</value>
</property>
</bean>
<bean id="ldapAuthenticationProvider"
class="org.springframework.security.providers.ldap.LdapAuthenticationProvider">
<constructor-arg ref="ldapBindAuthenticator"></constructor-arg>
<constructor-arg ref="ldapAuthoritiesPopulator">
</constructor-arg>
</bean>
</beans>
ClusterAwareLogoutHandler
public class ClusterAwareLogoutHandler implements LogoutHandler
{
private SessionRegistry sessionRegistry;
public SessionRegistry getSessionRegistry()
{
return sessionRegistry;
}
public void setSessionRegistry(SessionRegistry sessionRegistry)
{
this.sessionRegistry = sessionRegistry;
}
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
{
if (request.getSession() != null)
{
String sessionId = request.getSession().getId();
if(sessionRegistry instanceof ClustereAwareSessionRegistryImpl)
{
((ClustereAwareSessionRegistryImpl) sessionRegistry).expireNow(sessionId);
}else //this shouldn't happen since we are expecting a ClusterAwareSessionRegistryImpl to use this class.
{
getSessionRegistry().removeSessionInformation(sessionId);
}
}
}
}
MYWebAuthenticationDetails
public class MYWebAuthenticationDetails extends WebAuthenticationDetails
{
private boolean exceptionIfMaximumSessionsExceeded ;
/**
* @param request
*/
public MYWebAuthenticationDetails(HttpServletRequest request)
{
super(request);
}
/* (non-Javadoc)
* @see org.springframework.security.ui.WebAuthenticationDetails#doPopulateAdditionalInformation(javax.servlet.http.HttpServletRequest)
*/
protected void doPopulateAdditionalInformation(HttpServletRequest request)
{
super.doPopulateAdditionalInformation(request);
//default is true. change only 'false' is returned as request parameter.
this.exceptionIfMaximumSessionsExceeded = !("on".equals(request.getParameter("exceptionIfMaximumSessionsExceeded")));
}
/**
* @return Returns the exceptionIfMaximumSessionsExceeded.
*/
public boolean isExceptionIfMaximumSessionsExceeded()
{
return exceptionIfMaximumSessionsExceeded;
}
}
ConcurrentSessionControllerImpl
public class ConcurrentSessionControllerImpl extends
org.springframework.security.concurrent.ConcurrentSessionControllerImpl
{
public void checkAuthenticationAllowed(Authentication request) throws AuthenticationException
{
Assert.notNull(request, "Authentication request cannot be null (violation of interface contract)");
Object principal = SessionRegistryUtils.obtainPrincipalFromAuthentication(request);
String sessionId = SessionRegistryUtils.obtainSessionIdFromAuthentication(request);
SessionInformation[] sessions = getSessionRegistry().getAllSessions(principal, false);
int sessionCount = 0;
if (sessions != null)
{
sessionCount = sessions.length;
}
int allowableSessions = getMaximumSessionsForThisUser(request);
Assert.isTrue(allowableSessions != 0, "getMaximumSessionsForThisUser() must return either -1 to allow "
+ "unlimited logins, or a positive integer to specify a maximum");
if (sessionCount < allowableSessions)
{
// They haven't got too many login sessions running at present
return;
}
else if (allowableSessions == -1)
{
// We permit unlimited logins
return;
}
else if (sessionCount == allowableSessions)
{
// Only permit it though if this request is associated with one of the sessions
for (int i = 0; i < sessionCount; i++)
{
if (sessions[i].getSessionId().equals(sessionId))
{
return;
}
}
}
allowableSessionsExceeded(sessionId, sessions, allowableSessions, getSessionRegistry(), request.getDetails());
}
protected void allowableSessionsExceeded(String sessionId, SessionInformation[] sessions, int allowableSessions,
SessionRegistry registry, Object authenticationDetail)
{
boolean exceptionIfMaximumExceeded = true;
if (authenticationDetail instanceof MYWebAuthenticationDetails)
{
MYWebAuthenticationDetails authDetails = (MYWebAuthenticationDetails) authenticationDetail;
exceptionIfMaximumExceeded = authDetails.isExceptionIfMaximumSessionsExceeded();
}
if (exceptionIfMaximumExceeded || (sessions == null))
{
throw new ConcurrentLoginException(messages.getMessage("ConcurrentSessionControllerImpl.exceededAllowed",
new Object[]
{ new Integer(allowableSessions) }, "Maximum sessions of {0} for this principal exceeded"));
}
// Determine least recently used session, and mark it for invalidation
SessionInformation leastRecentlyUsed = null;
//expire the last recently used session
/*
for (int i = 0; i < sessions.length; i++)
{
if ((leastRecentlyUsed == null) || sessions[i].getLastRequest().before(leastRecentlyUsed.getLastRequest()))
{
leastRecentlyUsed = sessions[i];
}
}
//this class expects the use of ClustereAwareSessionRegistryImpl .
if (getSessionRegistry() instanceof ClustereAwareSessionRegistryImpl)
{
ClustereAwareSessionRegistryImpl sessRegistry = (ClustereAwareSessionRegistryImpl) getSessionRegistry();
sessRegistry.expireNow(leastRecentlyUsed.getSessionId());
}
*/
//expire all the sessions except this one.
for (int i = 0; i < sessions.length; i++)
{
if ( !sessions[i].getSessionId().equals(sessionId))
{
//this class expects the use of ClustereAwareSessionRegistryImpl .
if (getSessionRegistry() instanceof ClustereAwareSessionRegistryImpl)
{
ClustereAwareSessionRegistryImpl sessRegistry = (ClustereAwareSessionRegistryImpl) getSessionRegistry();
sessRegistry.expireNow(sessions[i].getSessionId());
}
}
}
}
}
Blog Archive
Search This Blog
Subscribe to:
Post Comments (Atom)
2 comments:
Hi, Can you share the full code and files for implementing the custom sessionregistry for handling sessions?
Thanks.
Post a Comment