In mountain biking and kayaking, the most thrilling moves are running ledges. It doesn’t start that way in either sport. With kayaks, you tend to do a whole lot of work to set yourself up for a few seconds of freefall. You line up against a landmark, stroke hard to build speed, push your body forward, and then simultaneously thrust with your hips and paddle off of the edge, which launches you. With bikes, you tend to pick a line through that won’t smash your chain ring, line your bike up, balance yourself behind the saddle so you don’t fly over the handlebars, roll forward slowly, compress your shocks, jump up to lighten the bike, and hope for the best while you’re falling.
Like most other services in Spring, you can use transactions programmatically or declaratively. When you’re using a new service, it often makes sense to understand that service programmatically within a method before you break it out as a declarative service.
Sometimes, you want to hide services, and Spring certainly makes it easy to do so. Other times, you want to be explicit, such as when transactional logic is the central job of your method. Spring makes it easy to do transaction logic programmatically.
You’ll use the same mechanism for transactions as
you do for JDBC: templates. For programmatic transactions,
you’ll use a TransactionTemplate
.
You’ll put
all
of the code in the transaction body that you want to execute
together.
In this case, imagine that a customer wants to cancel her current
reservation and make a new one for later in the week. If the new
reservation fails for any reason, she wants to keep her old one.
You’ll add a method to RentABike
called transferReservation
. It will delete an
existing reservation and create a new one (Example 7-1).
public void transferReservation(final Reservation oldRes, final Reservation newRes) throws ReservationTransferException { TransactionTemplate template = new TransactionTemplate(this.transactionManager); template.setPropagationBehavior( TransactionDefinition.PROPAGATION_REQUIRED); try { template.execute(new TransactionCallbackWithoutResult( ) { protected void doInTransactionWithoutResult( TransactionStatus transactionStatus) { getHibernateTemplate( ).save(newRes); getHibernateTemplate( ).delete(oldRes); } }); } catch (Exception ex) { throw new ReservationTransferException( ); } }
You’ll need to set up the transaction strategy for
the template in the context, and create the accounts and the
RentABike
(Example 7-2).
<beans> <bean id="sessionFactory" class="org.springframework.orm.hibernate.LocalSessionFactoryBean"> <property name="dataSource"><ref local="dataSource"/></property> <property name="mappingResources"> <list> <value>com/springbook/Bike.hbm.xml</value> <value>com/springbook/Customer.hbm.xml</value> <value>com/springbook/Reservation.hbm.xml</value> <value>com/springbook/LogEvent.hbm.xml</value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect"> net.sf.hibernate.dialect.MySQLDialect </prop> <prop key="hibernate.show_sql">false</prop> </props> </property> </bean> <bean id="transactionManager" class="org.springframework.orm. hibernate.HibernateTransactionManager"> <property name="sessionFactory"> <ref local="sessionFactory"/> </property> </bean> <bean id="rentaBike" class="com.springbook.HibRentABike"> <property name="storeName"> <value>Bruce's Bikes</value> </property> <property name="sessionFactory"> <ref local="sessionFactory"/> </property> <property name="transactionManager"> <ref local="transactionManager"/> </property> </bean> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName"> <value>com.mysql.jdbc.Driver</value> </property> <property name="url"> <value>jdbc:mysql://localhost/rentabike</value> </property> <property name="username"><value>bikestore</value></property> </bean> </beans>
Take special care. Because you are using MySQL as the database for
this example, you’ll need one final step to get it
to work. In order to support transactions, you have to mark your
tables in MySQL as InnoDB
type, which lets MySQL
be transactional, with full ACID semantics.
A template is a simple, default transaction-handling method. All of the boilerplate code is in the template. You just have to implement the method that does all of the work. It will either all succeed or roll back.
You’re looking for two things: a pluggable architecture, and leverage. Of course, Hibernate transactions need only a command or two. Think about a native implementation. If you wanted to replace the Hibernate transactions with JTA transactions (to coordinate with changes in another database), you’d have to replace your implementation throughout your code, and you’d have to do much more work.
With your Spring implementation, you need only change configuration. The template gives you exactly what you want: a place to specify what needs to happen together successfully for the transaction to succeed.
Now, you’ll see the most useful type of transaction support: declarative transactions. It’s time to go back and explain the configuration of the transactions on your façade. In Chapter 4, you configured the transactions without explanation. It’s time to go back and fill in the holes.
Declarative transactions are perhaps the most important feature of EJB session beans. Many developers decide not to use EJB at all when they discover that you can in fact use declarative transactions without sacrificing your first-born child.
You will implement declarative transactions without ever touching the code. You only need to change the context. Just like the examples in the previous chapter, you will configure an interceptor. The interceptor defines the methods to be marked as transactional and defines their expected behavior. Some of the methods will use full transaction propagation, and some will use a lighter-weight, read-only propagation (Example 7-3).
<bean name="transactionInterceptor" class="org.springframework. transaction.interceptor.TransactionInterceptor"> <property name="transactionManager"> <ref local="transactionManager"/> </property> <property name="transactionAttributeSource"> <value> com.springbook.RentABike.transferReservation= PROPAGATION_REQUIRED,-ReservationTransferException com.springbook.RentABike.save*=PROPAGATION_REQUIRED com.springbook.RentABike.*=PROPAGATION_REQUIRED,readOnly </value> </property> </bean>
In the definition of the behavior for the
transferReservation
method, notice that you pass
the required transaction flag and the name
of a checked exception. This tells your interceptor to roll back the
transaction if an exception of that type is thrown; otherwise, not.
Next, you’ll build a proxy that specifies the target bean, like in Example 7-4.
<bean id="rentaBike" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces"> <value>com.springbook.RentABike</value> </property> <property name="interceptorNames"> <value>transactionInterceptor,rentaBikeTarget</value> </property> </bean>
The execution remains unchanged.
Think of this bean as the behavior in the original target, plus all of the aspects that you’ve added. In this case, the rentaBike is a transactional version of the target.
You just configured declarative transactions on a POJO. When any of the methods that we specified starts, the transaction advisor will tell the underlying transaction strategy to start the transaction. When the method successfully completes, the advisor will commit the transaction. If there’s an exception, the advisor will roll back. That’s exactly the behavior that you’re looking for.
Java Transaction API, or JTA? So far, you’ve used lightweight transactions. The nice thing about Spring is that you can change the transaction strategy without rewriting any code. Spring fully supports JTA, so you can use it to span multiple databases or perform distributed transactions (called XA transactions).
XA transactions may be more powerful than the alternatives, but you don’t always need them. In fact, there are at least two good reasons to use something else:
You may need individual features in a more direct transactional API, such as the isolation level support from JDBC transactions. In that case, XA transactions won’t work, because they don’t provide domain-specific abstractions.
You might need better performance. If XA transactions are overkill
(such as applications that only access a single database), then XA
transactions are likely to be much slower than an alternative, such
as HibernateTransactions
.
JTA tends to be the best fit when you’re using multiple resources. If your application needs to commit data from the same transaction across multiple databases, then JTA will likely be your best fit.
If you have to refactor a simple application to use multiple resources, Spring’s pluggable transaction strategies can save you a whole lot of effort. In this example, you’re going to use JTA transactions to span multiple databases. For this example to work, your application must be running in a JTA-aware J2EE container.
If you’re using some kind of agile programming method, you’ll likely run into this kind of scenario. Agile methods suggest that you take simple approaches to a problem, and implement more complex features only when they’re needed. Dependency injection makes it much easier to take slightly different versions of application resources and plug them into a given application without major upheaval.
In this case, you’re going to maintain monetary
transaction information in a separate database,
rentaBikeAccounts
. Whenever users make a
reservation, they have to provide a down payment, and
you’ll transactionally add this amount to the
monetaryTransactions
table in the new database.
Example 7-5 is the script for setting up the new
database and table.
create database rentaBikeAccounts; use rentabikeAccounts; create table monetaryTransactions ( txId int(11) not null auto_increment, resId int(11) not null default '0', amount double not null default '0', `type` varchar(50) not null, primary key (txId)) type=InnoDB;
Don’t forget that you’ll need to set up a user account in MySQL with privileges on the new database:
GRANT ALL PRIVILEGES ON rentaBikeAccounts.* TO 'rentaBikeAccounts'@'localhost' WITH GRANT OPTION;
We just created a user named rentaBikeAccounts, with complete access to the new database.
Next, Example 7-6 creates a persistent domain class.
public class MonetaryTransaction { private int txId; private int resId; private double amount; public int getTxId( ) { return txId; } public void setTxId(int txId) { this.txId = txId; } public int getResId( ) { return custId; } public void setResId(int resId) { this.resId = resId; } public double getAmount( ) { return amount; } public void setAmount(double amount) { this.amount = amount; } public MonetaryTransaction(double amount, int resid) { this.resId = resid; this.amount = amount; this.txId = -1; } public MonetaryTransaction( ) { this(0.0, 0); } }
Map the schema to the class, as in Example 7-7.
<hibernate-mapping> <class name="com.springbook.MonetaryTransaction" table="monetaryTransactions"> <id name="txId" column="txid" type="java.lang.Integer" unsaved-value="-1"> <generator class="native"></generator> </id> <property name="resId" column="resid" type="int"/> <property name="amount" column="amount" type="double"/> </class> </hibernate-mapping>
To access this new database, you have to configure a second data
source and SessionFactory
in your application
configuration. Session factories handle access for to a single
database, so you need a new SessionFactory
for the
second one (Example 7-8).
<bean id="dataSourceForAccounts" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName"> <value>com.mysql.jdbc.Driver</value> </property> <property name="url"> <value>jdbc:mysql://localhost/justbikes</value> </property> <property name="username"><value>rentaBikeAccounts</value></property> </bean> <bean id="sessionFactoryForAccounts" class="org.springframework.orm.hibernate.LocalSessionFactoryBean"> <property name="dataSource"><ref local="dataSourceForBikes"/></property> <property name="mappingResources"> <list> <value>com/springbook/MonetaryTransaction.hbm.xml</value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect"> net.sf.hibernate.dialect.MySQLDialect </prop> <prop key="hibernate.show_sql">false</prop> </props> </property> </bean>
Unfortunately, you can’t use this new
SessionFactory
directly within the
HibRentABike
implementation.
That’s because HibRentABike
extends the Spring-provided HibernateDaoSupport
class, which uses a single SessionFactory
as the
backing behind getHibernateTemplate( )
. As such,
you need a second façade class, this one to model access
to this new database through the new
SessionFactory
(Examples Example 7-9 and Example 7-10).
public interface Accounts { void addTx(MonetaryTransaction tx); MonetaryTransaction getTx(int id); List getTxs( ); }
public class RentABikeAccounts extends HibernateDaoSupport implements Accounts { public void addTx(MonetaryTransaction tx) { getHibernateTemplate( ).saveOrUpdate(tx); } public MonetaryTransaction getTx(int id) { return (MonetaryTransaction)getHibernateTemplate( ). load(MonetaryTransaction.class, new Integer(id)); } public List getTxs( ) { return getHibernateTemplate( ).find("from MonetaryTransaction"); } }
You need to configure this façade in your application configuration (Example 7-11).
<bean id="rentaBikeAccountsTarget" class="com.springbook.RentABikeAccounts"> <property name="sessionFactory"> <ref local="sessionFactoryForAccounts"/> </property> </bean> <bean id="rentaBikeAccounts" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces"> <value>com.springbook.Accounts</value> </property> <property name="interceptorNames"> <value>transactionInterceptor,rentaBikeAccountsTarget</value> </property> </bean>
Finally, use this new functionality within the
RentABike
façade, which means
you’ll need to add a setter to
RentABike
and wire them together in the
application context. Example 7-12 shows the additions
to HibRentABike
.
private Accounts accountsFacade; public Accounts getAccountsFacade( ) { return accountsFacade; } public void setAccountsFacade(Accounts accountsFacade) { this.accountsFacade = accountsFacade; } public void addReservation(Reservation reservation, double amount) throws AddReservationException { try { MonetaryTransaction tx = new MonetaryTransaction(amount, reservation.getReservationId( )); getHibernateTemplate( ).saveOrUpdate(reservation); accountsFacade.addTx(tx); } catch (Exception ex) { throw new AddReservationException( ); } }
Example 7-13 shows the additions to the configuration file.
<bean id="rentaBikeTarget" class="com.springbook.HibRentABike"> <property name="storeName"><value>Bruce's Bikes</value></property> <property name="sessionFactory"><ref local="sessionFactory"/></property> <property name="accountsFacade"> <ref local="rentaBikeAccounts"/> </property> </bean> <bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager"> </bean> <bean name="transactionInterceptor" class="org.springframework.transaction.interceptor.TransactionInterceptor"> <property name="transactionManager"> <ref local="transactionManager"/> </property> <property name="transactionAttributeSource"> <value> com.springbook.RentABike.transferReservation=PROPAGATION_REQUIRED, -ReservationTransferException com.springbook.RentABike.addReservation=PROPAGATION_REQUIRED, -AddReservationException </value> </property> </bean>
After adding a second data source, you were able to quickly add XA
transactional functionality to the application. Now, whenever the
HibRentABike.addReservation( )
method is called,
the application transactionally adds a reservation to the original
database and a MonetaryTransaction
to the new
database. You added a few new classes to the domain model, but mostly
this was accomplished through configuration settings.
Spring’s Hibernate and JTA support handle most of
the heavy lifting, and now the application handles multiple physical
data stores inside a single distributed transaction.
Declarative transactions are the most popular declarative services in Spring, but not the only ones. Spring also allows declarative remoting and declarative security. In this example, you’ll use a declarative security service called ACEGI.
Since ACEGI is a declarative service, you can use it to secure any method on any bean in the context, without writing extra supportive code. ACEGI also has advanced features that are well beyond most EJB implementations, including the following:
You can access many of ACEGI’s features declaratively. Your POJOs will not be overwhelmed with security code.
First, you’ll use
the servlet-based
implementation of ACEGI. This approach uses servlet filters.
You’ll configure it through a combination of
additions to the Spring and web.xml
configurations. To get going with ACEGI, download the latest version
from their web site (http://acegisecurity.sourceforge.net/downloads.html).
We used Version 0.6.1. From the /dist
folder, copy
acegi-security-catalina-server.jar
into the
/server/lib
folder of your Tomcat install. You
will also need available to your web application
aopalliance.jar
, spring.jar
,
and acegi-security-catalina-common.jar
, and
acegi-security.jar
.
Your first implementation will used forms-based authentication to
verify users. You have to create a login form that users will be
redirected to whenever they attempt to access any of our restricted
site pages. This file will be called
acegilogin.jsp
. Its only requirements are that it
be a non-restricted page and that it contain a username field and a
password field with well-known identifiers. Example 7-14 is acegilogin.jsp
.
<%@ taglib prefix='c' uri='http://java.sun.com/jstl/core' %> <%@ page import="net.sf.acegisecurity.ui.AbstractProcessingFilter, net.sf.acegisecurity.ui.webapp.HttpSessionIntegrationFilter" %> <%@ page import="net.sf.acegisecurity.AuthenticationException" %> <h1>Login</h1> <form action="j_acegi_security_check" method="POST"> Username: <input type="text" name="j_username"><br/> Password: <input type="password" name="j_password"><br/> <input type="submit" value="Login"> </form> <c:choose> <c:when test="${not empty param.error}"> <font color="red"> Your login attempt was not successful, try again.<BR><BR> Reason: <%= ((AuthenticationException) session.getAttribute( AbstractProcessingFilter.ACEGI_SECURITY_LAST_EXCEPTION_KEY)). getMessage( ) %> </font> </c:when> </c:choose>
You should add a special section at the end to display login errors
if the user enters invalid credentials. Note that the
username
field must be called
j_username
and the password field must be
j_password
. The ACEGI standard action for the form
is j_acegi_security_check
, though you could rename
it to whatever you like.
Next you have to configure the ACEGI security beans in your Spring
configuration (Example 7-15). Although ACEGI provides
several ways to authenticate user credentials (via LDAP, a database,
external provider, and so on), the easiest way is through a special
provider called the InMemoryDaoImpl
. This allows
you to configure the user credentials in your configuration file.
<bean id="inMemoryDaoImpl" class="net.sf.acegisecurity.providers.dao.memory.InMemoryDaoImpl"> <property name="userMap"> <value> justin=gehtland,ROLE_USER,ROLE_ADMIN bruce=tate,ROLE_USER </value> </property> </bean>
Configure users with username to the left of the equals and a comma-separated list of values to the right. Put the password first. Remaining values are roles for the user. You can choose to use an ACEGI-provided password encoding scheme here that employs MD5 hashing.
To finish the authentication configuration, you have to wrap the
InMemoryDaoImpl
in a
DaoAuthenticationProvider
, then feed that provider
to a ProviderManager
, as in Example 7-16.
<bean id="daoAuthenticationProvider" class="net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider"> <property name="authenticationDao"> <ref local="inMemoryDaoImpl"/> </property> </bean> <bean id="authenticationManager" class="net.sf.acegisecurity.providers.ProviderManager"> <property name="providers"> <list> <ref local="daoAuthenticationProvider"/> </list> </property> </bean>
The DaoAuthenticationProvider
ACEGI class
implements ACEGI’s
AuthenticationProvider
interface. It exposes
authentication methods like authenticate
and
isPasswordCorrect
. The
ProviderManager
simply iterates through a list of
AuthenticationProviders
to authenticate a given
request. In rentaBike
, you only have
DaoAuthenticationProvider
.
Next, you need to tell ACEGI how to determine if a user is authorized
to make a specific call (Example 7-17). ACEGI has the
notion of DecisionManagers
and
Voters
. Voters
are beans that
examine credentials and cast votes about access;
DecisionManagers
collect votes from
Voters
and determine outcomes. Although you can
author your own Voters
and
DecisionManagers
, ACEGI provides a straightforward
approach through pre-built classes, the RoleVoter
and the AffirmativeBasedDecisionManager
.
<bean id="roleVoter" class="net.sf.acegisecurity.vote.RoleVoter"/> <bean id="httpRequestAccessDecisionManager" class="net.sf.acegisecurity.vote.AffirmativeBased"> <property name="allowIfAllAbstainDecisions"><value>false</value> </property> <property name="decisionVoters"> <list> <ref local="roleVoter"/> </list> </property> </bean>
The RoleVoter
requires that the roles be
configured using some role beginning with ROLE_
,
like our ROLE_USER role above. The
AffirmativeBased
DecisionManager
asks each configured
Voter
for its vote, and then permits access if any
vote “yes”.
Next, you need to configure a
FilterSecurityInterceptor
that protects access to
resources. You configure it to point to your
ProviderManager
and
DecisionManager
, then feed it a list of protected
resources (Example 7-18).
<bean id="filterInvocationInterceptor" class="net.sf.acegisecurity.intercept.web.FilterSecurityInterceptor"> <property name="authenticationManager"> <ref local="authenticationManager"/> </property> <property name="accessDecisionManager"> <ref local="httpRequestAccessDecisionManager"/> </property> <property name="objectDefinitionSource"> <value> CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON A/.*/*.bikes=ROLE_USER </value> </property> </bean>
Note the
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
directive at the top of our protected resources list. There are other
available directives, the most common is
PATTERN_TYPE_APACHE_ANT
, for Ant-style
pattern-matching. The default is regular expression-matching. Your
protection list says that any URL anywhere in the application that
ends in .bikes
(our standard redirect) requires
the user to belong to ROLE_USER
. You could then
add further expressions to the list to require other roles. For
example, to lock down a subfolder called admin
to
users belonging to ROLE_ADMIN
, see Example 7-19.
<property name="objectDefinitionSource"> <value> CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON A/.*/admin/*.htm=ROLE_ADMIN A/.*/*.bikes=ROLE_USER </value> </property>
In the list of protected resources, order is important. ACEGI matches
the first role in the list, so if you listed the
admin folder last, any user of
ROLE_USER
could access the
admin folder, which would be
bad.
Finally, configure the servlet filters themselves (Example 7-20). You will need three: a
SecurityEnforcementFilter
, an
AutoIntegrationFilter
, and an
AuthenticationProcessingFilter
. The enforcement
filter uses FilterSecurityInterceptor
to determine
if access to a resource has been granted, and the
AuthenticationProcessingFilter
redirects
unauthenticated users to the login page. The
SecurityEnforcementFilter
also needs access to a
configured
AuthenticationProcessingFilterEntryPoint
, which
simply stores the URL for the login page and specifies whether it
requires HTTPS.
<bean id="securityEnforcementFilter" class="net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter"> <property name="filterSecurityInterceptor"> <ref local="filterInvocationInterceptor"/> </property> <property name="authenticationEntryPoint"> <ref local="authenticationProcessingFilterEntryPoint"/> </property> </bean> <bean id="authenticationProcessingFilterEntryPoint" class="net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint"> <property name="loginFormUrl"><value>/acegilogin.jsp</value></property> <property name="forceHttps"><value>false</value></property> </bean> <bean id="authenticationProcessingFilter" class="net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilter"> <property name="authenticationManager"> <ref local="authenticationManager"/> </property> <property name="authenticationFailureUrl"> <value>/acegilogin.jsp?login_error=1</value> </property> <property name="defaultTargetUrl"><value>/</value></property> <property name="filterProcessesUrl"> <value>/j_acegi_security_check</value> </property> </bean> <bean id="autoIntegrationFilter" class="net.sf.acegisecurity.ui.AutoIntegrationFilter"></bean>
The AuthenticationProcessingFilter
requires four
properties:
Provides the AuthenticationProvider
that will
authenticate the user.
Redirects when the user enters invalid credentials.
Redirects if a user accesses the login page directly instead of being redirected from a protected URL.
Specifies the target of the login page’s form.
Last but not least, you have to modify the web.xml
file to enable the required filters (Example 7-21).
<filter> <filter-name>Acegi Authentication Processing Filter</filter-name> <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class> <init-param> <param-name>targetClass</param-name> <param-value> net.sf.acegisecurity.ui.webapp.AuthenticationProcessingFilter </param-value> </init-param> </filter> <filter> <filter-name> Acegi Security System for Spring Auto Integration Filter </filter-name> <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class> <init-param> <param-name>targetClass</param-name> <param-value> net.sf.acegisecurity.ui.AutoIntegrationFilter </param-value> </init-param> </filter> <filter> <filter-name>Acegi HTTP Request Security Filter</filter-name> <filter-class>net.sf.acegisecurity.util.FilterToBeanProxy</filter-class> <init-param> <param-name>targetClass</param-name> <param-value> net.sf.acegisecurity.intercept.web.SecurityEnforcementFilter </param-value> </init-param> </filter> <filter-mapping> <filter-name>Acegi Authentication Processing Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name> Acegi Security System for Spring Auto Integration Filter </filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>Acegi HTTP Request Security Filter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
Now, when you run it, ACEGI will force you to authenticate. Example 7-22 is the log output from attempting to access the root of the site, /bikes.htm.
2004-11-28 14:05:13,532 DEBUG [net.sf.acegisecurity.ui.AbstractIntegrationFilter] - Authentication not added to ContextHolder (could not extract an authentication object from the container which is an instance of Authentication) 2004-11-28 14:05:13,532 DEBUG [net.sf.acegisecurity.intercept.web. RegExpBasedFilterInvocationDefinitionMap] - Converted URL to lowercase, from: 'org.apache.coyote.tomcat5.CoyoteRequestFacade@3290aa'; to: '/bikes.htm' 2004-11-28 14:05:13,532 DEBUG [net.sf.acegisecurity.intercept.web. RegExpBasedFilterInvocationDefinitionMap] - Candidate is: '/bikes.htm'; pattern is A/.*/*.htm; matched=true 2004-11-28 14:05:13,532 DEBUG [net.sf.acegisecurity.intercept.AbstractSecurityInterceptor] - Secure object: FilterInvocation: URL: /bikes.htm; ConfigAttributes: [ROLE_USER] 2004-11-28 14:05:13,533 DEBUG [net.sf.acegisecurity.intercept.web. SecurityEnforcementFilter] - Authentication failed - adding target URL to Session: http://localhost:8080/rentabike/bikes.htm net.sf.acegisecurity.AuthenticationCredentialsNotFoundException: A valid SecureContext was not provided in the RequestContext at net.sf.acegisecurity.intercept.AbstractSecurityInterceptor.interceptor (AbstractSecurityInterceptor.java:280) ...etc. 2004-11-28 14:05:13,534 DEBUG [net.sf.acegisecurity.ui.webapp. AuthenticationProcessingFilterEntryPoint] - Redirecting to: http://localhost:8080/ rentabike/acegilogin.jsp 2004-11-28 14:05:13,534 DEBUG [net.sf.acegisecurity.ui.AbstractIntegrationFilter] - ContextHolder does not contain any authentication information ...[other log entries] net.sf.acegisecurity.providers.dao.event. AuthenticationSuccessEvent[source=net.sf.acegisecurity.providers. UsernamePasswordAuthenticationToken@514faa: Username: bruce; Password: [PROTECTED]; Authenticated: false; Details: 127.0.0.1; Not granted any authorities] 2004-11-28 14:05:18,495 INFO [net.sf.acegisecurity.providers.dao.event.LoggerListener] - Authentication success for user: bruce; details: 127.0.0.1 2004-11-28 14:05:18,495 DEBUG [net.sf.acegisecurity.ui.AbstractProcessingFilter] - Authentication success: net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken@453924: Username: bruce; Password: [PROTECTED]; Authenticated: false; Details: 127.0.0.1; Granted Authorities: ROLE_USER 2004-11-28 14:05:18,495 DEBUG [net.sf.acegisecurity.ui.AbstractProcessingFilter] - Redirecting to target URL from HTTP Session (or default):http://localhost:8080/rentabike/bikes.htm
As you can see from the log, the request for
/bikes.htm is matched against our rule and ACEGI
knows that only users belonging to ROLE_USER
can
access it. ACEGI checks the Context
for an
Authentication
object, finds none, so redirects
the user to /acegilogin.jsp, but first captures
the original target URL in the HttpSession
. After
the user authenticates successfully, ACEGI automatically redirects
the user to the original target. This time, when ACEGI attempts to
verify the user, the Authentication
object is
already in the session, permission is granted, and normal Spring
dispatching takes over.
You just saw one of three ways that ACEGI can secure an application. This is the most straightforward method, with the least amount of control. The servlet filters-based approach uses J2EE servlet filters to intercept control before the initial Spring dispatcher is called. Then, ACEGI allows access if the user has appropriate credentials.
In this lab, you’ll learn to use a different kind of security. You’ll secure a method on a bean instead of a servlet. This type of security is still declarative, but it’s a little more involved.
EJB security works by assigning users roles, giving permissions to
those roles, and then assigning permissions to individual methods. In
Spring, to use that model, you’ll want to secure a
method on a bean in the context, instead. In most cases,
you’ll want to secure the methods on your
façade layer—in your case, the
rentaBike
bean.
Method-based security relies on user roles, just like servlet-based security does (Example 7-23). You have already established two users with different roles in the previous lab.
<bean id="inMemoryDaoImpl" class="net.sf.acegisecurity.providers.dao.memory.InMemoryDaoImpl"> <property name="userMap"> <value> justin=gehtland,ROLE_USER,ROLE_ADMIN bruce=tate,ROLE_USER </value> </property> </bean>
To establish access rules for methods on a bean, you have to create
an instance of ACEGI’s
MethodSecurityInterceptor
(Example 7-24). For this application, you will want to
secure methods on the façade layer that controls your data
model. The interceptor needs references to a
ProviderManager
and a
DecisionManager
, just like the
FilterSecurityInterceptor
in the previous lab did.
Similary, it will have a property called
objectDefinitionSource
that lists the methods on
the beans that need to be secured, and what roles have access to
them. For example, all members of ROLE_USER
should
be able to read data from the database, but only members of
ROLE_ADMIN
should be able to save, update, or
delete.
<bean id="bikeRentalSecurity" class="net.sf.acegisecurity.intercept.method.MethodSecurityInterceptor"> <property name="authenticationManager"> <ref local="authenticationManager"/> </property> <property name="accessDecisionManager"> <ref local="httpRequestAccessDecisionManager"/> </property> <property name="objectDefinitionSource"> <value> com.springbook.RentABike.saveBike=ROLE_ADMIN com.springbook.RentABike.deleteBike=ROLE_ADMIN com.springbook.RentABike.saveCustomer=ROLE_ADMIN com.springbook.RentABike.deleteCustomer=ROLE_ADMIN </value> </property> </bean>
To wire up the interceptor, you have to create a proxy around the
rentaBike
controller, just like we did in Chapter 6. First, make sure that your actual
BikeStore
is configured properly (Example 7-25).
<bean id="rentaBikeTarget" class="com.springbook.HibRentABike"> <property name="storeName"><value>Bruce's Bikes</value></ property> <property name="sessionFactory"><ref local="sessionFactory"/></ property> <property name="transactionManager"> <ref local="transactionManager"/> </property> </bean>
Then, wrap it in a Spring interceptor, using the
MethodSecurityInterceptor
as the filter (Example 7-26).
<bean id="rentaBike" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces"> <value>com.springbook.RentABike</value> </property> <property name="interceptorNames"> <list> <idref local="bikeRentalSecurity"/> <idref local="rentaBikeTarget"/> </list> </property> </bean>
Now, try to sign on as a user belonging to ROLE_ADMIN. Example 7-27 is the log output when you attempt to issue a
saveBike
command.
2004-11-29 10:41:39,906 DEBUG [net.sf.acegisecurity.intercept.AbstractSecurityInterceptor] - Secure object: Invocation: method=[public abstract void com.springbook.RentABike. saveBike(com.springbook.Bike)] args=[Ljava.lang.Object;@68b9f] target is of class [com.springbook.HibRentABike]; ConfigAttributes: [ROLE_ADMIN] 2004-11-29 10:41:39,906 DEBUG [net.sf.acegisecurity.providers.ProviderManager] - Authentication attempt using net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider 2004-11-29 10:41:39,906 DEBUG [org.springframework.web.context.support. XmlWebApplicationContext] - Publishing event in context [XmlWebApplicationContext for namespace 'rentaBikeApp-servlet']: net.sf.acegisecurity.providers.dao.event.AuthenticationSuccessEvent[source=net.sf. acegisecurity.providers.UsernamePasswordAuthenticationToken@d92c96: Username: justin; Password: [PROTECTED]; Authenticated: false; Details: 127.0.0.1; Granted Authorities: ROLE_USER, ROLE_ADMIN] 2004-11-29 10:41:39,906 INFO [net.sf.acegisecurity.providers.dao.event.LoggerListener] - Authentication success for user: justin; details: 127.0.0.1 ...[call then proceeds as normal]
If you log out and log back in as a user NOT in the
ROLE_ADMIN
role, Example 7-28 is
the log output.
2004-11-29 11:04:59,240 DEBUG [net.sf.acegisecurity.intercept.AbstractSecurityInterceptor] - Secure object: Invocation: method=[public abstract void com.springbook.RentABike.saveBike (com.springbook.Bike)] args=[Ljava.lang.Object;@26bd42] target is of class [com.springbook.HibRentABike]; ConfigAttributes: [ROLE_ADMIN] 2004-11-29 11:04:59,240 DEBUG [net.sf.acegisecurity.providers.ProviderManager] - Authentication attempt using net.sf.acegisecurity.providers.dao.DaoAuthenticationProvider 2004-11-29 11:04:59,240 DEBUG [org.springframework.web.context.support. XmlWebApplicationContext] - Publishing event in context [XmlWebApplicationContext for namespace 'rentaBikeApp-servlet']: net.sf.acegisecurity.providers.dao.event. AuthenticationSuccessEvent[source=net.sf.acegisecurity.providers. UsernamePasswordAuthenticationToken@c91c22: Username: bruce; Password: [PROTECTED]; Authenticated: false; Details: 127.0.0.1; Granted Authorities: ROLE_USER] 2004-11-29 11:04:59,241 INFO [net.sf.acegisecurity.providers.dao.event.LoggerListener] - Authentication success for user: bruce; details: 127.0.0.1 2004-11-29 11:04:59,241 DEBUG [org.springframework.web.context.support. XmlWebApplicationContext] - Publishing event in context [Root XmlWebApplicationContext]: net.sf.acegisecurity.providers.dao.event.AuthenticationSuccessEvent[source=net.sf. acegisecurity.providers.UsernamePasswordAuthenticationToken@c91c22: Username: bruce; Password: [PROTECTED]; Authenticated: false; Details: 127.0.0.1; Granted Authorities: ROLE_USER] 2004-11-29 11:04:59,241 INFO [net.sf.acegisecurity.providers.dao.event.LoggerListener] - Authentication success for user: bruce; details: 127.0.0.1 2004-11-29 11:04:59,241 DEBUG [net.sf.acegisecurity.intercept.AbstractSecurityInterceptor] - Authenticated: net.sf.acegisecurity.providers.UsernamePasswordAuthenticationToken@537945: Username: bruce; Password: [PROTECTED]; Authenticated: true; Details: 127.0.0.1; Granted Authorities: ROLE_USER 2004-11-29 11:04:59,241 ERROR [org.springframework.web.servlet.DispatcherServlet] - Could not complete request net.sf.acegisecurity.AccessDeniedException: Access is denied. at net.sf.acegisecurity.vote.AffirmativeBased.decide(AffirmativeBased.java:86) ETC. 2004-11-29 11:04:59,243 DEBUG [org.springframework.web.context.support. XmlWebApplicationContext] - Publishing event in context [XmlWebApplicationContext for namespace 'rentaBikeApp-servlet']: RequestHandledEvent: url=[/rentabike/editBike.htm]; time=[5ms]; client=[127.0.0.1]; method=[POST]; servlet=[rentaBikeApp]; session= [029AB8AD12ED146A4356817166C38083]; user=[null]; status=[failed: net.sf.acegisecurity. AccessDeniedException: Access is denied.] 2004-11-29 11:04:59,243 DEBUG [org.springframework.web.context.support. XmlWebApplicationContext] - Publishing event in context [Root XmlWebApplicationContext]: RequestHandledEvent: url=[/rentabike/editBike.htm]; time=[5ms]; client=[127.0.0.1]; method=[POST]; servlet=[rentaBikeApp]; session=[029AB8AD12ED146A4356817166C38083]; user=[null]; status=[failed: net.sf.acegisecurity.AccessDeniedException: Access is denied.] 2004-11-29 11:04:59,243 DEBUG [net.sf.acegisecurity.intercept.web. SecurityEnforcementFilter] - Access is denied - sending back forbidden response 2004-11-29 11:04:59,243 DEBUG [net.sf.acegisecurity.ui.AbstractIntegrationFilter] - Updating container with new Authentication object, and then removing Authentication from ContextHolder
The ACEGI model uses interceptors. These interceptors map credentials (or roles) onto an individual user when the user authenticates. ACEGI stores this information in its authentication engine, and lets you, or an interceptor, query this information.
Next, ACEGI stores a map of permissions on each role. Then, when you proxy an interface, you specify what credentials are required to do a particular thing, like fire a method. Like the other services in this chapter, these work declaratively.
...instance-based security? EJB security often breaks down because guarding an individual method is not enough. You don’t care whether a manager wants to retrieve invoices. You care which invoices the manager wants to retrieve. He should only have access to the invoices for employees in his department.
ACEGI provides instance-based security. You can attach ACLs (access
control lists) to individual instances, just as you attached
permissions to roles. ACEGI provides a mapping mechanism to do this.
You can assign AclEntries
to domain instances,
then use an AclManager
to look up
AclEntries
from a given domain instance.
AccessDecisionManagers
then decide if the current
user has appropriate permissions based on the
AclEntries
.
In this example, you’ll learn how to build interceptors that do nothing but improve your ability to test code. Usually, a transaction interceptor does three things:
Begins a transaction when a method starts.
Rolls back a transaction when an exception is thrown.
Commits the transaction when a method completes.
Of course, that’s what you usually want to happen. Test cases are different animals, though. They’re responsible for putting conditions back to the state where they were before a test case was fired, and they need to execute quickly.
For a test case, often it makes sense to replace the third step. You may instead want the transaction to roll back after the method completes. That way, you remove one of the most expensive database steps (the commit), and you also restore the database state to what it was before you executed the transaction.
In Spring, it’s easy to build this type of behavior into an interceptor, because you can build a custom interceptor. Example 7-29 shows the code for a transaction interceptor that rolls back.
public class TestTxInterceptor implements MethodInterceptor { private PlatformTransactionManager platformTransactionManager; public PlatformTransactionManager getPlatformTransactionManager( ) { return platformTransactionManager; } public void setPlatformTransactionManager(PlatformTransactionManager platformTransactionManager) { this.platformTransactionManager = platformTransactionManager; } public Object invoke(MethodInvocation methodInvocation) throws Throwable { DefaultTransactionDefinition def = new DefaultTransactionDefinition( ); def.setPropagationBehavior(TransactionDefinition. PROPAGATION_REQUIRED); TransactionStatus status = platformTransactionManager.getTransaction(def); Object results = null; try { results = methodInvocation.proceed( ); } finally { platformTransactionManager.rollback(status); } return results; } }
Next, Example 7-30 shows the configuration of the context.
<bean id="transactionManager" class="org.springframework.orm.hibernate.HibernateTransactionManager"> <property name="sessionFactory"><ref local="sessionFactory"/></property> </bean> <bean id="testTxInterceptor" class="com.springbook.interceptors.TestTxInterceptor"> <property name="platformTransactionManager"> <ref local="transactionManager"/> </property> </bean> <bean id="rentaBike" class="org.springframework.aop.framework.ProxyFactoryBean"> <property name="proxyInterfaces"> <value>com.springbook.RentABike</value> </property> <property name="interceptorNames"> <list> <idref local="bikeRentalSecurity"/> <idref local="testTxInterceptor"/> <idref local="rentaBikeTarget"/> </list> </property> </bean>
Now, you can write any JUnit tests you want against the façade layer, testing reads and writes, testing for appropriate security filtering, and so on, without worrying about changing the database. When running the unit tests, use a version of the configuration file that assigns the interceptor, and leave out the interceptor in a deployment scenario.
The typical JUnit flow works like this:
setUp
prepares the system for a test case. In our
setUp
method, we do all of the work
that’s common across the databases.
A series of individual tests then follow. In our case, we slightly change the flow, in order to call all of the methods within a method that uses the Spring context.
tearDown
then does all cleanup. For database
tests, the tearDown
can be particularly expensive,
because it’s got to restore the test data to a
usable state for other test cases. (An alternative approach is the
reset the database to a known good state in every
setUp
method. If the set of test data is small
enough, this is a safer approach. The larger the set of test data,
the more likely it is that you will want to use
tearDown
to undo test changes instead of
setUp
to recreate the full database.) A test
changes something in the database, verifies the results, and then
repeats. Notice also that all of the database access is in a central
method. That makes it easy to bracket the method with declarative
transactions.
Now look at the context. This is a case where you’re actually proxying the test code to add some behavior that makes testing easier. That’s using the power of Spring to build a much better test. In the next chapter, you’ll look into a few more services, like remoting and messaging.
13.58.209.201