Spring Data, Spring Security and Envers integration
Learn about pros, cons, and basics of Spring security and data, plus Envers integration.
Join the DZone community and get the full member experience.
Join For FreeSpring Data JPA, Spring Security and Envers are libraries that I personally enjoy working with (and I tend to think they are considered best-of-breed in their respective category). Anyway, I wanted to implement what I consider a simple use-case: entities have to be Envers-audited but the revision has to contain the identity of the user that initiated the action. Although it seem simple, I had some challenges to overcome to achieve this. This article lists them and provide a possible solution.
Software architecture
I used Spring MVC as the web framework, configured to use Spring Security. In order to ease my development, Spring Security was configured with the login/password to recognize. It's possible to connect it to a more adequate backend easily. In the same spirit, I use a datasource wrapper around a driver manager connection on a H2 memory database. This is simply changed by Spring configuration.A typical flow is handled by my Spring MVC Controller, and passed to the injected service, which manages the Spring Data JPA repository (the component that accesses the database).
Facts
Here are the facts that have proven to be a hindrance (read obstacles to overcome) during this development:
- Spring Security 3.1.1.RELEASE (latest) uses Spring 3.0.7.RELEASE (not latest)
- Spring Data JPA uses (wait for it) JPA 2. Thus, you have to parameterize your Spring configuration with LocalContainerEntityManagerFactoryBean, then configure the EntityManager factory bean with the implementation to use (Hibernate in our case). Some passed parameters are portable across different JPA implementations, other address Envers specifically and are thus completely non-portable.
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="jpaDialect"> <bean class="org.springframework.orm.jpa.vendor.HibernateJpaDialect" /> </property> </bean> <bean class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"> <property name="dataSource" ref="dataSource" /> <property name="jpaVendorAdapter"> <bean class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> <property name="generateDdl" value="true" /> <property name="database" value="H2" /> <property name="showSql" value="true" /> </bean> </property> <property name="jpaProperties"> <props> <prop key="org.hibernate.envers.auditTablePrefix"></prop> <prop key="org.hibernate.envers.auditTableSuffix">_HISTORY</prop> </props> </property> </bean>
- Spring Data JPA does provide some auditing capabilities in the form of the Auditable interface. Auditables have createdBy, createdDate, modifiedBy and modifiedDateproperties. Unfortunately, the library doesn't store previous state of the entities: we do have to use Envers that provide this feature
- After having fought like mad, I realized integrating Envers in Spring Data JPA was no small potatoes and stumbled upon the Spring Data Envers module, which does exactly that job. I did founf no available Maven artifacts in any repository, so I cloned the Git repo and built the current version (0.1.0.BUILD-SNAPSHOT then). It provided me with the opportunity to give a very modest contribution to the project.
On the bright side, and despite all examples I googled, the latest versions of Hibernate not only are shipped with Envers, but there's no configuration needed to register Envers listeners, thanks to a smart service provider use. You only need to provide the JAR on the classpath, and the JAR itself takes care of registration. This makes Envers integration much simpler.
How To
Beyond adding Spring Data Envers, there's little imagination to have, as it's basic Envers use.
- Create a revision entiy that has the needed attribute (user):
@RevisionEntity(AuditingRevisionListener.class) @Entity public class AuditedRevisionEntity extends DefaultRevisionEntity { private static final long serialVersionUID = 1L; private String user; public String getUser() { return user; } public void setUser(String user) { this.user = user; } }
- Create the listener to get the identity from the Spring Secuirty context and set is on the revision entity:
public class AuditingRevisionListener implements RevisionListener { @Override public void newRevision(Object revisionEntity) { AuditedRevisionEntity auditedRevisionEntity = (AuditedRevisionEntity) revisionEntity; String userName = SecurityContextHolder.getContext().getAuthentication().getName(); auditedRevisionEntity.setUser(userName); } }
Presto, you're done...
Bad News
... Aren't you? The aforementioned solution works perfectly when writing to the database. The rub comes from trying to get the information once it's there, because the Spring Data API doesn't allow that (yet?). So, there are basically three options (assuming you care about retrieval: this kind of information may not be for the user to see, and you can always connect to the database to query when -if - you need to access the data):
- Create code to retrieve the data. Obviously, you cannot use Spring Data JPA (your entity is missing the userfield, it's "created" at runtime). The best way IMHO would be to create a listener that get data on read queries for the audited entity and returns an enhanced entity. Performance as well as design considerations aren't in favor of this one.
- Hack the API, using reflection that make it dependent on internals of Spring Data. This is as bad an option as the one before, but I did it for educational purposes:
Revisions<Integer, Stuff> revisions = stuffRepository.findRevisions(stuff.getId()); List<AuditedRevisionEntity> auditedRevisionEntities = new ArrayList<AuditedRevisionEntity>(); for (Revision<Integer, Stuff> revision : revisions.getContent()) { Field field = ReflectionUtils.findField(Revision.class, "metadata"); // Oh, it's ugly! ReflectionUtils.makeAccessible(field); @SuppressWarnings("rawtypes") RevisionMetadata metadata = (RevisionMetadata) ReflectionUtils.getField(field, revision); AuditedRevisionEntity auditedRevisionEntity = (AuditedRevisionEntity) metadata.getDelegate(); // Do what your want with auditedRevisionEntity... }
- Last but not least, you can contribute to the code of Spring Data. Granted, you'll need knowledge of the API and the internals, skills and time, but it's worth it :-)
Conclusion
Integrating heterogeneous libraries is hardly a walkover. In the case of Spring Data JPA and Envers, it's as easy as pie thanks to the Spring Data Envers library. However, if you need to make your audit data accessible, you need to integrate further.
The sources for this article are here, in Eclipse/Maven format.
From http://blog.frankel.ch/spring-data-spring-security-and-envers-integration
To go further:
- The Spring Data Envers project on Github
- Hibernate Envers site
- Spring Data JPA site
- Finally, Spring Security
Opinions expressed by DZone contributors are their own.
Comments