Tuesday, November 24, 2009

Using CAS 4 within a Load Balanced Environment

My journey begins with HA which means High Availability.
I'm not crazy about acronyms because given the variety of words in the english language they can mean any combination of things.

 I have 2  main requirements/constraints when running my website:
1) Always be load balanced for High Availability,redundancy and also allow easy scalability.
2) Allow use Hibernate across multiple databases. I have a separate database for users (user db), and a separate database for the core of the web application.

My website is load balanced using Apache, I have a single load balance server , and 2 web servers also running Apache.
They get load balanced via Apache ProxyReverse or mod_proxy module.

I added a third requirement to my web application, always allow the users to sign in once and sign in with one set of credentials (username/password).
Another words, don't have separate sign up, sign in processes for third party applications like forums that you have tacked onto to your site.

This third requirement is straight forward and may appear easy to some, but it can be complicated and time consuming to implement.
Luckily, the folks at jasig.org , created CAS , Central Authentication Service, which can be implemented into one's website.
It is Java based, but can be integrated with lots of different website technology stacks like .Net, PHP, Perl, ColdFusion, Ruby on Rails, etc.

Combining these 3 requirements means that I need to be able to have CAS load balanced across multiple servers and multiple databases.
The standard implementation of CAS stores a user's credentials in the session. However, when CAS is implemented this way, the web site
ends up losing the credentials on redirects, etc.

To solve this issue CAS 4 (Cas version 4) introduced the concept of Database Session Storage named JpaSessionStorage in CAS.
Due to requirement #2, I had to implement my own Session Storage class that utilizes Spring's JtaTransactionManager rather EntityManager.
This allows for managing transactions over multiple databases.

I created a class called: JpaSessionStorageUsingJtaImpl within the package:org.jasig.cas.server.session, and part of the cas 4 project: cas-server-sessionstorage-jpa.
Here is my code:
package org.jasig.cas.server.session;

import org.springframework.test.context.transaction.TransactionConfiguration;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;
import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import org.javalid.annotations.validation.CollectionSize;
import org.javalid.annotations.validation.NotNull;
import org.javalid.annotations.validation.MinValue;
import org.jasig.cas.server.authentication.Authentication;
import org.jasig.cas.server.login.LoginRequest;
import org.jasig.cas.server.util.Cleanable;


import org.hibernate.Query;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.Transaction;

import javax.annotation.Resource;
import javax.persistence.*;

import java.util.ArrayList;
import java.util.List;

/***
* Session Storage Implementation that uses Spring's a JtaTransactionManager rather
* than EntityManager. Allows for managing transactions over multiple databases which
* is required in some web application implementations.
* @author David Driscoll
*
*/
@TransactionConfiguration(transactionManager="jtaTransactionManager", defaultRollback=false)
@Transactional
@Repository
/*public class JpaSessionStorageUsingJtaImpl extends AbstractSessionStorageImpl implements Cleanable{*/
public class JpaSessionStorageUsingJtaImpl implements SessionStorage, Cleanable{
@Resource(name="sessionFactory3")
private org.hibernate.SessionFactory sessionFactory;

@NotNull
@CollectionSize(mode=CollectionSize.MODE_MINIMUM, minimumSize = 1)
@Autowired(required=false)
private List sessionFactories = new ArrayList();

/*@Override
@Autowired
protected void setSessionFactories(List sessionFactories)
{
this.sessionFactories = sessionFactories;
}*/

@MinValue(1)
private long purgeTimeOut = 21600000;

@MinValue(1)
private int purgeMaxCount = Integer.MAX_VALUE;


public JpaSessionStorageUsingJtaImpl(){}

@Autowired(required=true)
public JpaSessionStorageUsingJtaImpl(List sessionFactories) {
this.sessionFactories = sessionFactories;
}

public org.jasig.cas.server.session.Session createSession(
LoginRequest loginRequest, Authentication authentication)
throws InvalidatedSessionException {
try{
if (loginRequest.getOriginalAccess() != null && loginRequest.getOriginalAccess().getParentSession() != null) {
org.jasig.cas.server.session.Session parentSession = loginRequest.getOriginalAccess().getParentSession();
org.jasig.cas.server.session.Session childSession = parentSession.createDelegatedSession(authentication);
addSession(childSession);
return childSession;
}

if(this.sessionFactories != null){
for ( org.jasig.cas.server.session.SessionFactory sessionFactory : this.sessionFactories) {
org.jasig.cas.server.session.Session session = sessionFactory.getSession(authentication);

if (session != null) {
addSession(session);
return session;
}
}
}

throw new IllegalStateException("No SessionFactory configured that can handle this type of Authentication.");
}
catch(Exception ex){
System.out.println("exception in createSession, exception message:" + ex.getMessage());
throw new IllegalStateException("No SessionFactory configured that can handle this type of Authentication.");
}
}

protected void addSession(org.jasig.cas.server.session.Session session) {
Assert.isInstanceOf(JpaSessionImpl.class, session);
this.sessionFactory.getCurrentSession().persist(session);
}

public org.jasig.cas.server.session.Session destroySession(String sessionId) {
try {

Query query = this.sessionFactory.getCurrentSession().createQuery("select s from session s where s.sessionId = :sessionId").setParameter("sessionId", sessionId);
org.jasig.cas.server.session.Session session = (org.jasig.cas.server.session.Session) query.uniqueResult();
this.sessionFactory.getCurrentSession().delete(session);
return session;
} catch (Exception e) {
return null;
}
}

public org.jasig.cas.server.session.Session findSessionBySessionId(String sessionId) {
try {
Query query = this.sessionFactory.getCurrentSession().createQuery("select s from session s where s.sessionId = :sessionId").setParameter("sessionId", sessionId);
org.jasig.cas.server.session.Session session = (org.jasig.cas.server.session.Session) query.uniqueResult();
return session;
} catch ( Exception e) {
return null;
}
}

public org.jasig.cas.server.session.Session updateSession(org.jasig.cas.server.session.Session session) {
this.sessionFactory.getCurrentSession().update(session);
return session;
}

public org.jasig.cas.server.session.Session findSessionByAccessId(String accessId) {
try {
Query query = this.sessionFactory.getCurrentSession().
createQuery("select s from session s, IN(s.casProtocolAccesses) c where c.id = :accessId").setParameter("accessId", accessId);
org.jasig.cas.server.session.Session session = (org.jasig.cas.server.session.Session) query.uniqueResult();
return session;

} catch ( Exception e) {
return null;
}
}

public void purge() {
Query query = this.sessionFactory.getCurrentSession().
createQuery("delete from session s");
query.executeUpdate();
}

public void prune() {

Query query = this.sessionFactory.getCurrentSession().
createQuery("Delete From session s where (s.state.creationTime + :timeOut) >= :currentTime or s.state.count > :maxCount").
setParameter("timeOut", this.purgeTimeOut).setParameter("currentTime", System.currentTimeMillis()).setParameter("maxCount", this.purgeMaxCount);
query.executeUpdate();
}

public void setPurgeTimeOut(long purgeTimeOut) {
this.purgeTimeOut = purgeTimeOut;
}

public void setPurgeMaxCount(int purgeMaxCount) {
this.purgeMaxCount = purgeMaxCount;
}


You will need to setup your JTA configuration for your CAS 4 datasource.
See my series of blog posts on setting up Spring with Multiple Databases to setup JTA appropriately, here : http://endurotracker.blogspot.com/2009/08/using-spring-with-multiple-databases.html
CAS 4 uses utilizes autowiring annotations, so you only need to reference this new class in your pom.xml for it to be found and used as the session storage mechanism.
Here is a xml snippet from the pom.xml file:
<dependency>
<groupId>org.jasig.cas</groupId>
<artifactId>cas-server-sessionstorage-jpa</artifactId>
<version>${project.version}</version>
<scope>runtime</scope>
<type>jar</type>
</dependency>
Using CAS 4 within a Load Balanced Environment Completed!

No comments:

Post a Comment