• No se han encontrado resultados

Comisión de Consenso

PREREQUISITE

Recipe 4.2 Externalizing strings in the view

KEY TECHNOLOGIES

Spring Web MVC, Spring binding and validation APIs, JSR 303 Bean Validation, JSR 223 Java Scripting, Hibernate Validator, Spring form tag library

Background

No matter how intuitive your registration form, people will accidentally or even inten-tionally fill it out with invalid information. You treat such errors as user errors rather than system or application exceptions, meaning you usually want to explain the error to the user in nontechnical language and help them overcome it.

Problem

When users submit form data, validate it before performing further processing. If there are errors, help the user understand what went wrong and how to address the issue.

Solution

At the highest level, this recipe addresses two types of validation:

3 The spring and form tag libraries come from the org.springframework.web.servlet artifact, and the corresponding tag library descriptors are spring.tld and spring-form.tld, respectively. You can find these inside the JAR’s META-INF directory.

Field filtering—Ensure that all submitted field names are permissible. In general, clients shouldn’t be allowed to submit fields that don’t appear on the form.

Field validation—Ensure that all submitted field values follow validation rules.

We’ll set the stage with an architectural overview. Spring Web MVC supports both types of validation just described using three key APIs: Spring’s form-binding API, Spring’s validation API, and JSR 303 Bean Validation. See figure 4.4.

Here’s how it works. When users submit HTML form data, Spring Web MVC uses the form-binding API to bind the HTTP parameters to form bean properties in an automated fashion. In certain cases—for example, when a form bean is performing double duty as a persistent entity—the form bean may have properties that aren’t intended binding targets. The form-binding API allows you to filter out unwanted HTTP parameters by silently ignoring them during binding.

When Spring Web MVC invokes a form-submission request-handler method, such as postRegistrationForm(), it passes in the form data. In general, the form data is encap-sulated within a form bean, and you want to validate it. This is the domain of JSR 303 Bean Validation. Spring Web MVC uses JSR 303 to validate form data encapsulated in this fashion, and developers use the Spring validation API (specifically, the BindingResult interface) from within a controller to determine whether the bean is valid.

Sometimes you need to perform a bit of custom validation logic. You’ll see an example. Spring’s validation API provides a programmatic interface for implementing such logic.

That will do for an overview. Let’s add field filtering to the AccountController.

FIELD FILTERING VIA @INITBINDER AND WEBDATABINDER

Recall that Spring Web MVC automatically binds HTML forms to an underlying form bean. Although this is a major convenience to application developers, it raises a security

Figure 4.4 Validation in Spring Web MVC. The form-binding API handles field filtering, JSR 303 handles bean validation, and there’s a Spring validation API for custom logic.

concern because it allows attackers to inject data into form bean properties that aren’t intended to be accessed via the HTML form. You’re not in that situation here, but it’s a common state of affairs in cases where a single model object performs double duty as both a form bean and a persistent entity. In such cases you need a way to guard against data injection.4

Spring Web MVC supports this using @InitBinder methods. Add the following method to AccountController:

@InitBinder

public void initBinder(WebDataBinder binder) { binder.setAllowedFields(new String[] {

"username", "password", "confirmPassword", "firstName", "lastName", "email", "marketingOk", "acceptTerms" });

}

The @InitBinder annotation tells Spring Web MVC to call this method when initializ-ing the WebDataBinder responsible for bindinitializ-ing HTTP parameters to form beans. The setAllowedFields() method defines a whitelist of bindable form bean fields. The binder silently ignores unlisted fields.

Now let’s examine field validation.

VALIDATING THE FORM DATA

Several steps are involved in adding form validation to your app:

1 Add a JSR 303 implementation to the classpath.

2 Add validation annotations to AccountForm.

3 Add @ModelAttribute, @Valid, BindingResult, and validation logic to Account-Controller.

4 Create a ValidationMessages.properties resource bundle, and update the messages.properties resource bundle.

4 Consider the case where you use a single Account POJO to serve as both an entity and a form bean. The entity might have an enabled field that indicates whether the account is enabled. You wouldn’t want clients to be able to manipulate that field by sending a value for the field to the form processor.

Whitelisting vs. blacklisting

The list of allowed fields is an example of a whitelist. The idea is that nothing gets through unless it’s on the whitelist.

There is an alternative approach called a blacklist. With a blacklist, everything gets through unless it’s on the blacklist.

Whitelists are generally more secure, because they start with an assumption of dis-trust rather than dis-trust. But blacklists have their place as well. For example, you might filter out comment spammers using an IP blacklist, because it wouldn’t be practical to use a whitelist for web traffic.

5 Update registrationForm.jsp to display error messages.

6 Confirm that beans-web.xml has <mvc:annotation-driven> (for validation) and a message source (for certain custom error messages).

There’s a lot to cover. Let’s start at the top of the list and work our way down.

STEP 1. PLACING A JSR 303 IMPLEMENTATION ON THE CLASSPATH

Your Maven build takes care of placing Hibernate Validator 4, a JSR 303 implementation, on the classpath. Spring Web MVC will automatically pick it up. You can therefore move on to the next step, which is marking up AccountForm with validation annotations.

STEP 2. ADDING BEAN-VALIDATION ANNOTATIONS TO THE FORM BEAN

The following listing updates the AccountForm from listing 4.1 by adding validation annotations.

script = "_this.confirmPassword.equals(_this.password)", message = "account.password.mismatch.message")

public class AccountForm { ... fields same as before ...

@NotNull

@Size(min = 1, max = 50)

public String getUsername() { return username; } @NotNull

@Size(min = 6, max = 50)

public String getPassword() { return password; } @NotNull

@Size(min = 6, max = 50) @Email

public String getEmail() { return email; } @AssertTrue(message = "{account.acceptTerms.assertTrue.message}") public boolean getAcceptTerms() { return acceptTerms; } ... other methods same as before ...

}

The previous listing uses the Bean Validation (JSR 303) standard and Hibernate Vali-dator to specify validation constraints. You attach the annotations either to the fields

Listing 4.7 AccountForm.java, with validation annotations (updates listing 4.1)

@ScriptAssert for

or to the getters. At

C

you indicate that the username property can’t be null, and its size must be 1–50 characters in length. At

D

you use the Hibernate-specific @Email annotation to ensure that the email property represents a valid e-mail address. At

E

you require that the acceptTerms property be true for validation to succeed, and you specify a message code to use when the validation fails. (More on that shortly.)

Finally, you declare a class-level @ScriptAssert annotation at

B

. This Hibernate annotation, which was introduced with Hibernate Validator 4.1, allows you to use a script to express validation constraints involving multiple fields. Here you use JavaScript to assert that the password and confirmation must be equal. (The Rhino JavaScript engine is automatically available if you’re using Java 6; otherwise you’ll need to place a JSR 223–compliant [Scripting for the Java Platform] script engine JAR on the classpath.) In addition to JavaScript, there are many other language options, including Groovy, Ruby, Python, FreeMarker, and Velocity.

Next you update AccountController to validate the account bean.

STEP 3. UPDATING THE CONTROLLER TO VALIDATE THE FORM DATA

The next listing shows how to update the AccountController from listing 4.2 to sup-port both Bean Validation via JSR 303 and custom password validation.

package com.springinpractice.ch04.web;

import javax.validation.Valid;

import org.springframework.stereotype.Controller;

import org.springframework.ui.Model;

import org.springframework.validation.BindingResult;

import org.springframework.validation.ObjectError;

import org.springframework.web.bind.WebDataBinder;

import org.springframework.web.bind.annotation.InitBinder;

import org.springframework.web.bind.annotation.ModelAttribute;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.bind.annotation.RequestMethod;

@Controller

@RequestMapping("/users") public class AccountController {

private static final String VN_REG_FORM = "users/registrationForm";

private static final String VN_REG_OK = "redirect:registration_ok";

@InitBinder

public void initBinder(WebDataBinder binder) { binder.setAllowedFields(new String[] {

"username", "password", "confirmPassword", "firstName", "lastName", "email", "marketingOk", "acceptTerms" });

}

@RequestMapping(value = "new", method = RequestMethod.GET) public String getRegistrationForm(Model model) {

model.addAttribute("account", new AccountForm());

return VN_REG_FORM;

}

Listing 4.8 AccountController.java, updated to validate form data (updates listing 4.2)

@RequestMapping(value = "", method = RequestMethod.POST) public String postRegistrationForm(

@ModelAttribute("account") @Valid AccountForm form, BindingResult result) {

convertPasswordError(result);

return (result.hasErrors() ? VN_REG_FORM : VN_REG_OK);

}

private static void convertPasswordError(BindingResult result) { for (ObjectError error : result.getGlobalErrors()) {

String msg = error.getDefaultMessage();

if ("account.password.mismatch.message".equals(msg)) { if (!result.hasFieldErrors("password")) {

You add @ModelAttribute and @Valid annotations to the AccountForm parameter

B

. The @ModelAttribute annotation causes the account bean to be placed automatically on the Model object for display by the view, using the key "account". The @Valid annotation causes the bean to be validated on its way into the method.

Spring exposes the validation result via the BindingResult object

C

. This is how you can tell whether bean validation turned up any errors. You can also programmati-cally add new errors to the BindingResult by using its various reject() and reject-Value() methods. The BindingResult method parameter must immediately follow the form bean in the method parameter list.

The logic of the postRegistrationForm() method itself is straightforward. You call convertPasswordError()

D

, which converts the global error that @ScriptAssert generates into an error on the password field. You use the rejectValue() method to do this, as mentioned, passing in an error code "error.mismatch". This error code resolves to one of the following message codes, depending on which message codes appear in the resource bundle:

error.mismatch.account.password (error code + . + object name + . + field name)

error.mismatch.password (error code + . + field name)

error.mismatch.java.lang.String (error code + . + field type)

error.mismatch (error code)

These message codes are listed in priority order: if the resource bundle contains the first message code, then that’s the resolution, and so forth.5 The first message code does in fact appear in messages.properties. See the Javadoc for Spring’s

5 It’s probably worth emphasizing the fact that despite superficial similarities, error codes and message codes aren’t the same thing. Validation errors have associated codes, and these generally map to a set of resource bundle message codes, which in turn map to error messages. It’s pretty easy to get these mixed up.

@ModelAttribute and @Valid

B

BindingResult

to record errors

C

Converts password errors

D

Routing logic

E

DefaultMessageCodesResolver for more information on the rules for converting error codes to message codes.

Finally, once you’ve processed any password errors, you check to see whether there were any validation errors, and route to a success or failure page accordingly

E

. Notice that you’re using the view name constants defined at the top of the file.

Let’s take a more detailed look at the error messages here.

STEP 4. CONFIGURING ERROR MESSAGES

First let’s talk about the default JSR 303 and Hibernate Validator messages. Strictly speaking, you don’t have to override them at all. But the defaults aren’t particularly user-centric (one of the defaults, for example, references regular expressions), so you’ll change the messages for the constraints you’re using. JSR 303 supports this by allowing you to place a ValidationMessages.properties resource bundle at the top of the classpath. You’ll use this resource bundle not only to override the JSR 303 and Hibernate Validator defaults, but also to define an error message specific to the acceptTerms property.

javax.validation.constraints.Size.message=

Please enter {min}-{max} characters.

org.hibernate.validator.constraints.Email.message=

Please enter a valid e-mail address.

account.acceptTerms.assertTrue.message=

You must accept the terms of use to register.

You override the default JSR 303 @Size

B

and default Hibernate Validator @Email

C

error messages as shown. The message for @Size is effectively a template that gener-ates messages with the minimum and maximum sizes substituted in. You aren’t over-riding the default JSR 303 error message for @NotNull because that error shouldn’t occur if you don’t forget to implement any form fields. (And if you do, the default error message is OK because this is a programming error rather than an end user error.) Finally, you define an error message for the acceptTerms property at

D

.

In addition to the JSR 303 error messages, you need messages for the Spring-man-aged errors. You’ll add these to messages.properties because ValidationMes-sages.properties is for JSR 303 error messages. Although it can be a little confusing to split the error messages into two resource bundles, it helps to do exactly this. The reason is that JSR 303 and Spring use different schemes for resolving error codes to message codes, and mixing error messages in a single resource bundle can make it harder to keep message codes straight.

Add the following two error messages to messages.properties:

error.global=Please fix the problems below.

error.mismatch.account.password=Your passwords do not match. Please try

again.

Now you have an error message for the password-mismatch error code you used in the controller. You’ll use the global error message in the form JSP.

Listing 4.9 ValidationMessages.properties, for JSR 303 error messages

Overrides default @Size message

B

Overrides default @Email message

C

Defines message for acceptTerms

D

STEP 5. DISPLAYING VALIDATION ERRORS IN THE VIEW

You use the Spring form tag library to display both a global error message (“Please fix the problems below”) and error messages on the form bean, as illustrated in figure 4.5.

The text fields for properties with errors are visually distinct (they have red borders), although it’s hard to tell if you’re viewing the figure in black and white. Also, fields are prepopulated with the user’s submitted data so the user can fix mistakes instead of reen-tering all the data. The only exceptions are the two password fields, which for security reasons you don’t prepopulate. The user has to reenter those values.

To accomplish this design, you’ll need to revise registrationForm.jsp as shown next. (See the code download for the full version.)

<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %>

<spring:message var="pageTitle" code="newUserRegistration.pageTitle" />

<spring:message var="msgAllFieldsRequired"

code="newUserRegistration.message.allFieldsRequired" />

<html>

<head><title>${pageTitle}</title></head>

<body>

<form:form cssClass="main" action="." modelAttribute="account">

<form:errors path="*">

<div><spring:message code="error.global" /></div>

</form:errors>

<h1>${pageTitle}</h1>

<div>${msgAllFieldsRequired}</div>

<div>

Listing 4.10 registrationForm.jsp, updated with validation error messages

Figure 4.5 The revised registration form, with a global error message and field-level error messages

Displays global error message

B

<div>

<spring:message code="newUserRegistration.label.username" />

<form:input path="username" cssClass="short"

cssErrorClass="short error" />

</div>

<form:errors path="username">

<div><form:errors path="username" htmlEscape="false" /></div>

</form:errors>

</div>

... other fields and submit button ...

</form:form>

</body>

</html>

At

B

you display the global error message. The tag logic here is to look for the exis-tence of any error whatsoever—a global error or a field error—and if there is one, dis-play the global error message. The path="*" piece is your error wildcard.

You display the username field at

C

. By using the <form:input> tag, you get data prepopulation for free. This time around you include the CSS attributes because there’s something interesting to show off. The cssClass attribute specifies the

<input> element’s CSS class when there’s no error. (The short class just sets the text-field width in the sample code.) The cssErrorClass attribute specifies the class when there is an error. This allows you to change the visual appearance of the text field when there’s an error.

In addition to the text field, you want to display the error message, and that’s what’s going on at

D

. You select the specific form bean property with the path attri-bute and use htmlEscape="false" so you can include HTML in the error message if desired.

The other fields are essentially the same, so we’ve suppressed them. Again, please see the code download for the full version of the code.

The last step in the process is to configure the application for validation.

STEP 6. CONFIGURING THE APP FOR VALIDATION

Surprise—you’ve already done what you need to do here. In recipe 4.1 you included the <mvc:annotation-driven> configuration inside beans-web.xml, which among sev-eral other things activates JSR 303 Bean Validation, causing Spring Web MVC to recog-nize the @Valid annotation. In recipe 4.2 you added a MessageSource.

Start up your browser and give the code a spin.

Discussion

The preceding recipe handles validation in the web tier. There’s nothing wrong with that, because the constraints you’ve used so far make sense as web tier constraints. But it’s important to bear in mind that modern validation frameworks like Spring valida-tion and JSR 303 validation abandon the traditional assumption that bean validation occurs exclusively in the web tier. In the following recipe, you’ll see what validation looks like in the service tier.

Displays username field

C

Displays username error messages

D