cayenne-user mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From Andrus Adamchik <and...@objectstyle.org>
Subject Re: [OT] Shiro
Date Tue, 16 Oct 2012 16:02:00 GMT
On Oct 16, 2012, at 4:07 PM, Juan José Gil <matero@gmail.com> wrote:

> I would be glad to see that happen! :)


Here it goes:

1. We start modeling a session as a DB table that stores a bunch of common session attributes
in columns, and all the custom attributes in a BLOB. E.g.:

CREATE TABLE `db_session` (
 `uuid` varchar(36) collate utf8_bin NOT NULL,
 `attributes` mediumblob,
 `created_on` datetime NOT NULL,
 `host` varchar(200) collate utf8_bin default NULL,
 `last_accessed_on` datetime NOT NULL,
 `stopped_on` datetime default NULL,
 `timeout_ms` bigint(20) NOT NULL,
 PRIMARY KEY  (`uuid`)
)

2. We map this in Cayenne as any other entity. So 'db_session' table would result in DbSession
persistent object.

3. Customize Shiro runtime to stick 2 custom object to its SessionManager that I will show
below: CayenneSessionDAO and CayenneSessionFactory. Both should be using a third custom object
- CayenneSession. And there is a custom Shiro filter (not shown here). Shiro is a bit all
over the place when you start overriding stuff. So from here the example becomes wordy and
rather environment-specific. So you will have to wire that somehow in a way appropriate to
your environment, so brace yourself for some Shiro hacking.

We are using Tapestry5 that provides an HttpServletRequest proxy that can be called from a
singleton DAO. CayenneSessionDAO takes advantage of the request to store an uncommitted DbSession
instance that can be updated multiple times during the request, and only serialized and committed
once at the end of the request (via an explicit call from the custom ShiroFilter to 'flushRequestChanges').
You can use some other mechanism, like ThreadLocal, instead of HttpServletRequest proxy. 

Another neat feature of CayenneSession is a 'touch' method that skips changing the session
last access timestamp if the last update happened recently. This way we significantly reduce
the DB traffic (very helpful when say images and CSS are served from the app, and fall under
the same security domain as the main page that includes them).

IMO this is a bit too much code for what we are trying to achieve here. I considered offering
some refactoring ideas to Shiro developers, but just don't have enough time to follow up.
But one way or another Shiro does make this integration possible, so I am not complaining
:)

Andrus

(lots of code follows…)


------------
package foo.shiro;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;

import org.apache.cayenne.Cayenne;
import org.apache.cayenne.ObjectContext;
import org.apache.cayenne.exp.ExpressionFactory;
import org.apache.cayenne.query.SelectQuery;
import org.apache.shiro.session.Session;
import org.apache.shiro.session.UnknownSessionException;
import org.apache.shiro.session.mgt.eis.AbstractSessionDAO;

public class CayenneSessionDAO extends AbstractSessionDAO {


	private static final String REQUEST_CONTEXT_ATTRIBUTE = "CayenneSessionDAO.context";
	private static final String REQUEST_SESSIONS_ATTRIBUTE = "CayenneSessionDAO.sessions";

	private ICayenneService cayenneService;
	private HttpServletRequest request;

	CayenneSessionDAO(HttpServletRequest request, ICayenneService cayenneService) {
		this.request = request;
		this.cayenneService = cayenneService;
	}

	void flushRequestChanges() {
		Map<Serializable, CayenneSession> sessions = requestSessions(false);
		if (!sessions.isEmpty()) {

			for (CayenneSession session : sessions.values()) {
				session.save();
			}

			requestContext(false).commitChanges();
		}
	}

	ObjectContext requestContext(boolean create) {
		ObjectContext context = (ObjectContext) request.getAttribute(REQUEST_CONTEXT_ATTRIBUTE);

		if (context == null && create) {
			context = cayenneService.newContext();
			request.setAttribute(REQUEST_CONTEXT_ATTRIBUTE, context);
		}

		return context;
	}

	private Map<Serializable, CayenneSession> requestSessions(boolean create) {
		@SuppressWarnings("unchecked")
		Map<Serializable, CayenneSession> sessions = (Map<Serializable, CayenneSession>)
request
				.getAttribute(REQUEST_SESSIONS_ATTRIBUTE);

		if (sessions == null && create) {
			sessions = new HashMap<Serializable, CayenneSession>();
			request.setAttribute(REQUEST_SESSIONS_ATTRIBUTE, sessions);
		}

		if (sessions == null) {
			return Collections.emptyMap();
		} else {
			return sessions;
		}
	}

	private void storeSession(Serializable id, Session session) {
		if (id == null) {
			throw new NullPointerException("id argument cannot be null.");
		}

		// postponing DB updates till the end of request... hoping that
		// doesn't cause in any inconsistency in Shiro...
		requestSessions(true).put(id, (CayenneSession) session);
	}

	@Override
	protected Session doReadSession(Serializable sessionId) {

		CayenneSession existing = (CayenneSession) requestSessions(false).get(sessionId);
		if (existing != null) {
			return existing;
		}

		SelectQuery query = new SelectQuery(DbSession.class);
		query.andQualifier(ExpressionFactory.matchExp(DbSession.UUID_PROPERTY, sessionId));

		DbSession shiroSession = (DbSession) Cayenne.objectForQuery(requestContext(true), query);

		if (shiroSession != null) {

			// manually cache in request scope
			CayenneSession session = new CayenneSession(shiroSession, CayenneSessionFactory.DEFAULT_TOUCH_DRIFT_MS);
			requestSessions(true).put(sessionId, session);

			return session;
		}

		return null;
	}

	@Override
	public void update(Session session) throws UnknownSessionException {
		storeSession(session.getId(), session);
	}

	@Override
	public void delete(Session session) {
		if (session == null) {
			throw new NullPointerException("session argument cannot be null.");
		}

		Serializable id = session.getId();
		if (id != null) {

			requestSessions(false).remove(id);

			// unlike updates, process DB deletes immediately...
			ObjectContext context = cayenneService.newContext();
			((CayenneSession) session).delete(context);
			context.commitChanges();
		}
	}

	@Override
	public Collection<Session> getActiveSessions() {
		SelectQuery query = new SelectQuery(DbSession.class);
		query.andQualifier(ExpressionFactory.matchExp(DbSession.STOPPED_ON_PROPERTY, null));
		@SuppressWarnings("unchecked")
		List<DbSession> savedSessions = cayenneService.sharedContext().performQuery(query);
		List<Session> sessions = new ArrayList<Session>(savedSessions.size());

		for (DbSession s : savedSessions) {
			sessions.add(new CayenneSession(s, CayenneSessionFactory.DEFAULT_TOUCH_DRIFT_MS));
		}

		return sessions;
	}

	@Override
	protected Serializable doCreate(Session session) {
		Serializable sessionId = generateSessionId(session);
		assignSessionId(session, sessionId);
		storeSession(sessionId, session);
		return sessionId;
	}

	@Override
	protected void assignSessionId(Session session, Serializable sessionId) {
		((CayenneSession) session).setId(sessionId.toString());
	}

}


--------------
package foo.shiro;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.text.DateFormat;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

import org.apache.cayenne.ObjectContext;
import org.apache.shiro.session.ExpiredSessionException;
import org.apache.shiro.session.InvalidSessionException;
import org.apache.shiro.session.StoppedSessionException;
import org.apache.shiro.session.mgt.ValidatingSession;

public class CayenneSession implements ValidatingSession {

	private DbSession session;

	private Map<Object, Object> savedAttributes;
	private Map<Object, Object> attributes;
	private long touchDriftMs;

	public CayenneSession(DbSession session, long touchDriftMs) {
		this.session = session;
		this.touchDriftMs = touchDriftMs;

		// clone session attributes to be able to save against the baseline
		this.savedAttributes = toMap(session.getAttributes());
		this.attributes = new HashMap<Object, Object>(savedAttributes);
	}

	@SuppressWarnings("unchecked")
	private Map<Object, Object> toMap(byte[] bytes) {

		if (bytes == null || bytes.length == 0) {
			return new HashMap<Object, Object>();
		}

		try {
			return (Map<Object, Object>) new ObjectInputStream(new ByteArrayInputStream(bytes)).readObject();
		} catch (Exception e) {
			throw new RuntimeException("Error deserializing attributes", e);
		}
	}

	void delete(ObjectContext context) {
		DbSession localSession = context.localObject(session);
		context.deleteObjects(localSession);
	}

	void save() {

		// serialize and update 'attributes' property only when there are
		// differences... All other properties are already Cayenne-managed
		if (!savedAttributes.equals(attributes)) {

			ByteArrayOutputStream bytes = new ByteArrayOutputStream() {

				// avoid unneeded array copy...
				@Override
				public synchronized byte[] toByteArray() {
					return buf;
				}
			};

			try {
				ObjectOutputStream out = new ObjectOutputStream(bytes);
				out.writeObject(attributes);
				out.close();
			} catch (Exception e) {
				throw new RuntimeException("Error serializing attributes", e);
			}

			session.setAttributes(bytes.toByteArray());
		}
	}

	@Override
	public String getId() {
		return session.getUuid();
	}

	void setId(String id) {
		session.setUuid(id);
	}

	@Override
	public Date getStartTimestamp() {
		return session.getCreatedOn();
	}

	@Override
	public Date getLastAccessTime() {
		return session.getLastAccessedOn();
	}

	@Override
	public long getTimeout() throws InvalidSessionException {
		return session.getTimeoutMs();
	}

	@Override
	public void setTimeout(long maxIdleTimeInMillis) throws InvalidSessionException {
		session.setTimeoutMs(maxIdleTimeInMillis);
	}

	@Override
	public String getHost() {
		return session.getHost();
	}

	@Override
	public void touch() throws InvalidSessionException {

		Date now = new Date();

		// do not update last access timestamp very often to prevent large
		// amount of updates when a page loads multiple secure resources
		if (touchDriftMs <= 0 || session.getLastAccessedOn().getTime() + touchDriftMs < now.getTime())
{
			session.setLastAccessedOn(now);
		}
	}

	@Override
	public void stop() throws InvalidSessionException {
		if (session.getStoppedOn() == null) {
			session.setStoppedOn(new Date());
		}
	}

	@Override
	public Collection<Object> getAttributeKeys() throws InvalidSessionException {
		return Collections.unmodifiableCollection(attributes.keySet());
	}

	@Override
	public Object getAttribute(Object key) throws InvalidSessionException {
		return attributes.get(key);
	}

	@Override
	public void setAttribute(Object key, Object value) throws InvalidSessionException {
		attributes.put(key, value);
	}

	@Override
	public Object removeAttribute(Object key) throws InvalidSessionException {
		return attributes.remove(key);
	}

	@Override
	public boolean isValid() {
		return session.getStoppedOn() == null && !isTimedOut();
	}

	@Override
	public void validate() throws InvalidSessionException {

		if (session.getStoppedOn() != null) {
			String msg = "Session with id [" + getId() + "] has been "
					+ "explicitly stopped.  No further interaction under this session is " + "allowed.";
			throw new StoppedSessionException(msg);
		}

		if (isTimedOut()) {
			stop();

			// throw an exception explaining details of why it expired:
			Date lastAccessTime = getLastAccessTime();
			long timeout = getTimeout();

			Serializable sessionId = getId();

			DateFormat df = DateFormat.getInstance();
			String msg = "Session with id [" + sessionId + "] has expired. " + "Last access time: "
					+ df.format(lastAccessTime) + ".  Current time: " + df.format(new Date())
					+ ".  Session timeout is set to " + timeout / 1000 + " seconds (" + timeout / 60000 +
" minutes)";
			throw new ExpiredSessionException(msg);
		}

	}

	private boolean isTimedOut() {

		long timeout = getTimeout();

		if (timeout >= 0l) {

			Date lastAccessTime = getLastAccessTime();

			if (lastAccessTime == null) {
				String msg = "session.lastAccessTime for session with id [" + getId()
						+ "] is null.  This value must be set at "
						+ "least once, preferably at least upon instantiation.  Please check the "
						+ getClass().getName() + " implementation and ensure "
						+ "this value will be set (perhaps in the constructor?)";
				throw new IllegalStateException(msg);
			}

			long expireTimeMillis = System.currentTimeMillis() - timeout;
			Date expireTime = new Date(expireTimeMillis);
			return lastAccessTime.before(expireTime);
		}

		return false;
	}

}

-----
package foo.shiro;

import java.util.Date;

import org.apache.shiro.session.Session;
import org.apache.shiro.session.mgt.DefaultSessionManager;
import org.apache.shiro.session.mgt.SessionContext;
import org.apache.shiro.session.mgt.SessionFactory;


public class CayenneSessionFactory implements SessionFactory {

	static final long DEFAULT_TOUCH_DRIFT_MS = 20000;

	private CayenneSessionDAO sessionDAO;

	public CayenneSessionFactory(CayenneSessionDAO sessionDAO) {
		this.sessionDAO = sessionDAO;
	}

	@Override
	public Session createSession(SessionContext initData) {

		String host = null;
		if (initData != null) {
			host = initData.getHost();
		}

		DbSession session = sessionDAO.requestContext(true).newObject(DbSession.class);
		session.setTimeoutMs(DefaultSessionManager.DEFAULT_GLOBAL_SESSION_TIMEOUT);
		session.setHost(host);
		session.setCreatedOn(new Date());
		session.setLastAccessedOn(session.getCreatedOn());

		return new CayenneSession(session, DEFAULT_TOUCH_DRIFT_MS);
	}
}

Mime
View raw message