验证可能很棘手且困难,在进行验证时需要考虑一些事情......
验证注意事项
-
MVC 中的模型(模型、视图、控制器)与域模型不同,通常也不应该相同。请参阅@wim-deblauwe 的评论和下面的问答部分。
- 通常,用户界面中显示的内容与域模型中可用的内容不同。
- 将 @Valid 注释放入域模型中意味着在使用域模型的每种形式中,都将应用相同的 @Valid 规则。这并非总是如此。旁注:这可能不适用于超级简单的 CRUD(创建、读取、更新、删除)应用程序,但一般来说,大多数应用程序比纯粹的 CRUD 更复杂。
-
由于 Spring 在表单提交期间自动设置值的方式,使用真实的域模型对象作为表单支持对象存在严重的安全问题。例如,如果我们使用带有密码字段的 User 对象作为表单支持对象,则浏览器开发人员工具可以操纵表单来发送密码字段的新值,现在该新值将被保留。
-
通过 html 表单输入的所有数据实际上都是字符串数据,稍后需要将其转置为其实际数据类型(整数、双精度、枚举等)。
-
在我看来,有不同类型的验证需要以不同的时间顺序进行。
- 必需的检查发生在类型检查(整数、双精度、枚举等)、有效值范围之前,最后进行持久性检查(唯一性、先前持久值等)
- If there are any errors in a temporal level, then don't check anything later.
- 这可以防止最终用户在同一错误消息中收到诸如需要电话号码、电话号码不是数字、电话号码格式不正确等错误。
-
验证器之间不应该存在任何时间耦合。这意味着如果字段是可选的,那么如果值不存在,“数据类型”验证器不应验证失败。请参阅下面的验证器。
Example
领域对象/业务对象:
@Entity
public class Person {
private String identifier;
private String name;
private int year;
public String getIdentifier() {
return identifier;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getYear() {
return year;
}
public void setYear(int year) {
this.year = year;
}
}
要通过 Spring MVC 控制器填充 html 表单,我们将创建一个表示该表单的特定对象。这还包括所有验证规则:
@GroupSequence({Required.class, Type.class, Data.class, Persistence.class, CreateOrUpdatePersonForm.class})
public class CreateOrUpdatePersonForm {
@NotBlank(groups = Required.class, message = "Name is required.")
private String name;
@NotBlank(groups = Required.class, message = "Year is required.")
@ValidInteger(groups = Type.class, message = "Year must be a number.")
@ValidDate(groups = Data.class, message = "Year must be formatted yyyy.")
private String year;
public CreateOrUpdatePersonForm(Person person) {
this.name = person.getName();
this.year = Integer.valueOf(person.getYear);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getYearStr() {
this.year;
}
public void setYearStr(String year) {
this.year = year;
}
public int getYear() {
return Integer.valueOf(this.year);
}
}
然后在您的控制器中使用新的 CreateOrUpdatePersonForm 对象:
@Controller
public class PersonController {
...
@ModelAttribute("command")
public CreateOrUpdatePersonForm setupCommand(@RequestParam("identifier") Person person) {
return new CreateOrUpdatePersonForm(person);
}
//@PreAuthorize("hasRole('ADMIN')")
@RequestMapping(value = "/person/{person}/form.html", method = RequestMethod.GET)
public ModelAndView getForm(@RequestParam("person") Person person) {
return new ModelAndView("/form/person");
}
//@PreAuthorize("hasRole('ADMIN')")
@RequestMapping(value = "/person/{person}/form.html", method = RequestMethod.POST)
public ModelAndView postForm(@RequestParam("person") Person person, @ModelAttribute("command") @Valid CreateOrUpdatePersonForm form,
BindingResult bindingResult, RedirectAttributes redirectAttributes) {
ModelAndView modelAndView;
if (bindingResult.hasErrors()) {
modelAndView = new ModelAndView("/form/person");
} else {
this.personService.updatePerson(person.getIdentifier(), form);
redirectAttributes.addFlashAttribute("successMessage", "Person updated.");
modelAndView = new ModelAndView("redirect:/person/" + person.getIdentifier() + ".html");
}
return modelAndView;
}
}
@ValidInteger 和 @ValidDate 是我们自己编写的验证器。
@有效整数:
public class ValidIntegerValidator implements ConstraintValidator<ValidInteger, String> {
@Override
public void initialize(ValidInteger annotation) {
}
@Override
public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
boolean valid = true;
if (StringUtils.hasText(value)) {
try {
Integer.parseInteger
(value);
} catch (NumberFormatException e) {
valid = false;
}
}
return valid;
}
}
@Target({METHOD, FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = ValidIntegerValidator.class)
@Documented
public @interface ValidInteger {
String message() default "{package.valid.integer}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
@有效日期
public class ValidDateValidator implements ConstraintValidator<ValidDate, String> {
private String format;
@Override
public void initialize(ValidDate annotation) {
this.format = annotation.format();
}
@Override
public boolean isValid(String inputDate, ConstraintValidatorContext constraintValidatorContext) {
boolean valid = true;
if (StringUtils.hasText(inputDate)) {
SimpleDateFormat dateFormat = new SimpleDateFormat(format);
dateFormat.setLenient(false);
try {
dateFormat.parse(inputDate);
} catch (ParseException e) {
valid = false;
}
}
return valid;
}
}
@Target({METHOD, FIELD})
@Retention(RUNTIME)
@Constraint(validatedBy = ValidDateValidator.class)
@Documented
public @interface ValidDate {
String message() default "{package.dateformat}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String format();
}
然后在您的视图 jsp 或模板中,您需要显示错误(如果有):
<html>
...
<body>
<common:form-errors modelAttribute="command"/>
...
</body>
</html>
验证还有很多事情需要处理,例如比较两个字段,或者访问持久层来验证人名是否唯一,但这需要更多解释。
问答
问:您能否提供链接来解释不使用领域模型作为 MVC 模型背后的想法?
答: 当然,实体 VS 领域模型 VS 视图模型 https://stackoverflow.com/questions/24588838/entities-vs-domain-models-vs-view-models and 实体 vs 模型 vs 视图模型 https://stackoverflow.com/questions/26563444/entity-vs-model-vs-view-model
TL;DR:为域模型和 MVC 模型使用不同的对象是因为它减少了应用程序层之间的耦合,并保护我们的 UI 和域模型免受任一层中的更改的影响。
其他考虑因素
数据验证需要在应用程序的所有入口点进行:UI、API 以及读入的任何外部系统或文件。
API 只是计算机的 UI,需要遵循与人类 UI 相同的规则。
接受来自互联网的数据充满危险。限制较多比限制较少要好。这还包括确保没有任何奇怪的字符cough微软的1252字符编码 https://en.wikipedia.org/wiki/Windows-1252cough、Sql 注入、JavaScript 注入,确保您的数据库设置为 unicode,并了解设置为 512 个字符的列(具体取决于语言)由于代码点而实际上只能处理 256 个字符。