Blog Archive
Search This Blog
Tuesday, March 3, 2009
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());
}
}
}
}
}
Sunday, February 8, 2009
Websphere JTA Transaction Configuration For Hibernate/spring
Websphere JTA Transaction Configuration
We use Websphere 6.0 and Spring 2.5 with Hibernate 3.0. I was struggling with getting to use JTA Transaction working for this environment, and finally got it working.
For the versions above WebSphere Application Server V6.0.2.19 or V6.1.0.9 the declaration of the transaction manager can be done with the following.
for those who are less fortunate and stuck with older versions there are various suggestions to get it working. Some solutions are bit risky since some hibernate classes uses (eg: org.hibernate.transaction.WebSphereTransactionManagerLookup) undocumented internal classes in the websphere App Server.
The safer approach is to use org.hibernate.transaction.WebSphereExtendedJTATransactionLookup with JTA transactions. But this has limitations. It doesn't support PROPAGATION_NOT_SUPPORTED and PROPAGATION_REQUIRES_NEW transaction attributes.
see following urls for detailed explanations and options.
http://robertmaldon.blogspot.com/2006/09/using-websphere-transaction-manager.html
http://www-128.ibm.com/developerworks/websphere/techjournal/0609_alcott/0609_alcott.html
I am listing down the solution worked for me here and the problems encountered.
The Spring configuration for transaction was declared as following.
transaction manager configuration<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="autodetectTransactionManager" value="false">
<property name="allowCustomIsolationLevels" value="true">
<property name="userTransactionName" value="java:comp/UserTransaction">
</bean>
<bean id="hibernateProperties"
class="org.springframework.beans.factory.config.PropertiesFactoryBean">
<property name="properties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.SQLServerDialect</prop>
<prop key="hibernate.show_sql">true</prop>
<prop key="hibernate.transaction.factory_class">org.hibernate.transaction.JTATransactionFactory</prop>
<prop key="hibernate.transaction.manager_lookup">org.hibernate.transaction.WebSphereExtendedJTATransactionLookup</prop>
<prop key="jta.UserTransaction">java:comp/UserTransaction</prop >
<prop key="hibernate.transaction.flush_before_completion">true</prop>
<prop key="hibernate.transaction.auto_close_session">true</prop>
</bean>
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<property name="mappingLocations">
<list>
<value>classpath*:**/../../model/State.hbm.xml</value>
<value>classpath*:**/../../Title.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<ref bean="hibernateProperties">
</property>
<property name="dataSource">
<ref bean="dataSource">
</property>
</bean>
<bean id="dataSource"
class="org.springframework.jndi.JndiObjectFactoryBean">
<property name="jndiName"><value>java:comp/env/jdbc/RRVTD</value></property>
</bean>
*When this is done application might return an error saying ...TransactionManager(websphere one) can't be assigned to
javax.transaction.UserTransaction.
It is a class loading problem: while spring loads the javax.TransactionManager interface definition from a jta.jar,
websphere loaded from an other one. Delete jta.jar from the classpath and it will work.*