You are viewing a plain text version of this content. The canonical link for it is here.
Posted to user@cayenne.apache.org by Andrus Adamchik <an...@objectstyle.org> on 2012/10/16 13:07:18 UTC

[OT] Shiro

On Oct 16, 2012, at 12:49 PM, Malcolm Edgar <ma...@gmail.com> wrote:

> Please also look at Apache Shiro, it provides very nice wrappers around JCE and incorporates crypto best practices.
> 
> Regards Malcolm Edgar
> 
> Sent from my iPad

Talking of Shiro, not so long ago our company implemented CayenneSessionDAO for Shiro. It allows to do Cayenne-based login session persistence and hence enable SSO across apps via Shiro. This code is not open source, but I can probably post an open source example if anyone's interested.

Andrus

Re: [OT] Shiro

Posted by Andrus Adamchik <an...@objectstyle.org>.
On Oct 16, 2012, at 4:07 PM, Juan José Gil <ma...@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);
	}
}

Re: [OT] Shiro

Posted by Juan José Gil <ma...@gmail.com>.
I would be glad to see that happen! :)


2012/10/16 Andrus Adamchik <an...@objectstyle.org>

> On Oct 16, 2012, at 12:49 PM, Malcolm Edgar <ma...@gmail.com>
> wrote:
>
> > Please also look at Apache Shiro, it provides very nice wrappers around
> JCE and incorporates crypto best practices.
> >
> > Regards Malcolm Edgar
> >
> > Sent from my iPad
>
> Talking of Shiro, not so long ago our company implemented
> CayenneSessionDAO for Shiro. It allows to do Cayenne-based login session
> persistence and hence enable SSO across apps via Shiro. This code is not
> open source, but I can probably post an open source example if anyone's
> interested.
>
> Andrus