Integrating with Spring Security

NOTE: AndroMDA has been upgraded to Spring 3.1, however the Acegi framework is not compatible with Spring 2.5+, and has been replaced by the spring-security framework. This completely changes the namespace and values in the Spring configuration file. The andromda templates have been updated to the correct classes and namespaces, but have not been tested yet. See http://static.springsource.org/spring-security/site/ and http://java.dzone.com/tips/pathway-acegi-spring-security- and http://raibledesigns.com/rd/entry/upgrading_to_spring_security_2

In this section we will add security to TimeTracker by integrating it with the Spring Security System. The integration will ensure that only authenticated users can access secured pages. Currently all pages in TimeTracker are specified to be secure, except of course the Login page. Even if the user has bookmarked a page and tries to view it directly without presenting her credentials, Spring Security will intercept the request and present the Login page. Once the credentials are presented and verified, user will be directed to the page she was trying to access.

We chose Spring Security System because it provides a portable and effective security framework for enterprise applications. Look at the Spring Security Home Page to understand its key features. Also look at the Frequently Asked Questions to understand what Spring Security provides over alternate solutions such as web.xml security and JAAS. You may also want to read the Main Features section of the Spring Security Reference Guide. Armed with all this knowledge, let's get down to business. Remember that if you run into problems, you can always compare your implementation with the completed solution to figure out what's going on.

SecurityService

We will first create a new service called SecurityService that allows the front-end to verify user credentials. We chose not to add this functionality to the UserService because of the confidential nature of this information. UserService may be something you want to expose to the outside world, but definately not passwords and other secure information of similar nature. Follow the steps below to add SecurityService to TimeTracker.

  1. In the Domain Objects diagram, add an enumeration called Role. This enumeration represents the roles that authenticated users can play. Make sure that Role is created under the org.andromda.timetracker.domain package.
  2. Again, in the Domain Objects diagram, enhance the User entity with additional attributes to support security. Note that Spring Security only requires the username and password fields, we have added the other attributes just to show how Spring Security can support additional security requirements.
  3. Now add the UserRole entity and the one-to-many relationship between User and UserRole.
  4. We are now ready to create the value objects that will be used by the SecurityService. Create UserDetailsVO, UserRoleVO and UserRoleVO[] in the Value Objects diagram as shown below. Note that UserDetailsVO extends UserVO using a Generalization relationship. Next create the dependency relationships as shown in the diagram. Note that the Dependency from User to UserRole needs to be created in addition to the Association which already connects them.
  5. We are finally ready to create the SecurityService. Add SecurityService to the Services diagram as shown below. Also add the dependency to the User entity.
  6. Add a method to the User entity to allow eager fetching of a User along with its roles - all in one database hit. Here's the specification for this method:
    +getUserDetails(username : String) : User
    Since we want this method to be generated in the DAO, change its scope to classifier (or, for ArgoUML select the static modifier).
  7. That completes all the changes to the model. Now let's generate code:
    mvn install
    The tests will fail because the database schema has changed.
  8. Drop the database schema and create a new one:
    mvn -f core/pom.xml andromdapp:schema -Dtasks=drop,create
  9. Populate the new schema with the following test data.
                                -- Password is 'cooldude' encoded using MD5
                                insert into USERS
                                (ID, USERNAME, PASSWORD, FIRST_NAME, LAST_NAME,
                                EMAIL, IS_ACTIVE, CREATION_DATE, COMMENT)
                                values (1, 'nbhatia', '756slLjeNViurJBGI5JeqA==', 'Naresh', 'Bhatia',
                                'nbhatia@northwind.com', 1, '2011/01/01 09:00', null);
                                insert into USERS
                                (ID, USERNAME, PASSWORD, FIRST_NAME, LAST_NAME,
                                EMAIL, IS_ACTIVE, CREATION_DATE, COMMENT)
                                values (2, 'lcoude', '756slLjeNViurJBGI5JeqA==', 'Louis', 'Coude',
                                'lcoude@northwind.com', 1, '2011/01/01 09:00', null);
                                insert into USERS
                                (ID, USERNAME, PASSWORD, FIRST_NAME, LAST_NAME,
                                EMAIL, IS_ACTIVE, CREATION_DATE, COMMENT)
                                values (3, 'ecrutchfield', '756slLjeNViurJBGI5JeqA==', 'Eric', 'Crutchfield',
                                'ecrutchfield@northwind.com', 1, '2011/01/01 09:00', null);
                                insert into USERS
                                (ID, USERNAME, PASSWORD, FIRST_NAME, LAST_NAME,
                                EMAIL, IS_ACTIVE, CREATION_DATE, COMMENT)
                                values (4, 'cmicali', '756slLjeNViurJBGI5JeqA==', 'Chris', 'Micali',
                                'cmicali@northwind.com', 1, '2011/01/01 09:00', null);
                                insert into USER_ROLE (ID, ROLE, USER_FK)
                                values (1, 'StandardUser', 1);
                                insert into USER_ROLE (ID, ROLE, USER_FK)
                                values (2, 'Administrator', 1);
                                insert into USER_ROLE (ID, ROLE, USER_FK)
                                values (3, 'StandardUser', 2);
                                insert into USER_ROLE (ID, ROLE, USER_FK)
                                values (4, 'StandardUser', 3);
                                insert into USER_ROLE (ID, ROLE, USER_FK)
                                values (5, 'StandardUser', 4);
                                insert into TIMECARD (ID, STATUS, START_DATE, COMMENTS, APPROVER_FK, SUBMITTER_FK)
                                values ( 1, 'Approved', '2011/05/15', 'Timecard 01', 1, 2);
                                insert into TIMECARD (ID, STATUS, START_DATE, COMMENTS, APPROVER_FK, SUBMITTER_FK)
                                values ( 2, 'Approved', '2011/05/15', 'Timecard 02', 2, 3);
                                insert into TIMECARD (ID, STATUS, START_DATE, COMMENTS, APPROVER_FK, SUBMITTER_FK)
                                values ( 3, 'Approved', '2011/05/15', 'Timecard 03', 3, 4);
                                insert into TIMECARD (ID, STATUS, START_DATE, COMMENTS, APPROVER_FK, SUBMITTER_FK)
                                values ( 4, 'Approved', '2011/05/15', 'Timecard 04', 4, 1);
                                insert into TIMECARD (ID, STATUS, START_DATE, COMMENTS, APPROVER_FK, SUBMITTER_FK)
                                values ( 5, 'Rejected', '2011/05/22', 'Timecard 05', 1, 2);
                                insert into TIMECARD (ID, STATUS, START_DATE, COMMENTS, APPROVER_FK, SUBMITTER_FK)
                                values ( 6, 'Rejected', '2011/05/22', 'Timecard 06', 2, 3);
                                insert into TIMECARD (ID, STATUS, START_DATE, COMMENTS, APPROVER_FK, SUBMITTER_FK)
                                values ( 7, 'Rejected', '2011/05/22', 'Timecard 07', 3, 4);
                                insert into TIMECARD (ID, STATUS, START_DATE, COMMENTS, APPROVER_FK, SUBMITTER_FK)
                                values ( 8, 'Rejected', '2011/05/22', 'Timecard 08', 4, 1);
                                insert into TIMECARD (ID, STATUS, START_DATE, COMMENTS, APPROVER_FK, SUBMITTER_FK)
                                values ( 9, 'Submitted', '2011/05/29', 'Timecard 09', 1, 2);
                                insert into TIMECARD (ID, STATUS, START_DATE, COMMENTS, APPROVER_FK, SUBMITTER_FK)
                                values (10, 'Submitted', '2011/05/29', 'Timecard 10', 2, 3);
                                insert into TIMECARD (ID, STATUS, START_DATE, COMMENTS, APPROVER_FK, SUBMITTER_FK)
                                values (11, 'Submitted', '2011/05/29', 'Timecard 11', 3, 4);
                                insert into TIMECARD (ID, STATUS, START_DATE, COMMENTS, APPROVER_FK, SUBMITTER_FK)
                                values (12, 'Submitted', '2011/05/29', 'Timecard 12', 4, 1);
                                insert into TIMECARD (ID, STATUS, START_DATE, COMMENTS, APPROVER_FK, SUBMITTER_FK)
                                values (13, 'Draft', '2011/06/05', 'Timecard 13', null, 2);
                                insert into TIMECARD (ID, STATUS, START_DATE, COMMENTS, APPROVER_FK, SUBMITTER_FK)
                                values (14, 'Draft', '2011/06/05', 'Timecard 14', null, 3);
                                insert into TIMECARD (ID, STATUS, START_DATE, COMMENTS, APPROVER_FK, SUBMITTER_FK)
                                values (15, 'Draft', '2011/06/05', 'Timecard 15', null, 4);
                                insert into TIMECARD (ID, STATUS, START_DATE, COMMENTS, APPROVER_FK, SUBMITTER_FK)
                                values (16, 'Draft', '2011/06/05', 'Timecard 16', null, 1);
                            
  10. Now build the application again by executing mvn install. This time the tests should pass.
  11. Let's now implement the UserDaoImpl.handleGetUserDetails() method. Open the file core\src\main\java\org\andromda\timetracker\domain\UserDaoImpl.java and add the following implentation for handleGetUserDetails():
                                protected User handleGetUserDetails(String username) throws Exception {
                                User user = (User)getSession().createQuery(
                                "from org.andromda.timetracker.domain.User user " +
                                "left join fetch user.roles " +
                                "where user.username = :username")
                                .setParameter("username", username)
                                .uniqueResult();
                                return user;
                                }
                            
  12. Add the following imports at the top of UserDaoImpl.java:
                                import java.util.Collection;
                                import org.andromda.timetracker.vo.UserRoleVO;
                            
  13. We now need to override the default implementation of UserDaoImpl.toUserDetailsVO(). The reason for this is to include the attributes from the associated UserRole objects into UserDetailsVO. Here's the implentation for the method:
                                public void toUserDetailsVO(
                                org.andromda.timetracker.domain.User sourceEntity,
                                org.andromda.timetracker.vo.UserDetailsVO targetVO)
                                {
                                super.toUserDetailsVO(sourceEntity, targetVO);
                                // Convert roles
                                Collection srcRoles = sourceEntity.getRoles();
                                UserRoleVO[] targetRoles = new UserRoleVO[srcRoles.size()];
                                int i=0;
                                for (Object srcRole : srcRoles)
                                {
                                targetRoles[i] = getUserRoleDao().toUserRoleVO((UserRole)srcRole);
                                i++;
                                }
                                targetVO.setRoles(targetRoles);
                                }
                            
  14. Finally, we need to add the implementation for SecurityServiceImpl.handleGetUserDetails(). Open the file core\src\main\java\org\andromda\timetracker\service\SecurityServiceImpl.java and the following code:
                                import org.andromda.timetracker.domain.User;
                                import org.andromda.timetracker.vo.UserDetailsVO;
                                ...
                                protected UserDetailsVO handleGetUserDetails(String username)
                                throws java.lang.Exception
                                {
                                UserDetailsVO userDetailsVO = null;
                                User user = getUserDao().getUserDetails(username);
                                if (user != null)
                                {
                                userDetailsVO = getUserDao().toUserDetailsVO(user);
                                }
                                return userDetailsVO;
                                }
                            

Spring Security Integration

We are now ready to introduce Spring Security as a dependency for TimeTracker.

  1. Edit the file C:\timetracker\pom.xml and add the following dependency under the dependencyManagement section. If you are unsure where to add this section, just look in the pom.xml file under the completed application.
                                <dependency>
                                <groupId>org.springframework.security</groupId>
                                <artifactId>spring-security-core</artifactId>
                                <version>3.1.0.RELEASE</version>
                                <exclusions>
                                <exclusion>
                                <groupId>commons-codec</groupId>
                                <artifactId>commons-codec</artifactId>
                                </exclusion>
                                <exclusion>
                                <groupId>org.springframework</groupId>
                                <artifactId>spring-remoting</artifactId>
                                </exclusion>
                                <exclusion>
                                <groupId>org.springframework</groupId>
                                <artifactId>spring-jdbc</artifactId>
                                </exclusion>
                                <exclusion>
                                <groupId>org.springframework</groupId>
                                <artifactId>spring-support</artifactId>
                                </exclusion>
                                </exclusions>
                                </dependency>
                            
    NOTE: If you get an runtime error from your application server saying it can't find org/apache/commons/codec/binary/Base64 when you try to log in, you may need to comment out the exclusion for commons-codec.
  2. Now edit the file C:\timetracker\web\pom.xml and add the following dependency under the dependencies section. Again, if you are unsure where to add this section, just look in the web\pom.xml file under the completed application.
                                <dependency>
                                <groupId>org.springframework.security</groupId>
                                <artifactId>spring-security-core</artifactId>
                                </dependency>
                            
  3. We need several changes in the WebMergeMappings.xml file under mda\src\main\config\mappings. These changes will trigger the inclusion of Spring Security filter chain in the web container. Copy the WebMergeMappings.xml from the completed tutorial to your version.
  4. Next we need to implement the UserDetailsService which is used by Spring Security to access UserDetails from the database. For us, this will be a very simple service that delegates to the SecurityService that we designed in the TimeTracker model. Copy the directory web\src\main\java\org\andromda\timetracker\web\security from the completed solution to your implementation. This directory contains two very simple Java files that implement the UserDetailsService.
  5. Delete the jsp directory under C:\timetracker\web\src\main. Remember that this is where we had cutomized the look and feel of the application. Well now we have to add some more files to this directory (such as the custom login page and Spring Security configuration file). For the purpose of this tutorial, it is simpler to just delete this directory and copy over a fresh one.
  6. Download custom-look-and-feel2.zip and unzip it at C:\timetracker\web\src\main. You will get a sub-directory under main called jsp.

Build and deploy TimeTracker

  1. Execute the commands mvn clean followed by mvn install in the Command Prompt.
  2. Make sure the JBoss server is running.
  3. Deploy the application: Copy timetracker.war from web\target to Jboss standalone\deployments directory.
  4. Open a browser and make it point to http://localhost:8080/timetracker. This time the login page will appear because the search page is configured as a secure page.
  5. Log in with the username nbhatia and password cooldude. The browser will now show the search page.

What's Next?

Congratulations! You have made it successfully to the end of this tutorial. Click here to find out where to go from here.