通常在编写Web应用程序时,我们希望在客户端执行验证以提供即时反馈,并在服务器端执行验证以确保数据的完整性和安全性。然而,客户端浏览器应用程序通常是用JavaScript编写的。服务器端可以使用Java、Php、Ruby、Python等多种语言编写。如果服务器端基于像node.js这样的东西,那么很容易在客户端和服务器上重用相同的验证代码,但如果服务器端基于Rails或Django(或任何其他你能想到的框架),怎样才能确保验证代码保持同步呢?不把同样的代码在多种语言中重新实现似乎有点冗余。
从我看到的项目来看,有三种常见策略:
完全重复客户端和服务器端验证。如果使用javascript前端和java/c#/ruby后端,则需要两个不同的代码库。您必须手动保持两者的逻辑同步。
进行最小限度的客户端验证。只检查非常基本的内容。让服务器端完成全部验证。让服务器端传递一种验证错误对象到客户端,并让客户端逻辑将其转换为UI消息(错误消息、红色边框等)。Asp.net MVC框架大致是这种模型的一个例子。
使用ajax在用户更改或离开每个控件时向服务器端发出验证调用。这可以允许您在服务器端完成所有验证,并减少用户的反馈等待时间,但可能会极大地增加客户端到服务器端的流量。
根据我的经验,选项1通常比维护选项2和3所需的额外代码和复杂性更容易。
我们的架构允许共享验证器代码:我们的后端是JAVA。前端是JavaScript/TypeScript(如果有关系,使用Angular)。JAVA后端处理所有业务事务。为了向用户报告MySQL和一些实用程序用户首选项,我们还编写了一个最小的NodeJS/Express后端。
为了重复使用验证,我们将一些端点从JAVA更改为NodeJS,然后NodeJS进行验证并在内部将请求正文传递给JAVA(通过系统调用或非公共API)。然后我们从JAVA代码中删除了验证。NodeJS/Express主要作为代理,它还检查请求的有效性。
这样我们就可以在前端和我们的最小后端之间共享TypeScript代码/类。
增加的开销:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.AllArgsConstructor;
import org.hibernate.validator.HibernateValidator;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.stream.Collectors;
import static java.util.stream.Collectors.groupingBy;
@RestController
@AllArgsConstructor
public class ValidationEndpoint {
static {
Locale.setDefault(Locale.GERMAN);
}
private final ObjectMapper objectMapper;
private final Validator validator;
@PostMapping("/")
public Map < String, List < String >> validate(@RequestBody String entity, @RequestParam String className) throws ClassNotFoundException, JsonProcessingException {
Class c = Class.forName(className);
Object jsonObject = objectMapper.readValue(entity, c);
return validator.validate(jsonObject).stream().map(constraintViolation -> {
Violation violation = new Violation();
violation.setMessage(constraintViolation.getMessage());
violation.setFieldName(constraintViolation.getPropertyPath().toString());
return violation;
}).collect(groupingBy(Violation::getFieldName, Collectors.mapping(Violation::getMessage, Collectors.toList())));
}
@PostMapping("/valida")
public List < String > validateField(@RequestBody String entity, @RequestParam String className, @RequestParam String propertyName) throws ClassNotFoundException, JsonProcessingException {
Class c = Class.forName(className);
Object jsonObject = objectMapper.readValue(entity, c);
ValidatorFactory validatorFactory = Validation.byProvider(HibernateValidator.class)
.configure()
.failFast(false)
.defaultLocale(Locale.GERMAN)
.buildValidatorFactory();
Validator validator = validatorFactory.getValidator();
return validator.validateProperty(jsonObject, propertyName).stream().map(constraintViolation -> {
Violation violation = new Violation();
violation.setMessage(constraintViolation.getMessage());
violation.setFieldName(constraintViolation.getPropertyPath().toString());
return violation;
}).map(Violation::getMessage).toList();
}
}