Pengesahan Custom MVC Spring

1. Gambaran keseluruhan

Secara amnya, apabila kita perlu mengesahkan input pengguna, Spring MVC menawarkan validator standard yang telah ditetapkan.

Namun, apabila kita perlu mengesahkan jenis input yang lebih khusus, kita mempunyai kemungkinan untuk membuat logik pengesahan tersuai kita sendiri .

Dalam artikel ini, kami akan melakukannya - kami akan membuat validator khusus untuk mengesahkan borang dengan bidang nombor telefon, kemudian menunjukkan validator khusus untuk beberapa bidang.

Artikel ini memberi tumpuan kepada Spring MVC. Artikel kami Validation in Spring Boot menerangkan bagaimana melakukan validasi tersuai di Spring Boot.

2. Persediaan

Untuk memanfaatkan API, tambahkan kebergantungan pada fail pom.xml anda :

 org.hibernate hibernate-validator 6.0.10.Final  

Versi ketergantungan terkini boleh diperiksa di sini.

Sekiranya kita menggunakan Spring Boot, maka kita hanya dapat menambahkan spring-boot-starter-web, yang akan membawa kebergantungan hibernate-validator juga.

3. Pengesahan Adat

Membuat validator tersuai memerlukan kita melancarkan anotasi kita sendiri dan menggunakannya dalam model kita untuk menguatkuasakan peraturan pengesahan.

Oleh itu, mari buat pengesah tersuai kami - yang memeriksa nombor telefon . Nombor telefon mestilah nombor dengan lebih daripada lapan digit tetapi tidak lebih daripada 11 digit.

4. Anotasi Baru

Mari buat @interface baru untuk menentukan penjelasan kami:

@Documented @Constraint(validatedBy = ContactNumberValidator.class) @Target( { ElementType.METHOD, ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public @interface ContactNumberConstraint { String message() default "Invalid phone number"; Class[] groups() default {}; Class[] payload() default {}; }

Dengan anotasi @Constraint , kami menentukan kelas yang akan mengesahkan bidang kami, mesej () adalah mesej ralat yang ditunjukkan di antara muka pengguna dan kod tambahan adalah kod boilerplate yang paling sesuai dengan standard Spring.

5. Membuat Pengesah

Mari sekarang buat kelas pengesahan yang menguatkuasakan peraturan pengesahan kami:

public class ContactNumberValidator implements ConstraintValidator { @Override public void initialize(ContactNumberConstraint contactNumber) { } @Override public boolean isValid(String contactField, ConstraintValidatorContext cxt) { return contactField != null && contactField.matches("[0-9]+") && (contactField.length() > 8) && (contactField.length() < 14); } }

Kelas pengesahan melaksanakan antara muka ConstraintValidator dan mesti melaksanakan kaedah isValid ; dalam kaedah inilah kita menentukan peraturan pengesahan kita.

Secara semula jadi, kita akan menggunakan peraturan pengesahan yang sederhana di sini, untuk menunjukkan bagaimana pengesahan berfungsi.

ConstraintValidator d menggunakan logik untuk mengesahkan kekangan yang diberikan untuk objek tertentu. Pelaksanaan mesti mematuhi sekatan berikut:

  • objek mesti diselesaikan ke jenis yang tidak berparameter
  • parameter generik objek mestilah jenis wildcard tanpa had

6. Mengaplikasikan Anotasi Pengesahan

Dalam kes kami, kami telah membuat kelas sederhana dengan satu bidang untuk menerapkan peraturan pengesahan. Di sini, kami menyediakan medan beranotasi untuk disahkan:

@ContactNumberConstraint private String phone;

Kami menentukan medan rentetan dan memberi penjelasan dengan anotasi tersuai kami @ContactNumberConstraint. Di pengawal kami, kami membuat pemetaan dan menangani ralat jika ada:

@Controller public class ValidatedPhoneController { @GetMapping("/validatePhone") public String loadFormPage(Model m) { m.addAttribute("validatedPhone", new ValidatedPhone()); return "phoneHome"; } @PostMapping("/addValidatePhone") public String submitForm(@Valid ValidatedPhone validatedPhone, BindingResult result, Model m) { if(result.hasErrors()) { return "phoneHome"; } m.addAttribute("message", "Successfully saved phone: " + validatedPhone.toString()); return "phoneHome"; } }

Kami menentukan pengawal mudah ini yang mempunyai satu halaman JSP , dan menggunakan kaedah submForm untuk menegakkan pengesahan nombor telefon kami.

7. Pandangan

Pandangan kami adalah halaman JSP asas dengan bentuk yang mempunyai satu bidang. Apabila pengguna menyerahkan borang, maka bidang akan disahkan oleh pengesah khusus kami dan mengarahkan ke halaman yang sama dengan mesej pengesahan yang berjaya atau gagal:

 Phone:      

8. Ujian

Sekarang mari kita uji pengawal kami dan periksa sama ada ia memberi respons dan pandangan yang sesuai:

@Test public void givenPhonePageUri_whenMockMvc_thenReturnsPhonePage(){ this.mockMvc. perform(get("/validatePhone")).andExpect(view().name("phoneHome")); }

Juga, mari kita uji bahawa bidang kami disahkan, berdasarkan input pengguna:

@Test public void givenPhoneURIWithPostAndFormData_whenMockMVC_thenVerifyErrorResponse() { this.mockMvc.perform(MockMvcRequestBuilders.post("/addValidatePhone"). accept(MediaType.TEXT_HTML). param("phoneInput", "123")). andExpect(model().attributeHasFieldErrorCode( "validatedPhone","phone","ContactNumberConstraint")). andExpect(view().name("phoneHome")). andExpect(status().isOk()). andDo(print()); }

Dalam ujian ini, kami memberi pengguna input "123", dan - seperti yang kami harapkan - semuanya berjalan lancar dan kami melihat kesalahan di pihak pelanggan .

9. Pengesahan Tahap Kelas Tersuai

A custom validation annotation can also be defined at the class level to validate more than one attribute of the class.

A common use case for this scenario is verifying if two fields of a class have matching values.

9.1. Creating the Annotation

Let's add a new annotation called FieldsValueMatch that can be later applied to a class. The annotation will have two parameters field and fieldMatch that represent the names of the fields to compare:

@Constraint(validatedBy = FieldsValueMatchValidator.class) @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) public @interface FieldsValueMatch { String message() default "Fields values don't match!"; String field(); String fieldMatch(); @Target({ ElementType.TYPE }) @Retention(RetentionPolicy.RUNTIME) @interface List { FieldsValueMatch[] value(); } }

We can see our custom annotation also contains a List sub-interface for defining multiple FieldsValueMatch annotations on a class.

9.2. Creating the Validator

Next, we need to add the FieldsValueMatchValidator class that will contain the actual validation logic:

public class FieldsValueMatchValidator implements ConstraintValidator { private String field; private String fieldMatch; public void initialize(FieldsValueMatch constraintAnnotation) { this.field = constraintAnnotation.field(); this.fieldMatch = constraintAnnotation.fieldMatch(); } public boolean isValid(Object value, ConstraintValidatorContext context) { Object fieldValue = new BeanWrapperImpl(value) .getPropertyValue(field); Object fieldMatchValue = new BeanWrapperImpl(value) .getPropertyValue(fieldMatch); if (fieldValue != null) { return fieldValue.equals(fieldMatchValue); } else { return fieldMatchValue == null; } } }

The isValid() method retrieves the values of the two fields and checks if they are equal.

9.3. Applying the Annotation

Let's create a NewUserForm model class intended for data required for user registration, that has two email and password attributes, along with two verifyEmail and verifyPassword attributes to re-enter the two values.

Since we have two fields to check against their corresponding matching fields, let's add two @FieldsValueMatch annotations on the NewUserForm class, one for email values, and one for password values:

@FieldsValueMatch.List({ @FieldsValueMatch( field = "password", fieldMatch = "verifyPassword", message = "Passwords do not match!" ), @FieldsValueMatch( field = "email", fieldMatch = "verifyEmail", message = "Email addresses do not match!" ) }) public class NewUserForm { private String email; private String verifyEmail; private String password; private String verifyPassword; // standard constructor, getters, setters }

To validate the model in Spring MVC, let's create a controller with a /user POST mapping that receives a NewUserForm object annotated with @Valid and verifies whether there are any validation errors:

@Controller public class NewUserController { @GetMapping("/user") public String loadFormPage(Model model) { model.addAttribute("newUserForm", new NewUserForm()); return "userHome"; } @PostMapping("/user") public String submitForm(@Valid NewUserForm newUserForm, BindingResult result, Model model) { if (result.hasErrors()) { return "userHome"; } model.addAttribute("message", "Valid form"); return "userHome"; } }

9.4. Testing the Annotation

To verify our custom class-level annotation, let's write a JUnit test that sends matching information to the /user endpoint, then verifies that the response contains no errors:

public class ClassValidationMvcTest { private MockMvc mockMvc; @Before public void setup(){ this.mockMvc = MockMvcBuilders .standaloneSetup(new NewUserController()).build(); } @Test public void givenMatchingEmailPassword_whenPostNewUserForm_thenOk() throws Exception { this.mockMvc.perform(MockMvcRequestBuilders .post("/user") .accept(MediaType.TEXT_HTML). .param("email", "[email protected]") .param("verifyEmail", "[email protected]") .param("password", "pass") .param("verifyPassword", "pass")) .andExpect(model().errorCount(0)) .andExpect(status().isOk()); } }

Seterusnya, mari kita tambahkan juga ujian JUnit yang menghantar maklumat yang tidak sepadan ke titik pengguna / pengguna dan menegaskan bahawa hasilnya akan mengandungi dua kesalahan:

@Test public void givenNotMatchingEmailPassword_whenPostNewUserForm_thenOk() throws Exception { this.mockMvc.perform(MockMvcRequestBuilders .post("/user") .accept(MediaType.TEXT_HTML) .param("email", "[email protected]") .param("verifyEmail", "[email protected]") .param("password", "pass") .param("verifyPassword", "passsss")) .andExpect(model().errorCount(2)) .andExpect(status().isOk()); }

10. Ringkasan

Dalam artikel ringkas ini, kami telah menunjukkan cara membuat validator tersuai untuk mengesahkan medan atau kelas dan memasukkannya ke Spring MVC.

Seperti biasa, anda boleh mendapatkan kod dari artikel di Github.