2012年12月6日星期四

Custom Roo login

http://sujitpal.blogspot.com/2010/07/ktm-customizing-roo-security.html

Thursday, July 22, 2010

KTM - Customizing Roo Security


KTM is a read-write app, ie, its functionality is driven by data that is entered into it. So I needed some way to restrict different classes of user to different areas of the app. Specifically, I would like one class of user (administrator) to control the Person entity, another class (managers) to control the Client, Project, Item and Allocations entities, and yet another class (developers) to control the Task and Hours entities. By control, I mean the ability to create, update or delete an entity - read-only operations, such as show, list, find, etc, are unrestricted.

Generate default Security Setup

I started off by letting Roo generate the default security setup, by issuing the following command from the Roo shell.
1
security setup
This creates and modifies a bunch of files in the app, but the one of interest to me was the applicationContext-security.xml file, which is shown below. I have edited it slightly to make it more readable. I also removed the password hashes in the password attribute for the user tag and replaced it with ellipsis.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?xml version="1.0" encoding="UTF-8"?>

<beans:beans xmlns="http://www.springframework.org/schema/security"
    xmlns:beans="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    http://www.springframework.org/schema/security
    http://www.springframework.org/schema/security/spring-security-3.0.xsd">

    <!-- HTTP security configurations -->
    <http auto-config="true" use-expressions="true">
      <form-login login-processing-url="/static/j_spring_security_check"
           login-page="/login" 
           authentication-failure-url="/login?login_error=t"/>
        <logout logout-url="/static/j_spring_security_logout"/>
        
        <!-- Configure these elements to secure URIs in your application -->
        <intercept-url pattern="/choice/**" access="hasRole('ROLE_ADMIN')"/>
        <intercept-url pattern="/member/**" access="isAuthenticated()" />
        <intercept-url pattern="/resources/**" access="permitAll" />
        <intercept-url pattern="/static/**" access="permitAll" />
        <intercept-url pattern="/**" access="permitAll" />
    </http>

    <!-- Configure Authentication mechanism -->
    <authentication-manager alias="authenticationManager">
      <!-- SHA-256 values can be produced using 'echo -n
             your_desired_password | sha256sum' (using normal 
             *nix environments) -->
      <authentication-provider>
        <password-encoder hash="sha-256"/>
        <user-service>
          <user name="admin" password="..." authorities="ROLE_ADMIN"/>
          <user name="user" password="..." authorities="ROLE_USER"/>
        </user-service>
      </authentication-provider>
    </authentication-manager>
</beans:beans>
As you can see, its completely generic, it has no references to the application generated so far. My understanding is that this is meant more as a template that you have to customize for your app.

Modifying Person

The Roo generated security configuration presented above uses an in-memory authentication provider that is configured using the user-service element. Since my app maintains a set of Person entities, I figured I could build a custom provider that used Person data. To do that though, I needed to add a password field to Person. I do that from the Roo shell.
I also wanted to use the Person's email address as his username. This has the advantage of being unique and is easy to type (compared to the Person.name which contains the full name). So I would also need to be able to look a Person up by his email address. Once again, I use the Roo shell to generate the finder.
1
2
field string --fieldName password --class com.healthline.ktm.domain.Person
finder add --finderName findPeopleByEmailAddress
This would save the password in plain-text in the database, which is probably not desirable. Unfortunately there is no --password switch for the Roo field command. So I added a method to encrypt the password in the Person bean to a 32-character MD5 Hash and annotate it so it is called before an INSERT or UPDATE.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class Person {
  ...
  @PrePersist
  @PreUpdate
  protected void encryptPassword() {
    if (password != null && (! password.matches("^[0-9a-fA-F]+$"))) {
      // prevent encryption if already encrypted
      password = DigestUtils.md5DigestAsHex(password.getBytes());
    }
  }
}
The Roo generated JSPs for Person will also need to be modified. Before I do that though, I want to make sure that our modifications don't get overwritten the next time I do something with the entity, so I set automaticallyMaintainView=false in the @RooWebScaffold annotation for PersonController.
For the create.jspx and update.jspx, the changes involve replacing the <form:input> tag for the password field with <form:password> tag. By default, the form:password tag will initialize the field, so for the update.jspx, I added another attribute showPassword="true" so it doesn't.
I left the generated password in for the list.jspx and show.jspx, since its encrypted on its way in anyway, and what shows up is a 32-character hex string which wouldn't mean much to most people, and because it could be helpful for debugging if you are the developer.

Custom Authentication Provider

My custom authentication provider extends AbstractUserDetailsAuthenticationProvider, which works with username/password kind of setups. Since KTM deals with projects, the administrator's details are not recorded in the Person table. So the provider declares a administrator pseudo-user, the username and password for which is injected into the provider via Spring configuration.
The provider calls its retrieveUser() method to authenticate the user using the email address as username and entered password for password, encrypting the password to match the one it looks up using the Person.findPeopleByEmailAddress() from the database. It then uses the WorkRole enum value in the Person entity to figure out the authorizations. For WorkRole.Manager, the GrantedAuthority is ROLE_MANAGER, for WorkRole.Developer it is ROLE_DEVELOPER, and for WorkRole.Combined, it is ROLE_MANAGER and ROLE_DEVELOPER. For the admin user, the GrantedAuthority is ROLE_ADMIN. The method returns a populated UserDetails object if the login succeeded, or throws a BadCredentialsException with the appropriate message if not.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
package com.healthline.ktm.security;

import java.util.ArrayList;
import java.util.List;

import javax.persistence.EntityNotFoundException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.Query;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Required;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.GrantedAuthorityImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;

import com.healthline.ktm.domain.Person;
import com.healthline.ktm.domain.WorkRoles;

@Service("ktmAuthenticationProvider")
public class KtmAuthenticationProvider extends 
    AbstractUserDetailsAuthenticationProvider {

  private final Logger logger = Logger.getLogger(getClass());

  private String adminUser;
  private String adminPassword;
  
  @Required
  public void setAdminUser(String adminUser) {
    this.adminUser = adminUser;
  }
  
  @Required
  public void setAdminPassword(String adminPassword) {
    this.adminPassword = adminPassword;
  }
  
  @Override
  protected void additionalAuthenticationChecks(UserDetails userDetails,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
    return;
  }

  @Override
  protected UserDetails retrieveUser(String username,
      UsernamePasswordAuthenticationToken authentication)
      throws AuthenticationException {
    String password = (String) authentication.getCredentials();
    if (! StringUtils.hasText(password)) {
      throw new BadCredentialsException("Please enter password");
    }
    String encryptedPassword = DigestUtils.md5DigestAsHex(password.getBytes()); 
    UserDetails user = null;
    String expectedPassword = null;
    List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
    if (adminUser.equals(username)) {
      // pseudo-user admin (ie not configured via Person)
      expectedPassword = DigestUtils.md5DigestAsHex(adminPassword.getBytes()); 
      // authenticate admin
      if (! encryptedPassword.equals(expectedPassword)) {
        throw new BadCredentialsException("Invalid password");
      }
      // authorize admin
      authorities.add(new GrantedAuthorityImpl("ROLE_ADMIN"));
    } else {
      try {
        Query query = Person.findPeopleByEmailAddress(username);
        Person person = (Person) query.getSingleResult();
        // authenticate the person
        expectedPassword = person.getPassword();
        if (! StringUtils.hasText(expectedPassword)) {
          throw new BadCredentialsException("No password for " + username + 
            " set in database, contact administrator");
        }
        if (! encryptedPassword.equals(expectedPassword)) {
          throw new BadCredentialsException("Invalid Password");
        }
        // authorize the person
        WorkRoles role = person.getWorkRole();
        switch (role) {
          case Manager:
            authorities.add(new GrantedAuthorityImpl("ROLE_MANAGER"));
            break;
          case Combined:
            authorities.add(new GrantedAuthorityImpl("ROLE_MANAGER"));
            authorities.add(new GrantedAuthorityImpl("ROLE_DEVELOPER"));
            break;
          case Developer:
            authorities.add(new GrantedAuthorityImpl("ROLE_DEVELOPER"));
            break;
          default:
            // should never happen since Person will have one of
            // the above WorkRoles defined, but just in case we
            // decide to add a new role in the future...
            throw new BadCredentialsException("User:[" + username + 
              "] has unknown role: " + role);
        }
      } catch (EntityNotFoundException e) {
        throw new BadCredentialsException("Invalid user");
      } catch (NonUniqueResultException e) {
        throw new BadCredentialsException(
          "Non-unique user, contact administrator");
      }
    }
    return new User(
      username,
      password,
      true, // enabled 
      true, // account not expired
      true, // credentials not expired 
      true, // account not locked
      authorities
    );
  }
}
In the applicationContext-security.xml file, the KtmAuthenticationProvider bean replaces the default in-memory authentication provider generated by Roo. Here is what the block under the "Configure Authentication Mechanism" looks like with my custom authentication provider.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
  <!-- Configure Authentication mechanism -->
  <beans:bean id="ktmAuthenticationProvider" 
      class="com.healthline.ktm.security.KtmAuthenticationProvider">
    <beans:property name="adminUser" value="admin"/>
    <beans:property name="adminPassword" value="admin"/>
  </beans:bean>
  
  <authentication-manager alias="authenticationManager">
    <authentication-provider ref="ktmAuthenticationProvider"/>
  </authentication-manager>

Customizing Intercept URL Patterns

Now that our authentication provider returns a UserDetail with a List of GrantedAuthority objects that correspond to our app, we can update the block titled "HTTP Security configurations" in the applicationContext-security.xml. This snippet from my updated applicationContext-security.xml file is shown below:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
  <!-- HTTP security configurations -->
  <http auto-config="true" use-expressions="true" 
      access-denied-page="/app/accessDenied">
    <form-login login-processing-url="/static/j_spring_security_check" 
        login-page="/login" 
        authentication-failure-url="/login?login_error=t"/>
    <logout logout-url="/static/j_spring_security_logout"/>
        
    <!-- ROLE_ADMIN has create/edit/delete on Person -->
        
    <intercept-url pattern="/person/form" access="hasRole('ROLE_ADMIN')"/>
    <intercept-url pattern="/person/\\d+/form" access="hasRole('ROLE_ADMIN')"/>
    <intercept-url pattern="/person/**" method="DELETE" 
      access="hasRole('ROLE_ADMIN')"/>
        
    <!-- ROLE_MANAGER has create/edit/delete on Client, Project, -->
    <!-- Item, Allocations                                       -->
      
    <intercept-url pattern="/client/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/client/\\d+/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/client/**" method="DELETE" 
      access="hasRole('ROLE_MANAGER')"/>

    <intercept-url pattern="/project/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/project/\\d+/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/project/**" method="DELETE" 
      access="hasRole('ROLE_MANAGER')"/>

    <intercept-url pattern="/item/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/item/\\d+/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/item/**" method="DELETE" 
      access="hasRole('ROLE_MANAGER')"/>

    <intercept-url pattern="/allocations/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/allocations/\\d+/form" access="hasRole('ROLE_MANAGER')"/>
    <intercept-url pattern="/allocations/**" method="DELETE" 
      access="hasRole('ROLE_MANAGER')"/>

    <!-- ROLE_DEVELOPER has create/edit/delete on Task, Hours -->
    
    <intercept-url pattern="/task/form" access="hasRole('ROLE_DEVELOPER')"/>
    <intercept-url pattern="/task/\\d+/form" access="hasRole('ROLE_DEVELOPER')"/>
    <intercept-url pattern="/task/**" method="DELETE" 
      access="hasRole('ROLE_DEVELOPER')"/>
        
    <intercept-url pattern="/hours/form" access="hasRole('ROLE_DEVELOPER')"/>
    <intercept-url pattern="/hours/\\d+/form" access="hasRole('ROLE_DEVELOPER')"/>
    <intercept-url pattern="/hours/**" method="DELETE" 
      access="hasRole('ROLE_DEVELOPER')"/>

    <!-- Everything else can be accessed by anybody, logged in or not -->
    <intercept-url pattern="/resources/**" access="permitAll" />
    <intercept-url pattern="/static/**" access="permitAll" />
    <intercept-url pattern="/**" access="permitAll" />
  </http>
The inline comments are pretty-self explanatory - the XML above basically codifies the rules outlined in the first paragraph in this post. Since Roo generates a REST-ful app, the deletes are handled using a HTTP delete method, which we need to handle using the method attribute in the intercept-url elements above as explained here.
At this point, if someone clicks "Create new Person" from the main page, they will be presented with a login screen. Once they login with admin/admin (as configured in the KtmAuthenticationProvider Spring config), they would then see the Create new Person screen. The same workflow would exist for the other "Create" links - based on the authorization, they would either be sent to the form page or to an "Access Denied" page (more on this in a bit).

Turning off Unauthorized Functionality

The interception style of authenticating is good for external facing web applications (such as websites), since most of the content is for public consumption, and you authenticate either when a user decides to participate, or when you are exposing personal content. For intranet/tool applications such as KTM, I prefer to start the user with a login page, and expose only the functionality which they are authorized for when they do login.
Spring Security (or Acegi) has some JSP tags which allow you to do this fairly easily. If I wanted to go the "put everything behind a login screen" route, I would need to wrap the protected portions (of menu.jspx) in <sec:authorize> tags and replace index.jspx with login.jspx (I think, haven't tried it). But I was too lazy to do this - since this would be an Intranet app, doing it one way or the other is merely a matter of user training.
I did, however, want to hide the update and delete icons from the "List all XXX" pages from unauthorized users. So I basically had to do wrap the update and delete icon columns in the table representing the listing, and to bind the sec: namespace to the URI for the Spring security TLD. The basic pattern is:
1
2
3
  <sec:authorize access="hasRole('ROLE_ADMIN')">
    // stuff you want to show only to admin goes here
  </sec:authorize>
Once this was done, the person listing page will look different based on whether you are logged in as "admin" (the one on the left below), or if you are either not logged in, or logged in with some other role (the one on the right below).

Static Access Denied Page

One of the problems with the intercept pattern is multiple roles. For example, if a user clicks on "Create a Person", and is presented with a login screen, it is not immediately obvious what he should login as. For example, if he logs in as himself, and he has ROLE_MANAGER, then what happens is that he falls through to a 403 Access Denied page served by the container (in my case Jetty).
So I decided to build an accessDenied.jspx page along the same lines as resourceNotFound.jspx - ie, a static page with an appropriate message and a short description of the different roles and what functions they can perform. It looks like this:
To build this page, I copied resourceNotFound.jspx into accessDenied.jspx and changed the title and problemdescription label names, then mimicked its configuration by doing a "find | xargs grep". Here are the locations I had to modify.
  • messages*.properties - created the contents of the accessdenied.title and accessdenied.problemdescription labels in 5 different languages and add them as properties in these files.
  • webmvc-config.xml - add in a line <mvc:view-controller path="/aceessDenied.jspx"/>, similar to the one for resourceNotFound. This creates a "static" Spring controller.
  • web.xml - add an <error-page> entry for HTTP error code 403 for /app/accessDenied.
  • views.xml (top level) - In the top level views.xml tiles definition file (under WEB-INF/views), I created an entry pointing to the actual JSPX file.
  • applicationContext-security.xml - I added the access-denied-page attribute to the http element in our applicationContext-security.xml pointing to /app/accessDenied. This is already in the snippet shown above.

Manual Change Password Controller

With the setup described so far, the administrator is the only one who can create or update a user's password. This is a bit inflexible in most real-world scenarios - ideally, the user should be able to change his password to something else. I built a simple manual Roo controller for this. Skeleton code is generated using the Roo shell:
1
controller class --class com.healthline.ktm.web.ChangePasswordController
The generated controller is similar to the SimpleFormController of the pre-annotation Spring days. I don't know about you, but to me, the SimpleFormController, while powerful, was anything but simple, and my work did not involve that much form handling anyway, so I never used it. In any case, based on the advice from here and here, I managed to figure out how to work with it, although the code I ended up with exposed different, but equivalent, method signatures compared to the one generated.
First, the controller. As you can see, it exposes a named bean via the @ModelAttribute annotation. When the "Change Password" link is clicked, the index() method is called. This sends an empty model attribute bean (the form bean) to the JSP which displays the form. When the user fills out the form and clicks submit, the update() method is called, which validates the data using the @Autowired validator instance. If everything is good, it sets the encrypted password into the Person instance and updates it, and forwards to the "thanks" view, powered by the thanks() method. If not, the form is redisplayed with the appropriate error message(s).
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.healthline.ktm.web;

import java.util.List;

import javax.persistence.Query;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import com.healthline.ktm.domain.Person;
import com.healthline.ktm.fbo.ChangePasswordForm;
import com.healthline.ktm.fbo.ChangePasswordValidator;

@RequestMapping("/changepassword/**")
@Controller
public class ChangePasswordController {

  private final Logger logger = Logger.getLogger(getClass());

  @Autowired private ChangePasswordValidator validator;
  
  @ModelAttribute("changePasswordForm")
  public ChangePasswordForm formBackingObject() {
    return new ChangePasswordForm();
  }

  @RequestMapping(value="/changepassword/index")
  public String index() {
    return "changepassword/index";
  }

  @RequestMapping(value="/changepassword/update", method=RequestMethod.POST)
  public String update(
      @ModelAttribute("changePasswordForm") ChangePasswordForm form, 
      BindingResult result) {
    validator.validate(form, result);
    if (result.hasErrors()) {
      return "changepassword/index"; // back to form
    } else {
      String newPassword = form.getNewPassword();
      Query query = Person.findPeopleByEmailAddress(form.getEmailAddress());
      Person person = (Person) query.getSingleResult();
      person.setPassword(newPassword);
      person.merge();
      return "changepassword/thanks";
    }
  }
  
  @RequestMapping(value="/changepassword/thanks")
  public String thanks() {
    return "changepassword/thanks";
  }
}
The form bean exposed through the @ModelAttribute annotation is a POJO that exposes getters and setters for the form fields. Like this:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
package com.healthline.ktm.fbo;

public class ChangePasswordForm {

  private String emailAddress;
  private String oldPassword;
  private String newPassword;
  private String newPasswordAgain;

  // getters and setters omitted, use your IDE to fill them out  
}
The controller also autowires in a custom validator to validate the fields.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
package com.healthline.ktm.fbo;

import javax.persistence.EntityNotFoundException;
import javax.persistence.NonUniqueResultException;
import javax.persistence.Query;

import org.springframework.stereotype.Service;
import org.springframework.util.DigestUtils;
import org.springframework.validation.Errors;
import org.springframework.validation.Validator;

import com.healthline.ktm.domain.Person;

@Service("changePasswordValidator")
public class ChangePasswordValidator implements Validator {

  @Override
  public boolean supports(Class<?> clazz) {
    return ChangePasswordForm.class.equals(clazz);
  }

  @Override
  public void validate(Object target, Errors errors) {
    ChangePasswordForm form = (ChangePasswordForm) target;
    String emailAddress = form.getEmailAddress();
    try {
      Query query = Person.findPeopleByEmailAddress(emailAddress);
      Person person = (Person) query.getSingleResult();
      String storedPassword = person.getPassword();
      String currentPassword = DigestUtils.md5DigestAsHex(
        form.getOldPassword().getBytes());
      if (! currentPassword.equals(storedPassword)) {
        errors.rejectValue("oldPassword", "changepassword.invalidpassword");
      }
      String newPassword = form.getNewPassword();
      String newPasswordAgain = form.getNewPasswordAgain();
      if (! newPassword.equals(newPasswordAgain)) {
        errors.reject("changepassword.passwordsnomatch");
      }
    } catch (EntityNotFoundException e) {
      errors.rejectValue("emailAddress", "changepassword.invalidemailaddress");
    } catch (NonUniqueResultException e) {
      errors.rejectValue("emailAddress", 
        "changepassword.duplicateemailaddress");
    }
  }
}
On the JSP side, I modified the generated index.jspx to look like this, using snippets from the various other generated JSPX files to make the end result look similar to the other forms.
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<div xmlns:c="http://java.sun.com/jsp/jstl/core" 
    xmlns:form="http://www.springframework.org/tags/form" 
    xmlns:jsp="http://java.sun.com/JSP/Page" 
    xmlns:spring="http://www.springframework.org/tags" version="2.0">
  <jsp:output omit-xml-declaration="yes"/>
  <script type="text/javascript">dojo.require('dijit.TitlePane');dojo.require('dijit.form.SimpleTextarea');dojo.require('dijit.form.FilteringSelect');</script>
  <div id="_title_div">
    <spring:message code="label.changepassword" var="title_msg"/>
    <script type="text/javascript">Spring.addDecoration(new Spring.ElementDecoration({elementId : '_title_div', widgetType : 'dijit.TitlePane', widgetAttrs : {title: '${title_msg}'}})); </script>
    <spring:url value="/changepassword" var="form_url"/>
    <spring:message var="title" code="label.changepassword"/>
    <script type="text/javascript">Spring.addDecoration(new Spring.ElementDecoration({elementId : '_title_div', widgetType : 'dijit.TitlePane', widgetAttrs : {title: '${title_msg}'}})); </script>
    <form:form action="/ktm/changepassword/update" method="POST" commandName="changePasswordForm">
      <div id="changepassword_emailaddress">
        <label for="_emailaddress_id">Email Address:</label>
        <form:input cssStyle="width:250px" id="_changepassword_emailaddress" maxlength="30" path="emailAddress"/>
        <br/>
        <form:errors cssClass="errors" path="emailAddress"/>
      </div>
      <br/>
      <div id="changepassword_oldpassword">
        <label for="_oldpassword_id">Current Password:</label>
        <form:password cssStyle="width:250px" id="_changepassword_oldpassword" maxlength="30" path="oldPassword"/>
        <br/>
        <form:errors cssClass="errors" path="oldPassword"/>
      </div>
      <br/>
      <div id="changepassword_newpassword">
        <label for="_newpassword_id">New Password:</label>
        <form:password cssStyle="width:250px" id="_changepassword_newpassword" maxlength="30" path="newPassword"/>
        <br/>
        <form:errors cssClass="errors" path="newPassword"/>
      </div>
      <br/>
      <div id="changepassword_newpasswordagain">
        <label for="_newpasswordagain_id">New Password (again):</label>
        <form:password cssStyle="width:250px" id="_changepassword_newpasswordagain" maxlength="30" path="newPasswordAgain"/>
        <br/>
        <form:errors cssClass="errors" path="newPasswordAgain"/>
      </div>
      <br/><br/>
      <div class="submit" id="changepassword_submit">
        <spring:message code="button.save" var="save_button"/>
        <script type="text/javascript">Spring.addDecoration(new Spring.ValidateAllDecoration({elementId: 'proceed', event : 'onclick'}));</script>
        <input id="proceed" type="submit" value="${save_button}"/>
      </div>
      <br/>
      <form:errors cssClass="errors" delimiter="&lt;p/&gt;"/>
    </form:form>
  </div>
</div>
The thanks.jspx is even simpler. Here it is:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<div xmlns:c="http://java.sun.com/jsp/jstl/core" 
    xmlns:form="http://www.springframework.org/tags/form" 
    xmlns:jsp="http://java.sun.com/JSP/Page" 
    xmlns:spring="http://www.springframework.org/tags" version="2.0">
  <jsp:output omit-xml-declaration="yes"/>
  <script type="text/javascript">dojo.require('dijit.TitlePane');dojo.require('dijit.form.SimpleTextarea');dojo.require('dijit.form.FilteringSelect');</script>
  <div id="_title_div">
    <spring:message code="label.changepassword" var="title_msg"/>
    <script type="text/javascript">Spring.addDecoration(new Spring.ElementDecoration({elementId : '_title_div', widgetType : 'dijit.TitlePane', widgetAttrs : {title: '${title_msg}'}})); </script>
    <spring:url value="/changepassword" var="form_url"/>
    <spring:message var="title" code="label.changepassword"/>
    <script type="text/javascript">Spring.addDecoration(new Spring.ElementDecoration({elementId : '_title_div', widgetType : 'dijit.TitlePane', widgetAttrs : {title: '${title_msg}'}})); </script>
    <spring:message code="changepassword.thankyoumessage" var="thankyou_message"/>
    <h3>${thankyou_message}</h3>
  </div>
</div>
As with the accessDenied.jspx, I had to add a bunch of labels for values of keys that are referred to from the JSPX files and the Validator. Here they are (for messages.properties).
1
2
3
4
5
6
7
#changepassword
changepassword.invalidpassword=Invalid Current Password
changepassword.passwordsnomatch=Passwords do not match
changepassword.invalidemailaddress=Invalid Email Address
changepassword.duplicateemailaddress=\
Duplicate Email Address, contact administrator
changepassword.thankyoumessage=Your password has been changed.
I also had to register the thanks.jspx into the views.xml tiles definition file for changepassword, similar to the index.jspx that was already set in there by Roo.
Once all this was set up, clicking the Change Password link from the left nav led to the form shown on the left below. On hitting submit after entering my email address, old and new password, I get the confirmation message shown on the right below.

Conclusion

I still haven't gotten to the "interesting" part of my application :-). But the process of customizing the standard Roo authentication template for my own purposes has taught me a great deal. I found it fairly easy to do the customization, even though a lot of time was spent trying to find all the places to update. But that is a one time learning effort, the process should go much quicker the next time.
Building the Change Password functionality has also taught me about the mechanics of building a manual Roo controller, and the places to add and update, so hopefully I will be able to concentrate on application logic for the next (and final) part of this project.

没有评论:

发表评论